Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/big-dodos-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

Implement SUI proposal analyzer
8 changes: 6 additions & 2 deletions engine/cld/legacy/cli/mcmsv2/mcms_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"

suibindings "github.com/smartcontractkit/chainlink-sui/bindings"

"github.com/smartcontractkit/chainlink-deployments-framework/chain"
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
Expand Down Expand Up @@ -1335,8 +1337,9 @@ func getExecutorWithChainOverride(cfg *cfgv2, chainSelector types.ChainSelector)
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
}
chain := cfg.blockchains.SuiChains()[uint64(chainSelector)]
entrypointEncoder := suibindings.NewCCIPEntrypointArgEncoder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean the encoder won't work with contracts from other products? (if/when they are created)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means that new products will need their own product encoders, or extend the existing one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we should figure out a way to allow product teams to specify their encoders from within their domain and pass it to the mcms CLI. See, for example, how the encoder for EVM is implemented (though I think that implementation can be simplified).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I'll look into the EVM encoder.

For this case I thought on allowing multiple Encoders to be passed to the MCMS lib, and probably the proposal would indicate the product is using, and select the Encoder based on it. I'll note to dive and add this into MCMS

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed the NewCCIPEntrypointArgEncoder doesn't receive the registryObjID.

Please use &CCIPEntrypointArgEncoder{registryObjID: <value>} to construct it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where can I get the value of registryObjID?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metadata.RegistryObj

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RegistryObjID is unexported field. We'll have to fix this before this PR can get merged.


return sui.NewExecutor(chain.Client, chain.Signer, encoder, metadata.McmsPackageID, metadata.Role, cfg.timelockProposal.ChainMetadata[chainSelector].MCMAddress, metadata.AccountObj, metadata.RegistryObj, metadata.TimelockObj)
return sui.NewExecutor(chain.Client, chain.Signer, encoder, entrypointEncoder, metadata.McmsPackageID, metadata.Role, cfg.timelockProposal.ChainMetadata[chainSelector].MCMAddress, metadata.AccountObj, metadata.RegistryObj, metadata.TimelockObj)
default:
return nil, fmt.Errorf("unsupported chain family %s", family)
}
Expand Down Expand Up @@ -1381,7 +1384,8 @@ func getTimelockExecutorWithChainOverride(cfg *cfgv2, chainSelector types.ChainS
if err != nil {
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
}
executor, err = sui.NewTimelockExecutor(chain.Client, chain.Signer, metadata.McmsPackageID, metadata.RegistryObj, metadata.AccountObj)
entrypointEncoder := suibindings.NewCCIPEntrypointArgEncoder()
executor, err = sui.NewTimelockExecutor(chain.Client, chain.Signer, entrypointEncoder, metadata.McmsPackageID, metadata.RegistryObj, metadata.AccountObj)
if err != nil {
return nil, fmt.Errorf("error creating sui timelock executor: %w", err)
}
Expand Down
14 changes: 14 additions & 0 deletions experimental/analyzer/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ func describeBatchOperations(ctx ProposalContext, batches []types.BatchOperation
for callIdx, decodedCall := range describedTxs {
describedBatches[batchIdx][callIdx] = decodedCall.Describe(ctx.ArgumentContext(chainSel))
}
case chainsel.FamilySui:
describedTxs, err := AnalyzeSuiTransactions(ctx, chainSel, batch.Transactions)
if err != nil {
return nil, err
}
for callIdx, decodedCall := range describedTxs {
describedBatches[batchIdx][callIdx] = decodedCall.Describe(ctx.ArgumentContext(chainSel))
}
default:
for callIdx := range batch.Transactions {
describedBatches[batchIdx][callIdx] = family + " transaction decoding is not supported"
Expand Down Expand Up @@ -112,6 +120,12 @@ func describeOperations(ctx ProposalContext, operations []types.Operation) ([]st
return nil, err
}
describedOperations[callIdx] = describedTransaction[0].Describe(ctx.ArgumentContext(uint64(operation.ChainSelector)))
case chainsel.FamilySui:
describedTransaction, err := AnalyzeSuiTransactions(ctx, uint64(operation.ChainSelector), []types.Transaction{operation.Transaction})
if err != nil {
return nil, err
}
describedOperations[callIdx] = describedTransaction[0].Describe(ctx.ArgumentContext(uint64(operation.ChainSelector)))

default:
describedOperations[callIdx] = family + " transaction decoding is not supported"
Expand Down
51 changes: 0 additions & 51 deletions experimental/analyzer/aptos_analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ package analyzer
import (
"encoding/json"
"fmt"
"reflect"

"github.com/aptos-labs/aptos-go-sdk"
"github.com/smartcontractkit/chainlink-aptos/bindings"
mcmssdk "github.com/smartcontractkit/mcms/sdk"
mcmsaptossdk "github.com/smartcontractkit/mcms/sdk/aptos"
"github.com/smartcontractkit/mcms/types"

Expand Down Expand Up @@ -56,51 +53,3 @@ func AnalyzeAptosTransaction(ctx ProposalContext, decoder *mcmsaptossdk.Decoder,
Outputs: []proposalutils.NamedArgument{},
}, nil
}

func toNamedArguments(decodedOp mcmssdk.DecodedOperation) ([]proposalutils.NamedArgument, error) {
args := decodedOp.Args()
keys := decodedOp.Keys()
if len(keys) != len(args) {
return nil, fmt.Errorf("mismatched keys and arguments length: %d keys, %d arguments", len(keys), len(args))
}
namedArgs := make([]proposalutils.NamedArgument, len(args))
for i := range args {
namedArgs[i] = proposalutils.NamedArgument{
Name: keys[i],
Value: getArgument(args[i]),
}
}

return namedArgs, nil
}

func getArgument(argument any) proposalutils.Argument {
var value proposalutils.Argument

switch arg := argument.(type) {
// Pretty-print byte arrays and addresses
case []byte:
value = proposalutils.BytesArgument{Value: arg}
case aptos.AccountAddress:
value = proposalutils.AddressArgument{Value: arg.StringLong()}
case *aptos.AccountAddress:
value = proposalutils.AddressArgument{Value: arg.StringLong()}
default:
//nolint:exhaustive // default case covers everything else
switch reflect.TypeOf(arg).Kind() {
// If the argument is a slice or array, iterate over every element individually
case reflect.Array, reflect.Slice:
array := proposalutils.ArrayArgument{}
v := reflect.ValueOf(arg)
for i := range v.Len() {
array.Elements = append(array.Elements, getArgument(v.Index(i).Interface()))
}
value = array
default:
// Simply print the argument as-is
value = proposalutils.SimpleArgument{Value: fmt.Sprintf("%v", arg)}
}
}

return value
}
38 changes: 12 additions & 26 deletions experimental/analyzer/aptos_analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,7 @@ import (
)

const testAddress = "0xe86f0e5a8b9cb6ab31b656baa83a0d2eb761b32eb31b9a9c74abb7d0cffd26fa"

// Helper function to create expected output strings in a readable format
func expectedOutput(method string, inputs [][]string) string {
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)

if len(inputs) > 0 {
result += "**Inputs:**\n\n| Name | Value | Annotation |\n|------|-------|------------|\n"
for _, input := range inputs {
result += fmt.Sprintf("| `%s` | `%s` | |\n", input[0], input[1])
}
}
result += "\n"

return result
}
const addressTitle = "address of TestCCIP 1.0.0 from aptos-testnet"

// Helper function for error cases where method contains an error message
func expectedErrorOutput(errorMessage string) string {
Expand Down Expand Up @@ -56,30 +42,30 @@ func TestDescribeBatchOperations(t *testing.T) {
operations: getOperations(1),
want: [][]string{
{
expectedOutput("ccip_onramp::onramp::initialize", [][]string{
expectedOutput("ccip_onramp::onramp::initialize", testAddress, addressTitle, [][]string{
{"chain_selector", "4457093679053095497"},
{"fee_aggregator", "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f"},
{"allowlist_admin", "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f"},
{"dest_chain_selectors", "[]"},
{"dest_chain_routers", "[]"},
{"dest_chain_allowlist_enabled", "[]"},
}),
expectedOutput("ccip_offramp::offramp::initialize", [][]string{
expectedOutput("ccip_offramp::offramp::initialize", testAddress, addressTitle, [][]string{
{"chain_selector", "4457093679053095497"},
{"permissionless_execution_threshold_seconds", "28800"},
{"source_chains_selector", "[11155111]"},
{"source_chains_is_enabled", "[true]"},
{"source_chains_is_rmn_verification_disabled", "[false]"},
{"source_chains_on_ramp", "[0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59]"},
}),
expectedOutput("ccip::rmn_remote::initialize", [][]string{
expectedOutput("ccip::rmn_remote::initialize", testAddress, addressTitle, [][]string{
{"local_chain_selector", "4457093679053095497"},
}),
expectedOutput("ccip_token_pool::token_pool::initialize", [][]string{
expectedOutput("ccip_token_pool::token_pool::initialize", testAddress, addressTitle, [][]string{
{"local_token", "0x0000000000000000000000000000000000000000000000000000000000000003"},
{"allowlist", "[0x0000000000000000000000000000000000000000000000000000000000000001,0x0000000000000000000000000000000000000000000000000000000000000002]"},
}),
expectedOutput("ccip_offramp::offramp::apply_source_chain_config_updates", [][]string{
expectedOutput("ccip_offramp::offramp::apply_source_chain_config_updates", testAddress, addressTitle, [][]string{
{"source_chains_selector", "[743186221051783445,16015286601757825753]"},
{"source_chains_is_enabled", "[true,false]"},
{"source_chains_is_rmn_verification_disabled", "[true,true]"},
Expand All @@ -94,15 +80,15 @@ func TestDescribeBatchOperations(t *testing.T) {
operations: getOperations(2),
want: [][]string{
{
expectedOutput("ccip_onramp::onramp::initialize", [][]string{
expectedOutput("ccip_onramp::onramp::initialize", testAddress, addressTitle, [][]string{
{"chain_selector", "4457093679053095497"},
{"fee_aggregator", "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f"},
{"allowlist_admin", "0x13a9f1a109368730f2e355d831ba8fbf5942fb82321863d55de54cb4ebe5d18f"},
{"dest_chain_selectors", "[]"},
{"dest_chain_routers", "[]"},
{"dest_chain_allowlist_enabled", "[]"},
}),
expectedOutput("ccip_offramp::offramp::initialize", [][]string{
expectedOutput("ccip_offramp::offramp::initialize", testAddress, addressTitle, [][]string{
{"chain_selector", "4457093679053095497"},
{"permissionless_execution_threshold_seconds", "28800"},
{"source_chains_selector", "[11155111]"},
Expand All @@ -112,14 +98,14 @@ func TestDescribeBatchOperations(t *testing.T) {
}),
},
{
expectedOutput("ccip::rmn_remote::initialize", [][]string{
expectedOutput("ccip::rmn_remote::initialize", testAddress, addressTitle, [][]string{
{"local_chain_selector", "4457093679053095497"},
}),
expectedOutput("ccip_token_pool::token_pool::initialize", [][]string{
expectedOutput("ccip_token_pool::token_pool::initialize", testAddress, addressTitle, [][]string{
{"local_token", "0x0000000000000000000000000000000000000000000000000000000000000003"},
{"allowlist", "[0x0000000000000000000000000000000000000000000000000000000000000001,0x0000000000000000000000000000000000000000000000000000000000000002]"},
}),
expectedOutput("ccip_offramp::offramp::apply_source_chain_config_updates", [][]string{
expectedOutput("ccip_offramp::offramp::apply_source_chain_config_updates", testAddress, addressTitle, [][]string{
{"source_chains_selector", "[743186221051783445,16015286601757825753]"},
{"source_chains_is_enabled", "[true,false]"},
{"source_chains_is_rmn_verification_disabled", "[true,true]"},
Expand All @@ -136,7 +122,7 @@ func TestDescribeBatchOperations(t *testing.T) {
{
expectedErrorOutput(
"failed to decode Aptos transaction: could not find function info for ccip_offramp::bad_module::initialize"),
expectedOutput("ccip::rmn_remote::initialize", [][]string{
expectedOutput("ccip::rmn_remote::initialize", testAddress, addressTitle, [][]string{
{"local_chain_selector", "4457093679053095497"},
}),
},
Expand Down
55 changes: 55 additions & 0 deletions experimental/analyzer/sui_analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package analyzer

import (
"encoding/json"
"fmt"

"github.com/smartcontractkit/chainlink-deployments-framework/experimental/proposalutils"
"github.com/smartcontractkit/chainlink-sui/bindings/generated"
mcmssuisdk "github.com/smartcontractkit/mcms/sdk/sui"
"github.com/smartcontractkit/mcms/types"
)

func AnalyzeSuiTransactions(ctx ProposalContext, chainSelector uint64, txs []types.Transaction) ([]*proposalutils.DecodedCall, error) {
decoder := mcmssuisdk.NewDecoder()
decodedTxs := make([]*proposalutils.DecodedCall, len(txs))
for i, op := range txs {
analyzedTransaction, err := AnalyzeSuiTransaction(ctx, decoder, chainSelector, op)
if err != nil {
return nil, fmt.Errorf("failed to analyze Sui transaction %d: %w", i, err)
}
decodedTxs[i] = analyzedTransaction
}

return decodedTxs, nil
}

func AnalyzeSuiTransaction(ctx ProposalContext, decoder *mcmssuisdk.Decoder, chainSelector uint64, mcmsTx types.Transaction) (*proposalutils.DecodedCall, error) {
var additionalFields mcmssuisdk.AdditionalFields
if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil {
return nil, fmt.Errorf("failed to unmarshal Sui additional fields: %w", err)
}

functionInfo := generated.FunctionInfoByModule[additionalFields.ModuleName]
decodedOp, err := decoder.Decode(mcmsTx, functionInfo)
if err != nil {
// Don't return an error to not block the whole proposal decoding because of a single missing method
errStr := fmt.Errorf("failed to decode Sui transaction: %w", err)

return &proposalutils.DecodedCall{
Address: mcmsTx.To,
Method: errStr.Error(),
}, nil
}
namedArgs, err := toNamedArguments(decodedOp)
if err != nil {
return nil, fmt.Errorf("failed to convert decoded operation to named arguments: %w", err)
}

return &proposalutils.DecodedCall{
Address: mcmsTx.To,
Method: decodedOp.MethodName(),
Inputs: namedArgs,
Outputs: []proposalutils.NamedArgument{},
}, nil
}
Loading
Loading