Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


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

Use Metadata.ContractVersion instead of ContractTypeAndVersion
5 changes: 0 additions & 5 deletions engine/cld/commands/mcms/testdata/proposal.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,34 @@
"data": "ebpQlw==",
"value": 0,
"contractType": "",
"contractTypeAndVersion": "",
"tags": null
},
{
"to": "0x53850c7ee54fce09174f21f6c4b5602e4ca46353",
"data": "ebpQlw==",
"value": 0,
"contractType": "",
"contractTypeAndVersion": "",
"tags": null
},
{
"to": "0x7ae20d26f2969ae44054a02e1acf19c3eecff148",
"data": "ebpQlw==",
"value": 0,
"contractType": "",
"contractTypeAndVersion": "",
"tags": null
},
{
"to": "0xed54914840214c01c06de50737ae9d9fe184cc26",
"data": "ebpQlw==",
"value": 0,
"contractType": "",
"contractTypeAndVersion": "",
"tags": null
},
{
"to": "0xfa2a8aa2982999f3df3b66405886c4007d6d8418",
"data": "ebpQlw==",
"value": 0,
"contractType": "",
"contractTypeAndVersion": "",
"tags": null
}
]
Expand Down
2 changes: 1 addition & 1 deletion engine/test/internal/mcmsutils/encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
14 changes: 13 additions & 1 deletion engine/test/internal/mcmsutils/testdata/proposal.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
}
},
"operations": [
{
"chainSelector": 3379446385462418246,
"transaction": {
"to": "0x123",
"additionalFields": {
"value": 0
},
"data": "",
"tags": null,
"contractType": ""
}
},
{
"chainSelector": 3379446385462418246,
"transaction": {
Expand All @@ -23,7 +35,7 @@
"data": "",
"tags": null,
"contractType": "",
"contractTypeAndVersion": ""
"contractVersion": "1.2.3"
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@
"transactions": [
{
"to": "0x0000000000000000000000000000000000000000",
"additionalFields": {"value": 0},
"additionalFields": {
"value": 0
},
"data": "ZGF0YQ==",
"contractType": "",
"contractTypeAndVersion": "",
"tags": null
}
]
}
]
}
}
19 changes: 12 additions & 7 deletions experimental/analyzer/decoded_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,35 @@ 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)
if ct == "" {
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() != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it should be the opposite: the datastore value should have precedence over the proposal metadata.

@ecPablo @ChrisAmora , what are your thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, it's more likely that datastore has more up-to-date data. For example, I generate proposalA that uses contractVersion 1.0.0, then very shortly after, someone updates datastore to contractVersion 2.0.0. I'd expect the decoded version to show me the 2.0.0 as that's the current on chain state. The only reasons where this could not be true is if we have out-of-cld systems updating the contracts, in that case maybe the proposal might be safer source of truth. But I don't think that's the case here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ok, let's keep system context (DS) to have priority. Override can be made with updating the DS directly with the proposal PR (if something is off).

Finally, we should also be able to interact with ad-hoc contracts which are not stored, and for that cases the proposal input ref (type, version) should resolve properly.

contractVersion = tv.Version.String()
}

return ct, cv
return ct, contractVersion
}
51 changes: 31 additions & 20 deletions experimental/analyzer/ton_analyzer.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -37,29 +36,41 @@ 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
}
decodedOp, err := decoder.Decode(mcmsTx, contractTypeAndVersion.Type.String())
var typeErr string
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)
return ""
Comment thread
patricios-space marked this conversation as resolved.
}

fullyQualifiedName := string(additionalFields.ContractTypeFull)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Let's extract into a type/func which we document and allows anybody to produce a FQN

// 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.
Comment thread
patricios-space marked this conversation as resolved.
Comment thread
patricios-space marked this conversation as resolved.
if mcmsTx.ContractVersion != nil {
Comment thread
patricios-space marked this conversation as resolved.
fullyQualifiedName += "@" + contractVersion
}
Comment on lines +45 to +51

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We don't want this in TON because we only want to select a version explicitly via TX params


return fullyQualifiedName
}()

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 := "failed to decode TON transaction: " + err.Error()
if typeErr != "" {
errStr += "; additionally" + typeErr
Comment thread
patricios-space marked this conversation as resolved.
Outdated
}

return &DecodedCall{
Address: mcmsTx.To,
Method: errStr.Error(),
ContractType: contractTypeAndVersion.Type.String(),
ContractVersion: contractTypeAndVersion.Version.String(),
Method: errStr,
ContractType: contractType,
ContractVersion: contractVersion,
}, nil
}

Expand All @@ -73,7 +84,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
}
87 changes: 70 additions & 17 deletions experimental/analyzer/ton_analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ 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"
"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"
Expand Down Expand Up @@ -49,25 +51,59 @@ 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: "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 {
Expand Down Expand Up @@ -173,7 +209,7 @@ func TestAnalyzeTONTransactions(t *testing.T) {
continue
}

require.Equal(t, tt.want[i], result, "call %d", i)
require.Equal(t, tt.want[i], result)
}
})
}
Expand All @@ -187,7 +223,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{
Expand All @@ -200,8 +236,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)
})
}

Expand Down Expand Up @@ -232,7 +285,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)
Expand All @@ -242,10 +296,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)",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: this DecodedCall should also contain a contract version, maybe as part of the Method by using a FQN vs ContractTypeFull (missing version)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was already done but it was not covered by test. Added a case to confirm this

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)},
Expand All @@ -259,11 +312,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 + `"}`),
}
}
Loading
Loading