From 2aabdbf5a8c2dc7b1cf32bb110bd941c4ae20f1d Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 11:05:19 -0300 Subject: [PATCH 01/17] feat: ContractVersion in OperationMetadata (#979) feat: ContractFullyQualifiedName in TON's AdditionalFields fix: revert ContractTypeAndVersion in OperationMetadata (#979) This reverts commit 68a7a80b6be0bde000aafd435089b994c38a2a23. --- .../cld/commands/mcms/testdata/proposal.json | 10 ++-- .../internal/mcmsutils/testdata/proposal.json | 2 +- .../mcmsutils/testdata/timelock_proposal.json | 8 +-- experimental/analyzer/ton_analyzer.go | 28 ++++------ experimental/analyzer/ton_analyzer_test.go | 52 +++++++++++++------ experimental/analyzer/upf/types.go | 9 ++-- experimental/analyzer/upf/upf.go | 6 +-- experimental/analyzer/upf/upf_test.go | 6 ++- go.mod | 2 +- go.sum | 4 +- 10 files changed, 72 insertions(+), 55 deletions(-) diff --git a/engine/cld/commands/mcms/testdata/proposal.json b/engine/cld/commands/mcms/testdata/proposal.json index a17fc60c7..ea63a6d64 100644 --- a/engine/cld/commands/mcms/testdata/proposal.json +++ b/engine/cld/commands/mcms/testdata/proposal.json @@ -24,7 +24,7 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", + "contractVersion": "", "tags": null }, { @@ -32,7 +32,7 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", + "contractVersion": "", "tags": null }, { @@ -40,7 +40,7 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", + "contractVersion": "", "tags": null }, { @@ -48,7 +48,7 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", + "contractVersion": "", "tags": null }, { @@ -56,7 +56,7 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractTypeAndVersion": "", + "contractVersion": "", "tags": null } ] diff --git a/engine/test/internal/mcmsutils/testdata/proposal.json b/engine/test/internal/mcmsutils/testdata/proposal.json index 9cb8cc768..70ba31f30 100644 --- a/engine/test/internal/mcmsutils/testdata/proposal.json +++ b/engine/test/internal/mcmsutils/testdata/proposal.json @@ -23,7 +23,7 @@ "data": "", "tags": null, "contractType": "", - "contractTypeAndVersion": "" + "contractVersion": "" } } ] diff --git a/engine/test/internal/mcmsutils/testdata/timelock_proposal.json b/engine/test/internal/mcmsutils/testdata/timelock_proposal.json index c77c92a08..9fbfb4d55 100644 --- a/engine/test/internal/mcmsutils/testdata/timelock_proposal.json +++ b/engine/test/internal/mcmsutils/testdata/timelock_proposal.json @@ -23,13 +23,15 @@ "transactions": [ { "to": "0x0000000000000000000000000000000000000000", - "additionalFields": {"value": 0}, + "additionalFields": { + "value": 0 + }, "data": "ZGF0YQ==", "contractType": "", - "contractTypeAndVersion": "", + "contractVersion": "", "tags": null } ] } ] -} +} \ No newline at end of file diff --git a/experimental/analyzer/ton_analyzer.go b/experimental/analyzer/ton_analyzer.go index fd43e1303..b7f537efa 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" ) @@ -37,19 +36,14 @@ func AnalyzeTONTransactions(ctx ProposalContext, chainSelector uint64, txs []typ // 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 + var additionalFields ton.AdditionalFields + if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil { + return nil, fmt.Errorf("failed to unmarshal TON additional fields: %w", err) } - decodedOp, err := decoder.Decode(mcmsTx, contractTypeAndVersion.Type.String()) + + decodedOp, err := decoder.Decode(mcmsTx, string(additionalFields.ContractFullyQualifiedName)) if err != 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. @@ -58,8 +52,8 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect return &DecodedCall{ Address: mcmsTx.To, Method: errStr.Error(), - ContractType: contractTypeAndVersion.Type.String(), - ContractVersion: contractTypeAndVersion.Version.String(), + ContractType: contractType, + ContractVersion: contractVersion, }, nil } @@ -73,7 +67,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..37d8918c6 100644 --- a/experimental/analyzer/ton_analyzer_test.go +++ b/experimental/analyzer/ton_analyzer_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/crypto" "github.com/smartcontractkit/mcms/sdk/ton" "github.com/smartcontractkit/mcms/types" @@ -49,21 +50,21 @@ 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}`), + AdditionalFields: json.RawMessage(`{"value":0, "contractFullyQualifiedName":"com.example.package.unknown.contract"}`), }, want: &DecodedCall{Address: testAddress}, - wantErrContain: "unknown contract interface: com.example.package.unknown.contract", + wantErrContain: "unknown contract interface: unknown.type", }, { 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, "contractFullyQualifiedName":"` + bindings.TypeMCMS + `"}`), }, want: &DecodedCall{Address: testAddress}, wantErrContain: "invalid cell BOC data", @@ -173,7 +174,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 +188,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 +201,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 +250,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(), + semver.MustParse("1.2.3"), + bindings.TypeRBAC, []string{"grantRole"}, ) require.NoError(t, err) @@ -242,10 +261,9 @@ func (s *testTONSetup) makeGrantRoleTx(t *testing.T, queryID uint64) types.Trans func (s *testTONSetup) expectedGrantRoleCall(queryID uint64) *DecodedCall { return &DecodedCall{ - Address: s.targetAddr.String(), - Method: string(bindings.TypeRBAC) + "::GrantRole(0x95cd540f)", - ContractType: string(bindings.TypeRBAC), - ContractVersion: "0.0.0", + Address: s.targetAddr.String(), + Method: string(bindings.TypeRBAC) + "::GrantRole(0x95cd540f)", + ContractType: string(bindings.ShortRBAC), 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 +277,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, contractFullyQualifiedName 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, "contractFullyQualifiedName":"` + contractFullyQualifiedName + `"}`), } } 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..9bacf2b99 100644 --- a/experimental/analyzer/upf/upf_test.go +++ b/experimental/analyzer/upf/upf_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" @@ -211,7 +212,7 @@ func TestUpfConvertTimelockProposalWithTon(t *testing.T) { 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") + require.Contains(t, gotUpf, "contractVersion: 1.2.3") }, }, } @@ -686,7 +687,8 @@ var timelockProposalTON = func(t *testing.T) string { grantRoleData.ToBuilder().ToSlice(), big.NewInt(0), bindings.ShortRBAC, - deployment.TypeAndVersion{Type: deployment.ContractType(bindings.TypeRBAC)}.String(), + semver.MustParse("1.2.3"), + bindings.TypeRBAC, []string{"grantRole"}, ) diff --git a/go.mod b/go.mod index 532b5823b..b12eaf863 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.44.1-0.20260518140146-3cd2e6339f56 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..5bbd04c9f 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.44.1-0.20260518140146-3cd2e6339f56 h1:5VwUthyd9kiMxrJExl5vWxHhyv/1ngmaRd9Gz5cCI/Q= +github.com/smartcontractkit/mcms v0.44.1-0.20260518140146-3cd2e6339f56/go.mod h1:r+3LeY3kO1VJHN5nigMWazXbQi8JOLTvsxBr19LNrOg= 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= From 5311b31c7fb460fa82bb0680daffd2df96452e62 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 13:36:14 -0300 Subject: [PATCH 02/17] fix: add version to fully qualified name --- experimental/analyzer/decoded_call.go | 19 ++++++++++------ experimental/analyzer/report_builder_test.go | 9 ++++---- experimental/analyzer/ton_analyzer.go | 6 ++++- experimental/analyzer/ton_analyzer_test.go | 23 +++++++++++++++----- experimental/analyzer/upf/upf_test.go | 4 +--- go.mod | 2 +- go.sum | 4 ++-- 7 files changed, 43 insertions(+), 24 deletions(-) diff --git a/experimental/analyzer/decoded_call.go b/experimental/analyzer/decoded_call.go index da9bc1cbb..ae254d39f 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) { + // Prefer the transaction's own ContractVersion when available (set by proposal creator) + 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 - if tv.Version.Original() != "" { - cv = tv.Version.String() + // If version wasn't already set from transaction metadata, use datastore version + if contractVersion == "" && tv.Version.Original() != "" { + contractVersion = tv.Version.String() } - return ct, cv + return ct, contractVersion } diff --git a/experimental/analyzer/report_builder_test.go b/experimental/analyzer/report_builder_test.go index d92cd9cbd..a31e24d83 100644 --- a/experimental/analyzer/report_builder_test.go +++ b/experimental/analyzer/report_builder_test.go @@ -272,8 +272,9 @@ func TestBuildProposalReport_TONDecodeFailure(t *testing.T) { { ChainSelector: types.ChainSelector(chainsel.TON_TESTNET.Selector), Transaction: types.Transaction{ - To: "0x1234567890123456789012345678901234567890", - Data: []byte{0x01, 0x02, 0x03, 0x04}, + To: "0x1234567890123456789012345678901234567890", + Data: []byte{0x01, 0x02, 0x03, 0x04}, + AdditionalFields: json.RawMessage("{}"), }, }, }, @@ -361,8 +362,8 @@ func TestBuildTimelockReport_TONDecodeFailure(t *testing.T) { { ChainSelector: types.ChainSelector(chainsel.TON_TESTNET.Selector), Transactions: []types.Transaction{ - {To: "0x1111111111111111111111111111111111111111", Data: []byte{0x01, 0x02, 0x03, 0x04}}, - {To: "0x2222222222222222222222222222222222222222", Data: []byte{0x05, 0x06, 0x07, 0x08}}, + {To: "0x1111111111111111111111111111111111111111", Data: []byte{0x01, 0x02, 0x03, 0x04}, AdditionalFields: json.RawMessage(`{}`)}, + {To: "0x2222222222222222222222222222222222222222", Data: []byte{0x05, 0x06, 0x07, 0x08}, AdditionalFields: json.RawMessage(`{}`)}, }, }, }, diff --git a/experimental/analyzer/ton_analyzer.go b/experimental/analyzer/ton_analyzer.go index b7f537efa..6bb9b1f92 100644 --- a/experimental/analyzer/ton_analyzer.go +++ b/experimental/analyzer/ton_analyzer.go @@ -43,7 +43,11 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect return nil, fmt.Errorf("failed to unmarshal TON additional fields: %w", err) } - decodedOp, err := decoder.Decode(mcmsTx, string(additionalFields.ContractFullyQualifiedName)) + fullyQualifiedName := string(additionalFields.ContractTypeFull) + if mcmsTx.ContractVersion != nil { + fullyQualifiedName += "@" + contractVersion + } + decodedOp, err := decoder.Decode(mcmsTx, fullyQualifiedName) if err != 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. diff --git a/experimental/analyzer/ton_analyzer_test.go b/experimental/analyzer/ton_analyzer_test.go index 37d8918c6..d9a173c5a 100644 --- a/experimental/analyzer/ton_analyzer_test.go +++ b/experimental/analyzer/ton_analyzer_test.go @@ -53,10 +53,21 @@ func TestAnalyzeTONTransaction(t *testing.T) { OperationMetadata: types.OperationMetadata{ContractType: "unknown.type", ContractVersion: semver.MustParse("1.2.3")}, To: testAddress, Data: []byte{0x01, 0x02}, - AdditionalFields: json.RawMessage(`{"value":0, "contractFullyQualifiedName":"com.example.package.unknown.contract"}`), + AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"com.example.package.unknown.contract"}`), }, want: &DecodedCall{Address: testAddress}, - wantErrContain: "unknown contract interface: unknown.type", + 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: "empty data", @@ -64,7 +75,7 @@ func TestAnalyzeTONTransaction(t *testing.T) { OperationMetadata: types.OperationMetadata{ContractType: bindings.ShortMCMS}, To: testAddress, Data: []byte{}, - AdditionalFields: json.RawMessage(`{"value":0, "contractFullyQualifiedName":"` + bindings.TypeMCMS + `"}`), + AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"` + bindings.TypeMCMS + `"}`), }, want: &DecodedCall{Address: testAddress}, wantErrContain: "invalid cell BOC data", @@ -250,7 +261,7 @@ func (s *testTONSetup) makeGrantRoleTx(t *testing.T, queryID uint64) types.Trans grantRoleData.ToBuilder().ToSlice(), big.NewInt(0), bindings.ShortRBAC, - semver.MustParse("1.2.3"), + nil, bindings.TypeRBAC, []string{"grantRole"}, ) @@ -277,11 +288,11 @@ func bigIntStr(v uint64) string { return new(big.Int).SetUint64(v).String() } -func makeInvalidTx(contractType string, contractFullyQualifiedName tvm.FullyQualifiedName) types.Transaction { +func makeInvalidTx(contractType string, contractTypeFull tvm.FullyQualifiedName) types.Transaction { return types.Transaction{ OperationMetadata: types.OperationMetadata{ContractType: contractType}, To: testAddress, Data: []byte{0xFF, 0xFF}, - AdditionalFields: json.RawMessage(`{"value":0, "contractFullyQualifiedName":"` + contractFullyQualifiedName + `"}`), + AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"` + contractTypeFull + `"}`), } } diff --git a/experimental/analyzer/upf/upf_test.go b/experimental/analyzer/upf/upf_test.go index 9bacf2b99..71a6dd7fe 100644 --- a/experimental/analyzer/upf/upf_test.go +++ b/experimental/analyzer/upf/upf_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" @@ -212,7 +211,6 @@ func TestUpfConvertTimelockProposalWithTon(t *testing.T) { require.Contains(t, gotUpf, "chainName: ton-testnet") require.Contains(t, gotUpf, "msigAddress: EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8") require.Contains(t, gotUpf, "contractType: RBACTimelock") - require.Contains(t, gotUpf, "contractVersion: 1.2.3") }, }, } @@ -687,7 +685,7 @@ var timelockProposalTON = func(t *testing.T) string { grantRoleData.ToBuilder().ToSlice(), big.NewInt(0), bindings.ShortRBAC, - semver.MustParse("1.2.3"), + nil, bindings.TypeRBAC, []string{"grantRole"}, ) diff --git a/go.mod b/go.mod index b12eaf863..7ef61be72 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.44.1-0.20260518140146-3cd2e6339f56 + github.com/smartcontractkit/mcms v0.44.1-0.20260518162713-bb4db0d68e34 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 5bbd04c9f..b19351fbe 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.44.1-0.20260518140146-3cd2e6339f56 h1:5VwUthyd9kiMxrJExl5vWxHhyv/1ngmaRd9Gz5cCI/Q= -github.com/smartcontractkit/mcms v0.44.1-0.20260518140146-3cd2e6339f56/go.mod h1:r+3LeY3kO1VJHN5nigMWazXbQi8JOLTvsxBr19LNrOg= +github.com/smartcontractkit/mcms v0.44.1-0.20260518162713-bb4db0d68e34 h1:PsijUMZtxtA5hYQRWRqQhLhpdbSEkFv9tU9f5zQlVrY= +github.com/smartcontractkit/mcms v0.44.1-0.20260518162713-bb4db0d68e34/go.mod h1:r+3LeY3kO1VJHN5nigMWazXbQi8JOLTvsxBr19LNrOg= 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= From f7f09ea3590115527573419bd1d76e768fa51ed5 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 13:37:56 -0300 Subject: [PATCH 03/17] feat: changeset --- .changeset/bold-olives-smell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bold-olives-smell.md 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 From 42f1045a30ceba25bc6da8c1c0f524735ea56108 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 14:18:54 -0300 Subject: [PATCH 04/17] fix: contractVersion ommit empty --- engine/cld/commands/mcms/testdata/proposal.json | 5 ----- engine/test/internal/mcmsutils/testdata/proposal.json | 3 +-- .../test/internal/mcmsutils/testdata/timelock_proposal.json | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/engine/cld/commands/mcms/testdata/proposal.json b/engine/cld/commands/mcms/testdata/proposal.json index ea63a6d64..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": "", - "contractVersion": "", "tags": null }, { @@ -32,7 +31,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractVersion": "", "tags": null }, { @@ -40,7 +38,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractVersion": "", "tags": null }, { @@ -48,7 +45,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractVersion": "", "tags": null }, { @@ -56,7 +52,6 @@ "data": "ebpQlw==", "value": 0, "contractType": "", - "contractVersion": "", "tags": null } ] diff --git a/engine/test/internal/mcmsutils/testdata/proposal.json b/engine/test/internal/mcmsutils/testdata/proposal.json index 70ba31f30..01cc67ef8 100644 --- a/engine/test/internal/mcmsutils/testdata/proposal.json +++ b/engine/test/internal/mcmsutils/testdata/proposal.json @@ -22,8 +22,7 @@ }, "data": "", "tags": null, - "contractType": "", - "contractVersion": "" + "contractType": "" } } ] diff --git a/engine/test/internal/mcmsutils/testdata/timelock_proposal.json b/engine/test/internal/mcmsutils/testdata/timelock_proposal.json index 9fbfb4d55..aa7f13c5c 100644 --- a/engine/test/internal/mcmsutils/testdata/timelock_proposal.json +++ b/engine/test/internal/mcmsutils/testdata/timelock_proposal.json @@ -28,7 +28,6 @@ }, "data": "ZGF0YQ==", "contractType": "", - "contractVersion": "", "tags": null } ] From c314860492037559c34d747293430dcf52b3c119 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 14:46:16 -0300 Subject: [PATCH 05/17] fix: bump imports --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7ef61be72..1b003fda9 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.44.1-0.20260518162713-bb4db0d68e34 + github.com/smartcontractkit/mcms v0.44.1-0.20260518174523-2a534bb72c2d 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 b19351fbe..7a4a8f1bb 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.44.1-0.20260518162713-bb4db0d68e34 h1:PsijUMZtxtA5hYQRWRqQhLhpdbSEkFv9tU9f5zQlVrY= -github.com/smartcontractkit/mcms v0.44.1-0.20260518162713-bb4db0d68e34/go.mod h1:r+3LeY3kO1VJHN5nigMWazXbQi8JOLTvsxBr19LNrOg= +github.com/smartcontractkit/mcms v0.44.1-0.20260518174523-2a534bb72c2d h1:n/B8vb8RSZ7NEPqMueVZ/BanoTo1BEOMlAbTEEx250E= +github.com/smartcontractkit/mcms v0.44.1-0.20260518174523-2a534bb72c2d/go.mod h1:r+3LeY3kO1VJHN5nigMWazXbQi8JOLTvsxBr19LNrOg= 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= From 651039c2bf5d12628c00a6ba4fa41e44d293df0c Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 15:03:28 -0300 Subject: [PATCH 06/17] fix: empty cell --- experimental/analyzer/ton_analyzer_test.go | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/experimental/analyzer/ton_analyzer_test.go b/experimental/analyzer/ton_analyzer_test.go index d9a173c5a..5f3a56add 100644 --- a/experimental/analyzer/ton_analyzer_test.go +++ b/experimental/analyzer/ton_analyzer_test.go @@ -12,6 +12,7 @@ import ( "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" @@ -52,8 +53,13 @@ func TestAnalyzeTONTransaction(t *testing.T) { mcmsTx: types.Transaction{ OperationMetadata: types.OperationMetadata{ContractType: "unknown.type", ContractVersion: semver.MustParse("1.2.3")}, To: testAddress, - Data: []byte{0x01, 0x02}, - AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"com.example.package.unknown.contract"}`), + 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", @@ -80,6 +86,24 @@ func TestAnalyzeTONTransaction(t *testing.T) { 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 { From 88be8e41fb3a647f783fb079b22d44327714dd32 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 16:27:49 -0300 Subject: [PATCH 07/17] fix: apply suggestions --- .../test/internal/mcmsutils/encoding_test.go | 2 +- .../internal/mcmsutils/testdata/proposal.json | 13 ++++++++ experimental/analyzer/report_builder_test.go | 9 +++--- experimental/analyzer/ton_analyzer.go | 30 ++++++++++++------- go.mod | 2 +- go.sum | 4 +-- 6 files changed, 41 insertions(+), 19 deletions(-) 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 01cc67ef8..a4168c056 100644 --- a/engine/test/internal/mcmsutils/testdata/proposal.json +++ b/engine/test/internal/mcmsutils/testdata/proposal.json @@ -24,6 +24,19 @@ "tags": null, "contractType": "" } + }, + { + "chainSelector": 3379446385462418246, + "transaction": { + "to": "0x123", + "additionalFields": { + "value": 0 + }, + "data": "", + "tags": null, + "contractType": "", + "contractVersion": "1.2.3" + } } ] } \ No newline at end of file diff --git a/experimental/analyzer/report_builder_test.go b/experimental/analyzer/report_builder_test.go index a31e24d83..d92cd9cbd 100644 --- a/experimental/analyzer/report_builder_test.go +++ b/experimental/analyzer/report_builder_test.go @@ -272,9 +272,8 @@ func TestBuildProposalReport_TONDecodeFailure(t *testing.T) { { ChainSelector: types.ChainSelector(chainsel.TON_TESTNET.Selector), Transaction: types.Transaction{ - To: "0x1234567890123456789012345678901234567890", - Data: []byte{0x01, 0x02, 0x03, 0x04}, - AdditionalFields: json.RawMessage("{}"), + To: "0x1234567890123456789012345678901234567890", + Data: []byte{0x01, 0x02, 0x03, 0x04}, }, }, }, @@ -362,8 +361,8 @@ func TestBuildTimelockReport_TONDecodeFailure(t *testing.T) { { ChainSelector: types.ChainSelector(chainsel.TON_TESTNET.Selector), Transactions: []types.Transaction{ - {To: "0x1111111111111111111111111111111111111111", Data: []byte{0x01, 0x02, 0x03, 0x04}, AdditionalFields: json.RawMessage(`{}`)}, - {To: "0x2222222222222222222222222222222222222222", Data: []byte{0x05, 0x06, 0x07, 0x08}, AdditionalFields: json.RawMessage(`{}`)}, + {To: "0x1111111111111111111111111111111111111111", Data: []byte{0x01, 0x02, 0x03, 0x04}}, + {To: "0x2222222222222222222222222222222222222222", Data: []byte{0x05, 0x06, 0x07, 0x08}}, }, }, }, diff --git a/experimental/analyzer/ton_analyzer.go b/experimental/analyzer/ton_analyzer.go index 6bb9b1f92..5bc71e5f4 100644 --- a/experimental/analyzer/ton_analyzer.go +++ b/experimental/analyzer/ton_analyzer.go @@ -38,24 +38,34 @@ func AnalyzeTONTransactions(ctx ProposalContext, chainSelector uint64, txs []typ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelector uint64, mcmsTx types.Transaction) (*DecodedCall, error) { contractType, contractVersion := resolveContractInfo(ctx, chainSelector, mcmsTx) - var additionalFields ton.AdditionalFields - if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil { - return nil, fmt.Errorf("failed to unmarshal TON additional fields: %w", err) - } + var errStr string + fullyQualifiedName := func() string { + var additionalFields ton.AdditionalFields + if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil { + errStr = fmt.Sprintf("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 contractType from resolveContractInfo because that only represents the short type used by the datastore. + if mcmsTx.ContractVersion != nil { + fullyQualifiedName += "@" + contractVersion + } + + return fullyQualifiedName + }() - fullyQualifiedName := string(additionalFields.ContractTypeFull) - if mcmsTx.ContractVersion != nil { - fullyQualifiedName += "@" + contractVersion - } decodedOp, err := decoder.Decode(mcmsTx, fullyQualifiedName) if err != 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 = fmt.Sprintf("failed to decode TON transaction: %s. failed to unmarshal additional fields: %s", err, errStr) return &DecodedCall{ Address: mcmsTx.To, - Method: errStr.Error(), + Method: errStr, ContractType: contractType, ContractVersion: contractVersion, }, nil diff --git a/go.mod b/go.mod index 1b003fda9..ce70e5c2e 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.44.1-0.20260518174523-2a534bb72c2d + github.com/smartcontractkit/mcms v0.44.1-0.20260518185333-8672938e14c9 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 7a4a8f1bb..6d9a13a15 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.44.1-0.20260518174523-2a534bb72c2d h1:n/B8vb8RSZ7NEPqMueVZ/BanoTo1BEOMlAbTEEx250E= -github.com/smartcontractkit/mcms v0.44.1-0.20260518174523-2a534bb72c2d/go.mod h1:r+3LeY3kO1VJHN5nigMWazXbQi8JOLTvsxBr19LNrOg= +github.com/smartcontractkit/mcms v0.44.1-0.20260518185333-8672938e14c9 h1:wgGVicYQc4GVdPBKTTaHJgnMi42vVQOLgfNY2+ozlM8= +github.com/smartcontractkit/mcms v0.44.1-0.20260518185333-8672938e14c9/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= From 237b655d1a0c67f8e2c1f07e05ede2788de42d63 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 16:57:43 -0300 Subject: [PATCH 08/17] fix: bump imports --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ce70e5c2e..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.44.1-0.20260518185333-8672938e14c9 + 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 6d9a13a15..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.44.1-0.20260518185333-8672938e14c9 h1:wgGVicYQc4GVdPBKTTaHJgnMi42vVQOLgfNY2+ozlM8= -github.com/smartcontractkit/mcms v0.44.1-0.20260518185333-8672938e14c9/go.mod h1:/uOE69QmF7opKlaBNzp8djypmBoYSW0mk4V2iKWP418= +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= From 4d69cd71d8cb0deb20733bce7e9ba1aa9890cb12 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 18 May 2026 17:22:09 -0300 Subject: [PATCH 09/17] test: contractVersion in proposal --- experimental/analyzer/ton_analyzer.go | 11 +++-- experimental/analyzer/upf/upf_test.go | 68 +++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/experimental/analyzer/ton_analyzer.go b/experimental/analyzer/ton_analyzer.go index 5bc71e5f4..f2e2123c3 100644 --- a/experimental/analyzer/ton_analyzer.go +++ b/experimental/analyzer/ton_analyzer.go @@ -38,18 +38,18 @@ func AnalyzeTONTransactions(ctx ProposalContext, chainSelector uint64, txs []typ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelector uint64, mcmsTx types.Transaction) (*DecodedCall, error) { contractType, contractVersion := resolveContractInfo(ctx, chainSelector, mcmsTx) - var errStr string + var typeErr string fullyQualifiedName := func() string { var additionalFields ton.AdditionalFields if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil { - errStr = fmt.Sprintf("failed to unmarshal TON additional fields: %s", err) + typeErr = fmt.Sprintf("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 contractType from resolveContractInfo because that only represents the short type used by the datastore. + // Note: we don't use contractVersion from resolveContractInfo because that only represents the short type used by the datastore. if mcmsTx.ContractVersion != nil { fullyQualifiedName += "@" + contractVersion } @@ -61,7 +61,10 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect if err != 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.Sprintf("failed to decode TON transaction: %s. failed to unmarshal additional fields: %s", err, errStr) + errStr := "failed to decode TON transaction: " + err.Error() + if typeErr != "" { + errStr += "; additionally" + typeErr + } return &DecodedCall{ Address: mcmsTx.To, diff --git a/experimental/analyzer/upf/upf_test.go b/experimental/analyzer/upf/upf_test.go index 71a6dd7fe..32e4f94cf 100644 --- a/experimental/analyzer/upf/upf_test.go +++ b/experimental/analyzer/upf/upf_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" @@ -230,6 +231,73 @@ func TestUpfConvertTimelockProposalWithTon(t *testing.T) { } } +func TestUpfConvertProposalWithTonContractVersion(t *testing.T) { + t.Parallel() + ds := datastore.NewMemoryDataStore() + + 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) + + 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, err := mcmstonsdk.NewTransaction( + targetAddr, + grantRoleData.ToBuilder().ToSlice(), + big.NewInt(0), + bindings.ShortRBAC, + semver.MustParse("1.2.3"), + bindings.TypeRBAC, + []string{"grantRole"}, + ) + require.NoError(t, err) + + proposal := &mcms.Proposal{ + BaseProposal: mcms.BaseProposal{ + Version: "v1", + Kind: "Proposal", + ValidUntil: 1999999999, + ChainMetadata: map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata{ + mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector): { + StartingOpCount: 1, + MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + }, + }, + Description: "TON proposal with ContractVersion", + }, + Operations: []mcmstypes.Operation{ + { + ChainSelector: mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector), + Transaction: tx, + }, + }, + } + + signers := map[mcmstypes.ChainSelector][]common.Address{ + mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector): { + common.HexToAddress("0xA5D5B0B844c8f11B61F28AC98BBA84dEA9b80953"), + }, + } + + got, err := UpfConvertProposal(t.Context(), proposalCtx, env, proposal, signers) + require.NoError(t, err) + require.Contains(t, got, "chainFamily: ton") + require.Contains(t, got, "contractType: RBAC") + require.Contains(t, got, "contractVersion: 1.2.3") +} + // ----- helpers ----- func convertTimelockProposal(ctx context.Context, t *testing.T, timelockProposal *mcms.TimelockProposal) *mcms.Proposal { From d558e85fe6faaa815e6f53f286ab502649530cb4 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Tue, 19 May 2026 09:04:47 -0300 Subject: [PATCH 10/17] fix: give ds version higher priority --- experimental/analyzer/decoded_call.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/analyzer/decoded_call.go b/experimental/analyzer/decoded_call.go index ae254d39f..821bca407 100644 --- a/experimental/analyzer/decoded_call.go +++ b/experimental/analyzer/decoded_call.go @@ -31,7 +31,7 @@ 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) { - // Prefer the transaction's own ContractVersion when available (set by proposal creator) + // Use ContractVersion from transaction metadata (set by proposal creator) as fallback if mcmsTx.ContractVersion != nil { contractVersion = mcmsTx.ContractVersion.String() } @@ -56,8 +56,8 @@ func resolveContractInfo(ctx ProposalContext, chainSelector uint64, mcmsTx types ct = mcmsTx.ContractType } - // If version wasn't already set from transaction metadata, use datastore version - if contractVersion == "" && tv.Version.Original() != "" { + // Override with the DataStore version if available + if tv.Version.Original() != "" { contractVersion = tv.Version.String() } From aad11d8d5ba437bc1d96a79a5cc4c7ff816ba0ec Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Tue, 19 May 2026 09:05:13 -0300 Subject: [PATCH 11/17] test: contract version in method prefix --- experimental/analyzer/ton_analyzer_test.go | 38 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/experimental/analyzer/ton_analyzer_test.go b/experimental/analyzer/ton_analyzer_test.go index 5f3a56add..b4a9f1864 100644 --- a/experimental/analyzer/ton_analyzer_test.go +++ b/experimental/analyzer/ton_analyzer_test.go @@ -7,6 +7,7 @@ import ( "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" @@ -33,6 +34,7 @@ func TestAnalyzeTONTransaction(t *testing.T) { tests := []struct { name string + decoder sdk.Decoder mcmsTx types.Transaction want *DecodedCall wantErrContain string @@ -75,6 +77,19 @@ func TestAnalyzeTONTransaction(t *testing.T) { want: &DecodedCall{Address: setup.targetAddr.String()}, wantErrContain: "unknown contract interface: link.chain.ton.lib.access.RBAC@0.0.0", }, + { + name: "success - RBAC GrantRol 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{ @@ -109,8 +124,11 @@ func TestAnalyzeTONTransaction(t *testing.T) { 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) @@ -294,11 +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.ShortRBAC), + Address: s.targetAddr.String(), + 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)}, From 9620685102702cbf71330b18e24fab9aa278cdc7 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Tue, 19 May 2026 10:41:46 -0300 Subject: [PATCH 12/17] ref: merge ton test into generic test --- experimental/analyzer/upf/upf_test.go | 216 +++++++++++--------------- 1 file changed, 88 insertions(+), 128 deletions(-) diff --git a/experimental/analyzer/upf/upf_test.go b/experimental/analyzer/upf/upf_test.go index 32e4f94cf..8cc509a40 100644 --- a/experimental/analyzer/upf/upf_test.go +++ b/experimental/analyzer/upf/upf_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" @@ -60,6 +59,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(), @@ -96,6 +100,9 @@ func TestUpfConvertTimelockProposal(t *testing.T) { common.HexToAddress("0x9A60462e4CA802E3E945663930Be0d162e662091"), common.HexToAddress("0x5f077BCeE6e285154473F65699d6F46Fd03D105A"), }, + mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector): { + common.HexToAddress("0xA5D5B0B844c8f11B61F28AC98BBA84dEA9b80953"), + }, }, }, } @@ -175,129 +182,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") - }, - }, - } - - 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) - }) - } -} - -func TestUpfConvertProposalWithTonContractVersion(t *testing.T) { - t.Parallel() - ds := datastore.NewMemoryDataStore() - - 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) - - 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, err := mcmstonsdk.NewTransaction( - targetAddr, - grantRoleData.ToBuilder().ToSlice(), - big.NewInt(0), - bindings.ShortRBAC, - semver.MustParse("1.2.3"), - bindings.TypeRBAC, - []string{"grantRole"}, - ) - require.NoError(t, err) - - proposal := &mcms.Proposal{ - BaseProposal: mcms.BaseProposal{ - Version: "v1", - Kind: "Proposal", - ValidUntil: 1999999999, - ChainMetadata: map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata{ - mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector): { - StartingOpCount: 1, - MCMAddress: "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", - }, - }, - Description: "TON proposal with ContractVersion", - }, - Operations: []mcmstypes.Operation{ - { - ChainSelector: mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector), - Transaction: tx, - }, - }, - } - - signers := map[mcmstypes.ChainSelector][]common.Address{ - mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector): { - common.HexToAddress("0xA5D5B0B844c8f11B61F28AC98BBA84dEA9b80953"), - }, - } - - got, err := UpfConvertProposal(t.Context(), proposalCtx, env, proposal, signers) - require.NoError(t, err) - require.Contains(t, got, "chainFamily: ton") - require.Contains(t, got, "contractType: RBAC") - require.Contains(t, got, "contractVersion: 1.2.3") -} - // ----- helpers ----- func convertTimelockProposal(ctx context.Context, t *testing.T, timelockProposal *mcms.TimelockProposal) *mcms.Proposal { @@ -356,6 +240,11 @@ var timelockProposalRMNCurse = `{ "cancellerRoleAccessController": "2hABoxD9U5A4j4x3kNDf4dBJ7ZP384Zbs3TuFn9QUTSs", "bypasserRoleAccessController": "68ds9kDfB6rJfC4zzeeQ8E9ZMwqSzFQEie1886VAPn68" } + }, + "1399300952838017768": { + "startingOpCount": 4, + "mcmAddress": "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + "additionalFields": null } }, "description": "simple EVM proposal with RMN curse", @@ -364,7 +253,9 @@ var timelockProposalRMNCurse = `{ "timelockAddresses": { "10344971235874465080": "0x5f077BCeE6e285154473F65699d6F46Fd03D105A", "13264668187771770619": "0x804759c9bdd258A810987FDe21c9E24C5383b722", - "16423721717087811551": "DoajfR5tK24xVw51fWcawUZWhAXD8yrBJVacc13neVQA.E4R6Nwg1K8Zvi6McLdkaGDD5ClX1KkyV" + "16423721717087811551": "DoajfR5tK24xVw51fWcawUZWhAXD8yrBJVacc13neVQA.E4R6Nwg1K8Zvi6McLdkaGDD5ClX1KkyV", + "1399300952838017768": "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", + "1399300952838017768": "UQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thiIY" }, "operations": [ { @@ -426,17 +317,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 @@ -639,11 +560,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 From 99a5e84a1253f8e220d4575bf6a7e4908aae3b97 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Tue, 19 May 2026 10:48:21 -0300 Subject: [PATCH 13/17] fix: use mcmsTx.ContractVersion --- experimental/analyzer/ton_analyzer.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/experimental/analyzer/ton_analyzer.go b/experimental/analyzer/ton_analyzer.go index f2e2123c3..09633b37d 100644 --- a/experimental/analyzer/ton_analyzer.go +++ b/experimental/analyzer/ton_analyzer.go @@ -28,10 +28,6 @@ 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. @@ -51,7 +47,7 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect // 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 += "@" + contractVersion + fullyQualifiedName += "@" + mcmsTx.ContractVersion.String() } return fullyQualifiedName From 8bc04c033065150466b0fd98a008ff74f498cf8a Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Tue, 19 May 2026 10:57:59 -0300 Subject: [PATCH 14/17] fix: error concatenation --- experimental/analyzer/ton_analyzer.go | 11 ++++------- experimental/analyzer/ton_analyzer_test.go | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/experimental/analyzer/ton_analyzer.go b/experimental/analyzer/ton_analyzer.go index 09633b37d..89737eeee 100644 --- a/experimental/analyzer/ton_analyzer.go +++ b/experimental/analyzer/ton_analyzer.go @@ -38,7 +38,7 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect fullyQualifiedName := func() string { var additionalFields ton.AdditionalFields if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil { - typeErr = fmt.Sprintf("failed to unmarshal TON additional fields: %s", err) + typeErr = fmt.Sprintf("; additionally failed to unmarshal TON additional fields: %s", err) return "" } @@ -53,14 +53,11 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect return fullyQualifiedName }() - decodedOp, err := decoder.Decode(mcmsTx, fullyQualifiedName) - if err != nil { + 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 := "failed to decode TON transaction: " + err.Error() - if typeErr != "" { - errStr += "; additionally" + typeErr - } + errStr := "failed to decode TON transaction: " + errDec.Error() + typeErr return &DecodedCall{ Address: mcmsTx.To, diff --git a/experimental/analyzer/ton_analyzer_test.go b/experimental/analyzer/ton_analyzer_test.go index b4a9f1864..187835d5a 100644 --- a/experimental/analyzer/ton_analyzer_test.go +++ b/experimental/analyzer/ton_analyzer_test.go @@ -78,7 +78,7 @@ func TestAnalyzeTONTransaction(t *testing.T) { wantErrContain: "unknown contract interface: link.chain.ton.lib.access.RBAC@0.0.0", }, { - name: "success - RBAC GrantRol with version", + name: "success - GrantRole with version", decoder: ton.NewDecoder(tvm.ContractTLBRegistry{ bindings.TypeRBAC + "@1.2.3": bindings.Registry[bindings.TypeRBAC], }), From 5fa548076eefcd52caab295b3e65cba86e3bfb4b Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Tue, 19 May 2026 10:59:26 -0300 Subject: [PATCH 15/17] fix: lint --- experimental/analyzer/upf/upf_test.go | 62 --------------------------- 1 file changed, 62 deletions(-) diff --git a/experimental/analyzer/upf/upf_test.go b/experimental/analyzer/upf/upf_test.go index 8cc509a40..5aa1112ca 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" @@ -696,60 +688,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, - nil, - bindings.TypeRBAC, - []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() From dd9760cf97e49a4f1112bc95dc83b2d78a2b0f08 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Tue, 19 May 2026 11:04:49 -0300 Subject: [PATCH 16/17] fix: nolint --- experimental/analyzer/ton_analyzer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/experimental/analyzer/ton_analyzer.go b/experimental/analyzer/ton_analyzer.go index 89737eeee..32b71338d 100644 --- a/experimental/analyzer/ton_analyzer.go +++ b/experimental/analyzer/ton_analyzer.go @@ -59,6 +59,7 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect // Instead, put the error message in the Method field so it's visible in the report. 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, From c35fcdbe197eea36f309ac90c50be61a339589e9 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Tue, 19 May 2026 11:15:34 -0300 Subject: [PATCH 17/17] fix: duplicated entry --- experimental/analyzer/upf/upf_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/experimental/analyzer/upf/upf_test.go b/experimental/analyzer/upf/upf_test.go index 5aa1112ca..19ff1f0c3 100644 --- a/experimental/analyzer/upf/upf_test.go +++ b/experimental/analyzer/upf/upf_test.go @@ -246,7 +246,6 @@ var timelockProposalRMNCurse = `{ "10344971235874465080": "0x5f077BCeE6e285154473F65699d6F46Fd03D105A", "13264668187771770619": "0x804759c9bdd258A810987FDe21c9E24C5383b722", "16423721717087811551": "DoajfR5tK24xVw51fWcawUZWhAXD8yrBJVacc13neVQA.E4R6Nwg1K8Zvi6McLdkaGDD5ClX1KkyV", - "1399300952838017768": "EQADa3W6G0nSiTV4a6euRA42fU9QxSEnb-WeDpcrtWzA2jM8", "1399300952838017768": "UQDVnPvHy7luXVeJ3VmNXSQxhme9fORLj3vylRCg6b3thiIY" }, "operations": [