diff --git a/.changeset/bold-olives-smell.md b/.changeset/bold-olives-smell.md new file mode 100644 index 000000000..b093953a7 --- /dev/null +++ b/.changeset/bold-olives-smell.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": minor +--- + +Use Metadata.ContractVersion instead of ContractTypeAndVersion diff --git a/engine/cld/commands/mcms/testdata/proposal.json b/engine/cld/commands/mcms/testdata/proposal.json index a17fc60c7..0befd3c95 100644 --- a/engine/cld/commands/mcms/testdata/proposal.json +++ b/engine/cld/commands/mcms/testdata/proposal.json @@ -24,7 +24,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", "tags": null }, { @@ -32,7 +31,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", "tags": null }, { @@ -40,7 +38,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", "tags": null }, { @@ -48,7 +45,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", "tags": null }, { @@ -56,7 +52,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", "tags": null } ] diff --git a/engine/test/internal/mcmsutils/encoding_test.go b/engine/test/internal/mcmsutils/encoding_test.go index dbabe412e..de335f750 100644 --- a/engine/test/internal/mcmsutils/encoding_test.go +++ b/engine/test/internal/mcmsutils/encoding_test.go @@ -51,7 +51,7 @@ func TestDecodeProposal(t *testing.T) { assert.Equal(t, "v1", proposal.Version) assert.Equal(t, mcmstypes.ProposalKind("Proposal"), proposal.Kind) require.Contains(t, proposal.ChainMetadata, mcmstypes.ChainSelector(3379446385462418246)) - require.Len(t, proposal.Operations, 1) + require.Len(t, proposal.Operations, 2) }) t.Run("failed decoding", func(t *testing.T) { diff --git a/engine/test/internal/mcmsutils/testdata/proposal.json b/engine/test/internal/mcmsutils/testdata/proposal.json index 9cb8cc768..a4168c056 100644 --- a/engine/test/internal/mcmsutils/testdata/proposal.json +++ b/engine/test/internal/mcmsutils/testdata/proposal.json @@ -13,6 +13,18 @@ } }, "operations": [ + { + "chainSelector": 3379446385462418246, + "transaction": { + "to": "0x123", + "additionalFields": { + "value": 0 + }, + "data": "", + "tags": null, + "contractType": "" + } + }, { "chainSelector": 3379446385462418246, "transaction": { @@ -23,7 +35,7 @@ "data": "", "tags": null, "contractType": "", - "contractTypeAndVersion": "" + "contractVersion": "1.2.3" } } ] diff --git a/engine/test/internal/mcmsutils/testdata/timelock_proposal.json b/engine/test/internal/mcmsutils/testdata/timelock_proposal.json index c77c92a08..aa7f13c5c 100644 --- a/engine/test/internal/mcmsutils/testdata/timelock_proposal.json +++ b/engine/test/internal/mcmsutils/testdata/timelock_proposal.json @@ -23,13 +23,14 @@ "transactions": [ { "to": "0x0000000000000000000000000000000000000000", - "additionalFields": {"value": 0}, + "additionalFields": { + "value": 0 + }, "data": "ZGF0YQ==", "contractType": "", - "contractTypeAndVersion": "", "tags": null } ] } ] -} +} \ No newline at end of file diff --git a/experimental/analyzer/decoded_call.go b/experimental/analyzer/decoded_call.go index da9bc1cbb..821bca407 100644 --- a/experimental/analyzer/decoded_call.go +++ b/experimental/analyzer/decoded_call.go @@ -31,19 +31,24 @@ func (d *DecodedCall) String(context *FieldContext) string { // resolveContractInfo looks up the contract type and version from the proposal // context's registered addresses. func resolveContractInfo(ctx ProposalContext, chainSelector uint64, mcmsTx types.Transaction) (contractType, contractVersion string) { + // Use ContractVersion from transaction metadata (set by proposal creator) as fallback + if mcmsTx.ContractVersion != nil { + contractVersion = mcmsTx.ContractVersion.String() + } + dpc, ok := ctx.(*DefaultProposalContext) if !ok { - return mcmsTx.ContractType, "" + return mcmsTx.ContractType, contractVersion } addresses, ok := dpc.AddressesByChain[chainSelector] if !ok { - return mcmsTx.ContractType, "" + return mcmsTx.ContractType, contractVersion } tv, ok := addresses[mcmsTx.To] if !ok { - return mcmsTx.ContractType, "" + return mcmsTx.ContractType, contractVersion } ct := string(tv.Type) @@ -51,10 +56,10 @@ func resolveContractInfo(ctx ProposalContext, chainSelector uint64, mcmsTx types ct = mcmsTx.ContractType } - var cv string + // Override with the DataStore version if available if tv.Version.Original() != "" { - cv = tv.Version.String() + contractVersion = tv.Version.String() } - return ct, cv + return ct, contractVersion } diff --git a/experimental/analyzer/ton_analyzer.go b/experimental/analyzer/ton_analyzer.go index fd43e1303..32b71338d 100644 --- a/experimental/analyzer/ton_analyzer.go +++ b/experimental/analyzer/ton_analyzer.go @@ -1,14 +1,13 @@ package analyzer import ( + "encoding/json" "fmt" "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/ton" "github.com/smartcontractkit/mcms/types" - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - "github.com/smartcontractkit/chainlink-ton/pkg/bindings" ) @@ -29,37 +28,43 @@ func AnalyzeTONTransactions(ctx ProposalContext, chainSelector uint64, txs []typ // AnalyzeTONTransaction decodes a single TON transaction using the MCMS TON decoder. // -// Unlike Aptos/Sui analyzers, this function does not unmarshal AdditionalFields because -// the TON decoder only requires tx.Data (BOC cell) and tx.ContractType (metadata). -// AdditionalFields in TON is only used by the encoder/timelock_converter for the Value field. -// // On decode failure, this function returns a DecodedCall with the error in the Method field // instead of returning an error. This allows the proposal to continue processing even if // a single transaction fails to decode. func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelector uint64, mcmsTx types.Transaction) (*DecodedCall, error) { - contractTypeAndVersion, err := deployment.TypeAndVersionFromString(mcmsTx.ContractTypeAndVersion) - if err != nil { - contractType, contractVersion := resolveContractInfo(ctx, chainSelector, mcmsTx) - errStr := fmt.Errorf("failed to decode TON transaction: failed to parse contract type and version: %w", err) + contractType, contractVersion := resolveContractInfo(ctx, chainSelector, mcmsTx) - return &DecodedCall{ - Address: mcmsTx.To, - Method: errStr.Error(), - ContractType: contractType, - ContractVersion: contractVersion, - }, nil - } - decodedOp, err := decoder.Decode(mcmsTx, contractTypeAndVersion.Type.String()) - if err != nil { + var typeErr string + fullyQualifiedName := func() string { + var additionalFields ton.AdditionalFields + if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil { + typeErr = fmt.Sprintf("; additionally failed to unmarshal TON additional fields: %s", err) + return "" + } + + fullyQualifiedName := string(additionalFields.ContractTypeFull) + // If ContractVersion is provided, append it to the fully qualified name to ensure the decoder uses the correct version. + // If it is skipped, the decoder will use the latest version available for the contract type. + // Note: we don't use contractVersion from resolveContractInfo because that only represents the short type used by the datastore. + if mcmsTx.ContractVersion != nil { + fullyQualifiedName += "@" + mcmsTx.ContractVersion.String() + } + + return fullyQualifiedName + }() + + decodedOp, errDec := decoder.Decode(mcmsTx, fullyQualifiedName) + if errDec != nil { // Don't return an error to not block the whole proposal decoding because of a single transaction decode failure. // Instead, put the error message in the Method field so it's visible in the report. - errStr := fmt.Errorf("failed to decode TON transaction: %w", err) + errStr := "failed to decode TON transaction: " + errDec.Error() + typeErr + //nolint:nilerr // We are intentionally not returning an error here to allow the proposal to be processed even if decoding fails. return &DecodedCall{ Address: mcmsTx.To, - Method: errStr.Error(), - ContractType: contractTypeAndVersion.Type.String(), - ContractVersion: contractTypeAndVersion.Version.String(), + Method: errStr, + ContractType: contractType, + ContractVersion: contractVersion, }, nil } @@ -73,7 +78,7 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect Method: decodedOp.MethodName(), Inputs: namedArgs, Outputs: []NamedField{}, - ContractType: contractTypeAndVersion.Type.String(), - ContractVersion: contractTypeAndVersion.Version.String(), + ContractType: contractType, + ContractVersion: contractVersion, }, nil } diff --git a/experimental/analyzer/ton_analyzer_test.go b/experimental/analyzer/ton_analyzer_test.go index 3f9e4a91e..187835d5a 100644 --- a/experimental/analyzer/ton_analyzer_test.go +++ b/experimental/analyzer/ton_analyzer_test.go @@ -5,12 +5,15 @@ import ( "math/big" "testing" + "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/crypto" + "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/ton" "github.com/smartcontractkit/mcms/types" "github.com/stretchr/testify/require" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" "github.com/smartcontractkit/chainlink-ton/pkg/bindings" "github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac" @@ -31,6 +34,7 @@ func TestAnalyzeTONTransaction(t *testing.T) { tests := []struct { name string + decoder sdk.Decoder mcmsTx types.Transaction want *DecodedCall wantErrContain string @@ -49,32 +53,82 @@ func TestAnalyzeTONTransaction(t *testing.T) { { name: "unknown contract type", mcmsTx: types.Transaction{ - OperationMetadata: types.OperationMetadata{ContractType: "unknown.type", ContractTypeAndVersion: deployment.TypeAndVersion{Type: "com.example.package.unknown.contract"}.String()}, + OperationMetadata: types.OperationMetadata{ContractType: "unknown.type", ContractVersion: semver.MustParse("1.2.3")}, To: testAddress, - Data: []byte{0x01, 0x02}, - AdditionalFields: json.RawMessage(`{"value":0}`), + Data: func() []byte { + c := cell.BeginCell() + c.MustStoreBinarySnake([]byte{0x01, 0x02}) + + return c.EndCell().ToBOC() + }(), + AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"com.example.package.unknown.contract"}`), }, want: &DecodedCall{Address: testAddress}, wantErrContain: "unknown contract interface: com.example.package.unknown.contract", }, + { + name: "fail - unknown version", + mcmsTx: func() types.Transaction { + tx := setup.makeGrantRoleTx(t, 1) + tx.ContractVersion = semver.MustParse("0.0.0") + + return tx + }(), + want: &DecodedCall{Address: setup.targetAddr.String()}, + wantErrContain: "unknown contract interface: link.chain.ton.lib.access.RBAC@0.0.0", + }, + { + name: "success - GrantRole with version", + decoder: ton.NewDecoder(tvm.ContractTLBRegistry{ + bindings.TypeRBAC + "@1.2.3": bindings.Registry[bindings.TypeRBAC], + }), + mcmsTx: func() types.Transaction { + tx := setup.makeGrantRoleTx(t, 1) + tx.ContractVersion = semver.MustParse("1.2.3") + + return tx + }(), + want: setup.expectedGrantRoleCall(1, "1.2.3"), + }, { name: "empty data", mcmsTx: types.Transaction{ - OperationMetadata: types.OperationMetadata{ContractType: bindings.ShortRBAC, ContractTypeAndVersion: deployment.TypeAndVersion{Type: deployment.ContractType(bindings.TypeMCMS)}.String()}, + OperationMetadata: types.OperationMetadata{ContractType: bindings.ShortMCMS}, To: testAddress, Data: []byte{}, - AdditionalFields: json.RawMessage(`{"value":0}`), + AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"` + bindings.TypeMCMS + `"}`), }, want: &DecodedCall{Address: testAddress}, wantErrContain: "invalid cell BOC data", }, + { + name: "empty cell", + mcmsTx: types.Transaction{ + OperationMetadata: types.OperationMetadata{ContractType: bindings.ShortMCMS}, + To: testAddress, + Data: tvm.EmptyCell.ToBOC(), + AdditionalFields: json.RawMessage(`{"value":0}`), + }, + want: &DecodedCall{ + Address: testAddress, + Method: "::(0x0)", + Inputs: []NamedField{}, + Outputs: []NamedField{}, + ContractType: bindings.ShortMCMS, + ContractVersion: "", + }, + wantErrContain: "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - - result, err := AnalyzeTONTransaction(ctx, decoder, 0, tt.mcmsTx) + testDecoder := decoder + if tt.decoder != nil { + testDecoder = tt.decoder + } + result, err := AnalyzeTONTransaction(ctx, testDecoder, 0, tt.mcmsTx) require.NoError(t, err) require.NotNil(t, result) require.Equal(t, tt.want.Address, result.Address) @@ -173,7 +227,7 @@ func TestAnalyzeTONTransactions(t *testing.T) { continue } - require.Equal(t, tt.want[i], result, "call %d", i) + require.Equal(t, tt.want[i], result) } }) } @@ -187,7 +241,7 @@ func TestAnalyzeTONTransactionResolveContractInfo(t *testing.T) { chainSelector := uint64(123456) tx := setup.makeGrantRoleTx(t, 1) - t.Run("ignores contract type and version from proposal context", func(t *testing.T) { + t.Run("resolves contract type and version from proposal context", func(t *testing.T) { t.Parallel() ctx := &DefaultProposalContext{ @@ -200,8 +254,25 @@ func TestAnalyzeTONTransactionResolveContractInfo(t *testing.T) { result, err := AnalyzeTONTransaction(ctx, decoder, chainSelector, tx) require.NoError(t, err) - require.EqualValues(t, bindings.TypeRBAC, result.ContractType) - require.Equal(t, "0.0.0", result.ContractVersion) + require.Equal(t, "TONRBAC", result.ContractType) + require.Equal(t, "1.2.3", result.ContractVersion) + }) + + t.Run("falls back when address is not in proposal context", func(t *testing.T) { + t.Parallel() + + ctx := &DefaultProposalContext{ + AddressesByChain: deployment.AddressesByChain{ + chainSelector: { + "EQCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": deployment.MustTypeAndVersionFromString("OtherContract 9.9.9"), + }, + }, + } + + result, err := AnalyzeTONTransaction(ctx, decoder, chainSelector, tx) + require.NoError(t, err) + require.Equal(t, tx.ContractType, result.ContractType) + require.Empty(t, result.ContractVersion) }) } @@ -232,7 +303,8 @@ func (s *testTONSetup) makeGrantRoleTx(t *testing.T, queryID uint64) types.Trans grantRoleData.ToBuilder().ToSlice(), big.NewInt(0), bindings.ShortRBAC, - deployment.TypeAndVersion{Type: deployment.ContractType(bindings.TypeRBAC)}.String(), + nil, + bindings.TypeRBAC, []string{"grantRole"}, ) require.NoError(t, err) @@ -240,12 +312,19 @@ func (s *testTONSetup) makeGrantRoleTx(t *testing.T, queryID uint64) types.Trans return tx } -func (s *testTONSetup) expectedGrantRoleCall(queryID uint64) *DecodedCall { +func (s *testTONSetup) expectedGrantRoleCall(queryID uint64, version ...string) *DecodedCall { + methodPrefix := string(bindings.TypeRBAC) + versionStr := "" + if len(version) > 0 { + versionStr = version[0] + methodPrefix = methodPrefix + "@" + versionStr + } + return &DecodedCall{ Address: s.targetAddr.String(), - Method: string(bindings.TypeRBAC) + "::GrantRole(0x95cd540f)", - ContractType: string(bindings.TypeRBAC), - ContractVersion: "0.0.0", + Method: methodPrefix + "::GrantRole(0x95cd540f)", + ContractType: string(bindings.ShortRBAC), + ContractVersion: versionStr, Inputs: []NamedField{ {Name: "QueryID", Value: SimpleField{Value: bigIntStr(queryID)}, RawValue: queryID}, {Name: "Role", Value: SimpleField{Value: s.exampleRoleBig.String()}, RawValue: tlbe.NewUint256(s.exampleRoleBig)}, @@ -259,11 +338,11 @@ func bigIntStr(v uint64) string { return new(big.Int).SetUint64(v).String() } -func makeInvalidTx(contractType string, contractFQN tvm.FullyQualifiedName) types.Transaction { +func makeInvalidTx(contractType string, contractTypeFull tvm.FullyQualifiedName) types.Transaction { return types.Transaction{ - OperationMetadata: types.OperationMetadata{ContractType: contractType, ContractTypeAndVersion: deployment.TypeAndVersion{Type: deployment.ContractType(contractFQN)}.String()}, + OperationMetadata: types.OperationMetadata{ContractType: contractType}, To: testAddress, Data: []byte{0xFF, 0xFF}, - AdditionalFields: json.RawMessage(`{"value":0}`), + AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"` + contractTypeFull + `"}`), } } diff --git a/experimental/analyzer/upf/types.go b/experimental/analyzer/upf/types.go index b20f8df40..65b0d31b4 100644 --- a/experimental/analyzer/upf/types.go +++ b/experimental/analyzer/upf/types.go @@ -4,6 +4,7 @@ import ( "bytes" "slices" + "github.com/Masterminds/semver/v3" mcmstypes "github.com/smartcontractkit/mcms/types" ) @@ -41,10 +42,10 @@ type Transaction struct { } type Metadata struct { - ContractType string `json:"contractType,omitempty"` - ContractTypeAndVersion string `json:"contractTypeAndVersion,omitempty"` - Comment string `json:"comment,omitempty" ` - DecodedCalldata *DecodedCallData `json:"decodedCalldata,omitempty"` + ContractType string `json:"contractType,omitempty"` + ContractVersion *semver.Version `json:"contractVersion,omitempty"` + Comment string `json:"comment,omitempty" ` + DecodedCalldata *DecodedCallData `json:"decodedCalldata,omitempty"` } type DecodedCallData struct { diff --git a/experimental/analyzer/upf/upf.go b/experimental/analyzer/upf/upf.go index 5cb4c6555..d94724b8b 100644 --- a/experimental/analyzer/upf/upf.go +++ b/experimental/analyzer/upf/upf.go @@ -192,9 +192,9 @@ func mcmsOperationToUpfTransaction( FunctionName: analyzeResult.Method, FunctionArgs: upfFunctionArgs, }, - Comment: "", - ContractType: mcmsOp.Transaction.ContractType, - ContractTypeAndVersion: mcmsOp.Transaction.ContractTypeAndVersion, + Comment: "", + ContractType: mcmsOp.Transaction.ContractType, + ContractVersion: mcmsOp.Transaction.ContractVersion, }, }, nil } diff --git a/experimental/analyzer/upf/upf_test.go b/experimental/analyzer/upf/upf_test.go index 8d8f958b3..19ff1f0c3 100644 --- a/experimental/analyzer/upf/upf_test.go +++ b/experimental/analyzer/upf/upf_test.go @@ -2,14 +2,11 @@ package upf import ( "context" - "encoding/json" "fmt" - "math/big" "strings" "testing" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/mcms" mcmssdk "github.com/smartcontractkit/mcms/sdk" @@ -19,15 +16,10 @@ import ( mcmstonsdk "github.com/smartcontractkit/mcms/sdk/ton" mcmstypes "github.com/smartcontractkit/mcms/types" "github.com/stretchr/testify/require" - "github.com/xssnick/tonutils-go/address" - "github.com/xssnick/tonutils-go/tlb" "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_6_0/rmn_remote" rmnremotebindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_0/rmn_remote" timelockbindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/v0_1_0/timelock" - "github.com/smartcontractkit/chainlink-ton/pkg/bindings" - "github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac" - "github.com/smartcontractkit/chainlink-ton/pkg/ton/tlbe" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" @@ -59,6 +51,11 @@ func TestUpfConvertTimelockProposal(t *testing.T) { dsAddContract(t, ds, 16423721717087811551, "68ds9kDfB6rJfC4zzeeQ8E9ZMwqSzFQEie1886VAPn68", "BypasserAccessControllerAccount 1.0.0") dsAddContract(t, ds, 16423721717087811551, "RmnXLft1mSEwDgMKu2okYuHkiazxntFFcZFrrcXxYg7", "RMNRemote 1.0.0") + // ---- TON: testnet + dsAddContract(t, ds, chainsel.TON_TESTNET.Selector, "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", "MCMS 1.0.0") + dsAddContract(t, ds, chainsel.TON_TESTNET.Selector, "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", "Router 1.2.3") + dsAddContract(t, ds, chainsel.TON_TESTNET.Selector, "UQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thiIY", "RBAC 1.0.0") + env := deployment.Environment{ DataStore: ds.Seal(), ExistingAddresses: deployment.NewMemoryAddressBook(), @@ -95,6 +92,9 @@ func TestUpfConvertTimelockProposal(t *testing.T) { common.HexToAddress("0x9A60462e4CA802E3E945663930Be0d162e662091"), common.HexToAddress("0x5f077BCeE6e285154473F65699d6F46Fd03D105A"), }, + mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector): { + common.HexToAddress("0xA5D5B0B844c8f11B61F28AC98BBA84dEA9b80953"), + }, }, }, } @@ -174,63 +174,6 @@ func TestUpfConvertTimelockProposalWithSui(t *testing.T) { } } -func TestUpfConvertTimelockProposalWithTon(t *testing.T) { - t.Parallel() - ds := datastore.NewMemoryDataStore() - - // ---- TON: testnet - dsAddContract(t, ds, chainsel.TON_TESTNET.Selector, "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", "MCMS 1.0.0") - - env := deployment.Environment{ - DataStore: ds.Seal(), - ExistingAddresses: deployment.NewMemoryAddressBook(), - } - - proposalCtx, err := mcmsanalyzer.NewDefaultProposalContext(env) - require.NoError(t, err) - - tests := []struct { - name string - timelockProposal string - signers map[mcmstypes.ChainSelector][]common.Address - assertion func(*testing.T, string, error) - }{ - { - name: "TON proposal with GrantRole transaction", - timelockProposal: timelockProposalTON(t), - signers: map[mcmstypes.ChainSelector][]common.Address{ - mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector): { - common.HexToAddress("0xA5D5B0B844c8f11B61F28AC98BBA84dEA9b80953"), - }, - }, - assertion: func(t *testing.T, gotUpf string, err error) { - t.Helper() - require.NoError(t, err) - // Verify it contains TON-specific content - require.Contains(t, gotUpf, "chainFamily: ton") - require.Contains(t, gotUpf, "chainName: ton-testnet") - require.Contains(t, gotUpf, "msigAddress: EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8") - require.Contains(t, gotUpf, "contractType: RBACTimelock") - require.Contains(t, gotUpf, "contractTypeAndVersion: link.chain.ton.mcms.Timelock 0.0.0") - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - timelockProposal, err := mcms.NewTimelockProposal(strings.NewReader(tt.timelockProposal)) - require.NoError(t, err) - mcmProposal := convertTimelockProposal(t.Context(), t, timelockProposal) - - got, err := UpfConvertTimelockProposal(t.Context(), proposalCtx, env, timelockProposal, mcmProposal, tt.signers) - - tt.assertion(t, got, err) - }) - } -} - // ----- helpers ----- func convertTimelockProposal(ctx context.Context, t *testing.T, timelockProposal *mcms.TimelockProposal) *mcms.Proposal { @@ -289,6 +232,11 @@ var timelockProposalRMNCurse = `{ "cancellerRoleAccessController": "2hABoxD9U5A4j4x3kNDf4dBJ7ZP384Zbs3TuFn9QUTSs", "bypasserRoleAccessController": "68ds9kDfB6rJfC4zzeeQ8E9ZMwqSzFQEie1886VAPn68" } + }, + "1399300952838017768": { + "startingOpCount": 4, + "mcmAddress": "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + "additionalFields": null } }, "description": "simple EVM proposal with RMN curse", @@ -297,7 +245,8 @@ var timelockProposalRMNCurse = `{ "timelockAddresses": { "10344971235874465080": "0x5f077BCeE6e285154473F65699d6F46Fd03D105A", "13264668187771770619": "0x804759c9bdd258A810987FDe21c9E24C5383b722", - "16423721717087811551": "DoajfR5tK24xVw51fWcawUZWhAXD8yrBJVacc13neVQA.E4R6Nwg1K8Zvi6McLdkaGDD5ClX1KkyV" + "16423721717087811551": "DoajfR5tK24xVw51fWcawUZWhAXD8yrBJVacc13neVQA.E4R6Nwg1K8Zvi6McLdkaGDD5ClX1KkyV", + "1399300952838017768": "UQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thiIY" }, "operations": [ { @@ -359,17 +308,47 @@ var timelockProposalRMNCurse = `{ } } ] + }, + { + "chainSelector": 1399300952838017768, + "transactions": [ + { + "contractType": "Router", + "contractVersion": "1.2.3", + "tags": [ + "grantRole" + ], + "to": "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + "data": "te6cckEBAQEAUAAAm5XNVA8AAAAAAAAAAamAjq/xjh1C9snrnzD1Q8U6hLD3ws36CvbU7ZXMJ03KgABtbrdDaTpRJq8NdPXIgcbPqeoYpCTt/LPB0uV2rZgbULu+N7g=", + "additionalFields": { + "contractTypeFull": "link.chain.ton.ccip.Router", + "value": 0 + } + }, + { + "contractType": "RBAC", + "tags": [ + "grantRole" + ], + "to": "UQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thiIY", + "data": "te6cckEBAQEAUAAAm5XNVA8AAAAAAAAAAamAjq/xjh1C9snrnzD1Q8U6hLD3ws36CvbU7ZXMJ03KgABtbrdDaTpRJq8NdPXIgcbPqeoYpCTt/LPB0uV2rZgbULu+N7g=", + "additionalFields": { + "contractTypeFull": "link.chain.ton.lib.access.RBAC", + "value": 0 + } + } + ] } ] }` var upfProposalRMNCurse = `--- msigType: mcms -proposalHash: "0x41ce69645a9ce865c1035dc310e49bcb8057e932d20f50879858fc0b3319f909" +proposalHash: "0x4e1528a57473fd4a244101bd921380b7885b2f774a46d9affdb7a85420a20420" mcmsParams: validUntil: 1999999999 - merkleRoot: "0x963d51589fcf57be4be3f35dd42a9519deaf47754b81a61b2ef48475fb1824bf" - asciiProposalHash: '\xfc&\x9b\xef; \xc6R\xde\xba\x97\xe8\xcd!\x9e\xb3\xe4ya\x99\x17\x8b\x10\xefqY\x82\xe4]\x7f\xd3\xd3' + merkleRoot: "0x7ad0df9b769648dc4a31c43d4b14c27c872082075622b58a79e0bd1bf4d326f4" + asciiProposalHash: '\xf5''hv\x05O\xbc\xcav yk\xae\xe2\xc6\x0eC\xdd\x9e\xd4\xe1XL\x849kI\xef\x13\xa8\x8a\xbe' overridePreviousRoot: false transactions: - index: 0 @@ -572,11 +551,50 @@ transactions: - 11111111111111111111111111111111 Subject: value: 0xfb968f03709115b80000000000000000 +- index: 7 + chainFamily: ton + chainId: "-3" + chainName: ton-testnet + chainShortName: ton-testnet + msigAddress: EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8 + timelockAddress: UQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thiIY + to: UQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thiIY + value: 150000000 + data: te6cckEBAwEA6wABoAlHGPQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdzWT/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEsAQKHgABtbrdDaTpRJq8NdPXIgcbPqeoYpCTt/LPB0uV2rZgbQQA1Zz7x8u5bl1Xid1ZjV0kMYZnvXzkS4978pUQoOm97YYICAgCblc1UDwAAAAAAAAABqYCOr/GOHUL2yeufMPVDxTqEsPfCzfoK9tTtlcwnTcqAAG1ut0NpOlEmrw109ciBxs+p6hikJO38s8HS5XatmBtQro9PYg== + txNonce: 4 + metadata: + contractType: RBACTimelock + decodedCalldata: + functionName: link.chain.ton.mcms.Timelock::ScheduleBatch(0x94718f4) + functionArgs: + Calls: + - map[Data:619[95CD540F0000000000000001A9808EAFF18E1D42F6C9EB9F30F543C53A84B0F7C2CDFA0AF6D4ED95CC274DCA80006D6EB743693A5126AF0D74F5C881C6CFA9EA18A424EDFCB3C1D2E576AD981B4_] Target:EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8 Value:0] + - map[Data:619[95CD540F0000000000000001A9808EAFF18E1D42F6C9EB9F30F543C53A84B0F7C2CDFA0AF6D4ED95CC274DCA80006D6EB743693A5126AF0D74F5C881C6CFA9EA18A424EDFCB3C1D2E576AD981B4_] Target:EQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thn_d Value:0] + Delay: "300" + Predecessor: "0" + QueryID: "0" + Salt: "53919893307341332922183390379372246260254658171444000539666648017328389750784" + calls: + - to: EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8 + value: 0 + data: + functionName: "failed to decode TON transaction: decoding failed - unknown contract interface: link.chain.ton.ccip.Router@1.2.3" + functionArgs: {} + - to: UQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thiIY + value: 0 + data: + functionName: link.chain.ton.lib.access.RBAC::GrantRole(0x95cd540f) + functionArgs: + Account: EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8 + QueryID: "1" + Role: "76668012626274288566148650389907500593868393123329715998308465998801700081098" signers: 10344971235874465080: - "0xA5D5B0B844c8f11B61F28AC98BBA84dEA9b80953" - "0x9A60462e4CA802E3E945663930Be0d162e662091" - "0x5f077BCeE6e285154473F65699d6F46Fd03D105A" + 1399300952838017768: + - "0xA5D5B0B844c8f11B61F28AC98BBA84dEA9b80953" ` //nolint:gosec // G101 all test values @@ -669,59 +687,6 @@ signers: - "0xA5D5B0B844c8f11B61F28AC98BBA84dEA9b80953" ` -// timelockProposalTON is generated using makeTONGrantRoleTx helper -var timelockProposalTON = func(t *testing.T) string { - t.Helper() - // Create a GrantRole transaction for the test - targetAddr := address.MustParseAddr("EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8") - exampleRole := crypto.Keccak256Hash([]byte("EXAMPLE_ROLE")) - grantRoleData, _ := tlb.ToCell(rbac.GrantRole{ - QueryID: 1, - Role: tlbe.NewUint256(new(big.Int).SetBytes(exampleRole[:])), - Account: targetAddr, - }) - - tx, _ := mcmstonsdk.NewTransaction( - targetAddr, - grantRoleData.ToBuilder().ToSlice(), - big.NewInt(0), - bindings.ShortRBAC, - deployment.TypeAndVersion{Type: deployment.ContractType(bindings.TypeRBAC)}.String(), - []string{"grantRole"}, - ) - - // Marshal the transaction data - txData, err := json.Marshal(tx) - require.NoError(t, err) - - return fmt.Sprintf(`{ - "version": "v1", - "kind": "TimelockProposal", - "validUntil": 1999999999, - "signatures": [], - "overridePreviousRoot": false, - "chainMetadata": { - "1399300952838017768": { - "startingOpCount": 1, - "mcmAddress": "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", - "additionalFields": null - } - }, - "description": "simple TON proposal with GrantRole", - "action": "schedule", - "delay": "5m0s", - "timelockAddresses": { - "1399300952838017768": "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8" - }, - "operations": [ - { - "chainSelector": 1399300952838017768, - "transactions": [%s] - } - ] -}`, string(txData)) -} - func TestIsTimelockBatchFunction(t *testing.T) { t.Parallel() diff --git a/go.mod b/go.mod index 532b5823b..5f7c02f28 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250908203554-5bd9d2fe9513 github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e - github.com/smartcontractkit/mcms v0.43.1-0.20260515150551-16b18dab10da + github.com/smartcontractkit/mcms v0.45.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index adced109b..f800f1408 100644 --- a/go.sum +++ b/go.sum @@ -711,8 +711,8 @@ github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12i github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e h1:poXTj5cFVM6XfC4HICIDYkDVc/A6OYB0eeID0wU2JQE= github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= -github.com/smartcontractkit/mcms v0.43.1-0.20260515150551-16b18dab10da h1:Taae0W6lZMPJ810LBl6deQlnScRRxD4ofqHry2A+tAg= -github.com/smartcontractkit/mcms v0.43.1-0.20260515150551-16b18dab10da/go.mod h1:r+3LeY3kO1VJHN5nigMWazXbQi8JOLTvsxBr19LNrOg= +github.com/smartcontractkit/mcms v0.45.0 h1:6Zx80KKLQOPXLhvrRkJKClANnBJmPa/r69CV5UUq/0I= +github.com/smartcontractkit/mcms v0.45.0/go.mod h1:/uOE69QmF7opKlaBNzp8djypmBoYSW0mk4V2iKWP418= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=