Skip to content

Commit 62ed5d0

Browse files
feat(analyzer): Implement SUI analyzer (#482)
Depends on: smartcontractkit/chainlink-sui#108 # Summary Implement SUI transaction analyzer from SUI decoder. # Features - Implement analyzer - Connect to `analyze.go` - Connect to `upf.go` - Move some Aptos functions to shared `utils` and `testhelper` files --------- Co-authored-by: RodrigoAD <[email protected]>
1 parent c035859 commit 62ed5d0

File tree

14 files changed

+1935
-123
lines changed

14 files changed

+1935
-123
lines changed

.changeset/big-dodos-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Implement SUI proposal analyzer

engine/cld/legacy/cli/mcmsv2/mcms_v2.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import (
3333
"go.uber.org/zap"
3434
"go.uber.org/zap/zapcore"
3535

36+
suibindings "github.com/smartcontractkit/chainlink-sui/bindings"
37+
3638
"github.com/smartcontractkit/chainlink-deployments-framework/chain"
3739
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
3840
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
@@ -1335,8 +1337,9 @@ func getExecutorWithChainOverride(cfg *cfgv2, chainSelector types.ChainSelector)
13351337
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
13361338
}
13371339
chain := cfg.blockchains.SuiChains()[uint64(chainSelector)]
1340+
entrypointEncoder := suibindings.NewCCIPEntrypointArgEncoder(metadata.RegistryObj)
13381341

1339-
return sui.NewExecutor(chain.Client, chain.Signer, encoder, metadata.McmsPackageID, metadata.Role, cfg.timelockProposal.ChainMetadata[chainSelector].MCMAddress, metadata.AccountObj, metadata.RegistryObj, metadata.TimelockObj)
1342+
return sui.NewExecutor(chain.Client, chain.Signer, encoder, entrypointEncoder, metadata.McmsPackageID, metadata.Role, cfg.timelockProposal.ChainMetadata[chainSelector].MCMAddress, metadata.AccountObj, metadata.RegistryObj, metadata.TimelockObj)
13401343
default:
13411344
return nil, fmt.Errorf("unsupported chain family %s", family)
13421345
}
@@ -1381,7 +1384,8 @@ func getTimelockExecutorWithChainOverride(cfg *cfgv2, chainSelector types.ChainS
13811384
if err != nil {
13821385
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
13831386
}
1384-
executor, err = sui.NewTimelockExecutor(chain.Client, chain.Signer, metadata.McmsPackageID, metadata.RegistryObj, metadata.AccountObj)
1387+
entrypointEncoder := suibindings.NewCCIPEntrypointArgEncoder(metadata.AccountObj)
1388+
executor, err = sui.NewTimelockExecutor(chain.Client, chain.Signer, entrypointEncoder, metadata.McmsPackageID, metadata.RegistryObj, metadata.AccountObj)
13851389
if err != nil {
13861390
return nil, fmt.Errorf("error creating sui timelock executor: %w", err)
13871391
}

experimental/analyzer/analyze.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ func describeBatchOperations(ctx ProposalContext, batches []types.BatchOperation
7272
for callIdx, decodedCall := range describedTxs {
7373
describedBatches[batchIdx][callIdx] = decodedCall.Describe(ctx.ArgumentContext(chainSel))
7474
}
75+
case chainsel.FamilySui:
76+
describedTxs, err := AnalyzeSuiTransactions(ctx, chainSel, batch.Transactions)
77+
if err != nil {
78+
return nil, err
79+
}
80+
for callIdx, decodedCall := range describedTxs {
81+
describedBatches[batchIdx][callIdx] = decodedCall.Describe(ctx.ArgumentContext(chainSel))
82+
}
7583
default:
7684
for callIdx := range batch.Transactions {
7785
describedBatches[batchIdx][callIdx] = family + " transaction decoding is not supported"
@@ -112,6 +120,12 @@ func describeOperations(ctx ProposalContext, operations []types.Operation) ([]st
112120
return nil, err
113121
}
114122
describedOperations[callIdx] = describedTransaction[0].Describe(ctx.ArgumentContext(uint64(operation.ChainSelector)))
123+
case chainsel.FamilySui:
124+
describedTransaction, err := AnalyzeSuiTransactions(ctx, uint64(operation.ChainSelector), []types.Transaction{operation.Transaction})
125+
if err != nil {
126+
return nil, err
127+
}
128+
describedOperations[callIdx] = describedTransaction[0].Describe(ctx.ArgumentContext(uint64(operation.ChainSelector)))
115129

116130
default:
117131
describedOperations[callIdx] = family + " transaction decoding is not supported"

experimental/analyzer/aptos_analyzer.go

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ package analyzer
33
import (
44
"encoding/json"
55
"fmt"
6-
"reflect"
76

8-
"github.com/aptos-labs/aptos-go-sdk"
97
"github.com/smartcontractkit/chainlink-aptos/bindings"
10-
mcmssdk "github.com/smartcontractkit/mcms/sdk"
118
mcmsaptossdk "github.com/smartcontractkit/mcms/sdk/aptos"
129
"github.com/smartcontractkit/mcms/types"
1310

@@ -56,51 +53,3 @@ func AnalyzeAptosTransaction(ctx ProposalContext, decoder *mcmsaptossdk.Decoder,
5653
Outputs: []proposalutils.NamedArgument{},
5754
}, nil
5855
}
59-
60-
func toNamedArguments(decodedOp mcmssdk.DecodedOperation) ([]proposalutils.NamedArgument, error) {
61-
args := decodedOp.Args()
62-
keys := decodedOp.Keys()
63-
if len(keys) != len(args) {
64-
return nil, fmt.Errorf("mismatched keys and arguments length: %d keys, %d arguments", len(keys), len(args))
65-
}
66-
namedArgs := make([]proposalutils.NamedArgument, len(args))
67-
for i := range args {
68-
namedArgs[i] = proposalutils.NamedArgument{
69-
Name: keys[i],
70-
Value: getArgument(args[i]),
71-
}
72-
}
73-
74-
return namedArgs, nil
75-
}
76-
77-
func getArgument(argument any) proposalutils.Argument {
78-
var value proposalutils.Argument
79-
80-
switch arg := argument.(type) {
81-
// Pretty-print byte arrays and addresses
82-
case []byte:
83-
value = proposalutils.BytesArgument{Value: arg}
84-
case aptos.AccountAddress:
85-
value = proposalutils.AddressArgument{Value: arg.StringLong()}
86-
case *aptos.AccountAddress:
87-
value = proposalutils.AddressArgument{Value: arg.StringLong()}
88-
default:
89-
//nolint:exhaustive // default case covers everything else
90-
switch reflect.TypeOf(arg).Kind() {
91-
// If the argument is a slice or array, iterate over every element individually
92-
case reflect.Array, reflect.Slice:
93-
array := proposalutils.ArrayArgument{}
94-
v := reflect.ValueOf(arg)
95-
for i := range v.Len() {
96-
array.Elements = append(array.Elements, getArgument(v.Index(i).Interface()))
97-
}
98-
value = array
99-
default:
100-
// Simply print the argument as-is
101-
value = proposalutils.SimpleArgument{Value: fmt.Sprintf("%v", arg)}
102-
}
103-
}
104-
105-
return value
106-
}

experimental/analyzer/aptos_analyzer_test.go

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,12 @@ import (
1212
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
1313
)
1414

15-
const testAddress = "0xe86f0e5a8b9cb6ab31b656baa83a0d2eb761b32eb31b9a9c74abb7d0cffd26fa"
16-
17-
// Helper function to create expected output strings in a readable format
18-
func expectedOutput(method string, inputs [][]string) string {
19-
result := fmt.Sprintf("**Address:** `%s` <sub><i>address of TestCCIP 1.0.0 from aptos-testnet</i></sub>\n**Method:** `%s`\n\n", testAddress, method)
20-
21-
if len(inputs) > 0 {
22-
result += "**Inputs:**\n\n| Name | Value | Annotation |\n|------|-------|------------|\n"
23-
for _, input := range inputs {
24-
result += fmt.Sprintf("| `%s` | `%s` | |\n", input[0], input[1])
25-
}
26-
}
27-
result += "\n"
28-
29-
return result
30-
}
15+
const aptosTestAddress = "0xe86f0e5a8b9cb6ab31b656baa83a0d2eb761b32eb31b9a9c74abb7d0cffd26fa"
16+
const aptosAddressTitle = "address of TestCCIP 1.0.0 from aptos-testnet"
3117

3218
// Helper function for error cases where method contains an error message
3319
func expectedErrorOutput(errorMessage string) string {
34-
return fmt.Sprintf("**Address:** `%s` <sub><i>address of TestCCIP 1.0.0 from aptos-testnet</i></sub>\n**Method:** `%s`\n\n", testAddress, errorMessage)
20+
return fmt.Sprintf("**Address:** `%s` <sub><i>address of TestCCIP 1.0.0 from aptos-testnet</i></sub>\n**Method:** `%s`\n\n", aptosTestAddress, errorMessage)
3521
}
3622

3723
func TestDescribeBatchOperations(t *testing.T) {
@@ -40,7 +26,7 @@ func TestDescribeBatchOperations(t *testing.T) {
4026
defaultProposalCtx := &DefaultProposalContext{
4127
AddressesByChain: deployment.AddressesByChain{
4228
chainsel.APTOS_TESTNET.Selector: {
43-
testAddress: deployment.MustTypeAndVersionFromString("TestCCIP 1.0.0"),
29+
aptosTestAddress: deployment.MustTypeAndVersionFromString("TestCCIP 1.0.0"),
4430
},
4531
},
4632
}
@@ -56,30 +42,30 @@ func TestDescribeBatchOperations(t *testing.T) {
5642
operations: getOperations(1),
5743
want: [][]string{
5844
{
59-
expectedOutput("ccip_onramp::onramp::initialize", [][]string{
45+
expectedOutput("ccip_onramp::onramp::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
6046
{"chain_selector", "4457093679053095497"},
6147
{"fee_aggregator", "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f"},
6248
{"allowlist_admin", "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f"},
6349
{"dest_chain_selectors", "[]"},
6450
{"dest_chain_routers", "[]"},
6551
{"dest_chain_allowlist_enabled", "[]"},
6652
}),
67-
expectedOutput("ccip_offramp::offramp::initialize", [][]string{
53+
expectedOutput("ccip_offramp::offramp::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
6854
{"chain_selector", "4457093679053095497"},
6955
{"permissionless_execution_threshold_seconds", "28800"},
7056
{"source_chains_selector", "[11155111]"},
7157
{"source_chains_is_enabled", "[true]"},
7258
{"source_chains_is_rmn_verification_disabled", "[false]"},
7359
{"source_chains_on_ramp", "[0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59]"},
7460
}),
75-
expectedOutput("ccip::rmn_remote::initialize", [][]string{
61+
expectedOutput("ccip::rmn_remote::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
7662
{"local_chain_selector", "4457093679053095497"},
7763
}),
78-
expectedOutput("ccip_token_pool::token_pool::initialize", [][]string{
64+
expectedOutput("ccip_token_pool::token_pool::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
7965
{"local_token", "0x0000000000000000000000000000000000000000000000000000000000000003"},
8066
{"allowlist", "[0x0000000000000000000000000000000000000000000000000000000000000001,0x0000000000000000000000000000000000000000000000000000000000000002]"},
8167
}),
82-
expectedOutput("ccip_offramp::offramp::apply_source_chain_config_updates", [][]string{
68+
expectedOutput("ccip_offramp::offramp::apply_source_chain_config_updates", aptosTestAddress, aptosAddressTitle, [][]string{
8369
{"source_chains_selector", "[743186221051783445,16015286601757825753]"},
8470
{"source_chains_is_enabled", "[true,false]"},
8571
{"source_chains_is_rmn_verification_disabled", "[true,true]"},
@@ -94,15 +80,15 @@ func TestDescribeBatchOperations(t *testing.T) {
9480
operations: getOperations(2),
9581
want: [][]string{
9682
{
97-
expectedOutput("ccip_onramp::onramp::initialize", [][]string{
83+
expectedOutput("ccip_onramp::onramp::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
9884
{"chain_selector", "4457093679053095497"},
9985
{"fee_aggregator", "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f"},
10086
{"allowlist_admin", "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f"},
10187
{"dest_chain_selectors", "[]"},
10288
{"dest_chain_routers", "[]"},
10389
{"dest_chain_allowlist_enabled", "[]"},
10490
}),
105-
expectedOutput("ccip_offramp::offramp::initialize", [][]string{
91+
expectedOutput("ccip_offramp::offramp::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
10692
{"chain_selector", "4457093679053095497"},
10793
{"permissionless_execution_threshold_seconds", "28800"},
10894
{"source_chains_selector", "[11155111]"},
@@ -112,14 +98,14 @@ func TestDescribeBatchOperations(t *testing.T) {
11298
}),
11399
},
114100
{
115-
expectedOutput("ccip::rmn_remote::initialize", [][]string{
101+
expectedOutput("ccip::rmn_remote::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
116102
{"local_chain_selector", "4457093679053095497"},
117103
}),
118-
expectedOutput("ccip_token_pool::token_pool::initialize", [][]string{
104+
expectedOutput("ccip_token_pool::token_pool::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
119105
{"local_token", "0x0000000000000000000000000000000000000000000000000000000000000003"},
120106
{"allowlist", "[0x0000000000000000000000000000000000000000000000000000000000000001,0x0000000000000000000000000000000000000000000000000000000000000002]"},
121107
}),
122-
expectedOutput("ccip_offramp::offramp::apply_source_chain_config_updates", [][]string{
108+
expectedOutput("ccip_offramp::offramp::apply_source_chain_config_updates", aptosTestAddress, aptosAddressTitle, [][]string{
123109
{"source_chains_selector", "[743186221051783445,16015286601757825753]"},
124110
{"source_chains_is_enabled", "[true,false]"},
125111
{"source_chains_is_rmn_verification_disabled", "[true,true]"},
@@ -136,7 +122,7 @@ func TestDescribeBatchOperations(t *testing.T) {
136122
{
137123
expectedErrorOutput(
138124
"failed to decode Aptos transaction: could not find function info for ccip_offramp::bad_module::initialize"),
139-
expectedOutput("ccip::rmn_remote::initialize", [][]string{
125+
expectedOutput("ccip::rmn_remote::initialize", aptosTestAddress, aptosAddressTitle, [][]string{
140126
{"local_chain_selector", "4457093679053095497"},
141127
}),
142128
},
@@ -164,7 +150,7 @@ func getOperations(n int) []types.BatchOperation {
164150
mcmsTxs := []types.Transaction{
165151
{
166152
OperationMetadata: types.OperationMetadata{},
167-
To: testAddress,
153+
To: aptosTestAddress,
168154
Data: []byte{
169155
0x49, 0x42, 0x99, 0x1e, 0x16, 0xc7, 0xda, 0x3d, 0x13, 0xa9, 0xf1, 0xa1, 0x09, 0x36, 0x87, 0x30,
170156
0xf2, 0xe3, 0x55, 0xd8, 0x31, 0xba, 0x8f, 0xbf, 0x59, 0x42, 0xfb, 0x82, 0x32, 0x18, 0x63, 0xd5,
@@ -176,7 +162,7 @@ func getOperations(n int) []types.BatchOperation {
176162
},
177163
{
178164
OperationMetadata: types.OperationMetadata{},
179-
To: testAddress,
165+
To: aptosTestAddress,
180166
Data: []byte{
181167
0x49, 0x42, 0x99, 0x1e, 0x16, 0xc7, 0xda, 0x3d, 0x80, 0x70, 0x00, 0x00, 0x01, 0xa7, 0x36, 0xaa,
182168
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x14, 0x0b, 0xf3, 0xde, 0x8c, 0x5d,
@@ -186,13 +172,13 @@ func getOperations(n int) []types.BatchOperation {
186172
},
187173
{
188174
OperationMetadata: types.OperationMetadata{},
189-
To: testAddress,
175+
To: aptosTestAddress,
190176
Data: []byte{0x49, 0x42, 0x99, 0x1e, 0x16, 0xc7, 0xda, 0x3d},
191177
AdditionalFields: json.RawMessage(`{"package_name":"ccip","module_name":"rmn_remote","function":"initialize"}`),
192178
},
193179
{
194180
OperationMetadata: types.OperationMetadata{},
195-
To: testAddress,
181+
To: aptosTestAddress,
196182
Data: []byte{
197183
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
198184
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
@@ -206,7 +192,7 @@ func getOperations(n int) []types.BatchOperation {
206192
},
207193
{
208194
OperationMetadata: types.OperationMetadata{},
209-
To: testAddress,
195+
To: aptosTestAddress,
210196
Data: []byte{
211197
0x02, 0x15, 0xa9, 0xc1, 0x33, 0xee, 0x53, 0x50, 0x0a, 0xd9, 0x1a, 0xd9, 0xc9, 0x4f, 0xba, 0x41,
212198
0xde, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x14, 0xc2, 0x30, 0x71, 0xa8, 0xae, 0x83, 0x67,
@@ -244,7 +230,7 @@ func getBadOperations() []types.BatchOperation {
244230
mcmsTxs := []types.Transaction{
245231
{
246232
OperationMetadata: types.OperationMetadata{},
247-
To: testAddress,
233+
To: aptosTestAddress,
248234
Data: []byte{
249235
0x49, 0x42, 0x99, 0x1e, 0x16, 0xc7, 0xda, 0x3d, 0x80, 0x70, 0x00, 0x00, 0x01, 0xa7, 0x36, 0xaa,
250236
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x14, 0x0b, 0xf3, 0xde, 0x8c, 0x5d,
@@ -254,7 +240,7 @@ func getBadOperations() []types.BatchOperation {
254240
},
255241
{
256242
OperationMetadata: types.OperationMetadata{},
257-
To: testAddress,
243+
To: aptosTestAddress,
258244
Data: []byte{0x49, 0x42, 0x99, 0x1e, 0x16, 0xc7, 0xda, 0x3d},
259245
AdditionalFields: json.RawMessage(`{"package_name":"ccip","module_name":"rmn_remote","function":"initialize"}`),
260246
},
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/smartcontractkit/chainlink-sui/bindings/generated"
8+
mcmssuisdk "github.com/smartcontractkit/mcms/sdk/sui"
9+
"github.com/smartcontractkit/mcms/types"
10+
11+
"github.com/smartcontractkit/chainlink-deployments-framework/experimental/proposalutils"
12+
)
13+
14+
func AnalyzeSuiTransactions(ctx ProposalContext, chainSelector uint64, txs []types.Transaction) ([]*proposalutils.DecodedCall, error) {
15+
decoder := mcmssuisdk.NewDecoder()
16+
decodedTxs := make([]*proposalutils.DecodedCall, len(txs))
17+
for i, op := range txs {
18+
analyzedTransaction, err := AnalyzeSuiTransaction(ctx, decoder, chainSelector, op)
19+
if err != nil {
20+
return nil, fmt.Errorf("failed to analyze Sui transaction %d: %w", i, err)
21+
}
22+
decodedTxs[i] = analyzedTransaction
23+
}
24+
25+
return decodedTxs, nil
26+
}
27+
28+
func AnalyzeSuiTransaction(ctx ProposalContext, decoder *mcmssuisdk.Decoder, chainSelector uint64, mcmsTx types.Transaction) (*proposalutils.DecodedCall, error) {
29+
var additionalFields mcmssuisdk.AdditionalFields
30+
if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil {
31+
return nil, fmt.Errorf("failed to unmarshal Sui additional fields: %w", err)
32+
}
33+
34+
functionInfo, ok := generated.FunctionInfoByModule[additionalFields.ModuleName]
35+
if !ok {
36+
// Don't return an error to not block the whole proposal decoding because of a single missing method
37+
errStr := fmt.Errorf("no function info found for module %s on chain selector %d", additionalFields.ModuleName, chainSelector)
38+
39+
return &proposalutils.DecodedCall{
40+
Address: mcmsTx.To,
41+
Method: errStr.Error(),
42+
}, nil
43+
}
44+
45+
decodedOp, err := decoder.Decode(mcmsTx, functionInfo)
46+
if err != nil {
47+
// Don't return an error to not block the whole proposal decoding because of a single transaction decode failure
48+
errStr := fmt.Errorf("failed to decode Sui transaction: %w", err)
49+
50+
return &proposalutils.DecodedCall{
51+
Address: mcmsTx.To,
52+
Method: errStr.Error(),
53+
}, nil
54+
}
55+
namedArgs, err := toNamedArguments(decodedOp)
56+
if err != nil {
57+
return nil, fmt.Errorf("failed to convert decoded operation to named arguments: %w", err)
58+
}
59+
60+
return &proposalutils.DecodedCall{
61+
Address: mcmsTx.To,
62+
Method: decodedOp.MethodName(),
63+
Inputs: namedArgs,
64+
Outputs: []proposalutils.NamedArgument{},
65+
}, nil
66+
}

0 commit comments

Comments
 (0)