Skip to content

Commit d8a156c

Browse files
feat: proposal decoding interfaces + EVM implementation (#301)
This pull request introduces a new interface for decoding operations and implements it for the EVM decoder. The most important changes include the creation of a `DecodedOperation` interface, updates to the `Decoder` interface, and the implementation of these interfaces in the EVM decoder. ### Interface and Type Definitions: * Added `DecodedOperation` interface with methods `MethodName`, `Args`, and `String` in `sdk/decoder.go`. * Updated `Decoder` interface to use `DecodedOperation` as the return type of the `Decode` method in `sdk/decoder.go`. ### EVM Decoder Implementation: * Implemented `DecodedOperation` struct with methods `MethodName`, `Args`, and `String` in `sdk/evm/decoder.go`. * Implemented `Decoder` struct with the `Decode` method in `sdk/evm/decoder.go`. * Added `ParseFunctionCall` function to parse the ABI and transaction data into a `DecodedOperation` in `sdk/evm/decoder.go`. --------- Co-authored-by: Graham Goh <[email protected]>
1 parent e0efcd2 commit d8a156c

11 files changed

+587
-78
lines changed

.changeset/moody-llamas-serve.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smartcontractkit/mcms": minor
3+
---
4+
5+
Expose additional functions to help with proposal decoding

sdk/decoded_operation.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package sdk
2+
3+
type DecodedOperation interface {
4+
MethodName() string // MethodName returns the name of the method.
5+
Keys() []string // Keys returns the names of the input arguments.
6+
Args() []any // Args returns the values input arguments.
7+
8+
// String returns a human readable representation of the decoded operation.
9+
//
10+
// The first return value is the method name.
11+
// The second return value is a string representation of the input arguments.
12+
// The third return value is an error if there was an issue generating the string.
13+
String() (string, string, error)
14+
}

sdk/decoder.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@ import (
88
//
99
// This is only required if the chain supports decoding.
1010
type Decoder interface {
11-
Decode(op types.Operation, abi string) (methodName string, args string, err error)
11+
// Decode decodes the transaction data of a chain operation.
12+
//
13+
// contractInterfaces is the collection of functions and contract interfaces of the contract
14+
// that the operation is interacting with.
15+
Decode(op types.Transaction, contractInterfaces string) (DecodedOperation, error)
1216
}

sdk/evm/decoded_operation.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package evm
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/smartcontractkit/mcms/sdk"
8+
)
9+
10+
type DecodedOperation struct {
11+
FunctionName string
12+
InputArgs []any
13+
InputKeys []string
14+
}
15+
16+
var _ sdk.DecodedOperation = &DecodedOperation{}
17+
18+
func NewDecodedOperation(functionName string, inputKeys []string, inputArgs []any) (*DecodedOperation, error) {
19+
if len(inputKeys) != len(inputArgs) {
20+
return nil, fmt.Errorf("input keys and input args must have the same length")
21+
}
22+
23+
return &DecodedOperation{
24+
FunctionName: functionName,
25+
InputKeys: inputKeys,
26+
InputArgs: inputArgs,
27+
}, nil
28+
}
29+
30+
func (d *DecodedOperation) MethodName() string {
31+
return d.FunctionName
32+
}
33+
34+
func (d *DecodedOperation) Keys() []string {
35+
return d.InputKeys
36+
}
37+
38+
func (d *DecodedOperation) Args() []any {
39+
return d.InputArgs
40+
}
41+
42+
func (d *DecodedOperation) String() (string, string, error) {
43+
// Create a human readable representation of the decoded operation
44+
// by displaying a map of input keys to input values
45+
// e.g. {"key1": "value1", "key2": "value2"}
46+
inputMap := make(map[string]any)
47+
for i, key := range d.InputKeys {
48+
inputMap[key] = d.InputArgs[i]
49+
}
50+
51+
byteMap, err := json.MarshalIndent(inputMap, "", " ")
52+
if err != nil {
53+
return "", "", err
54+
}
55+
56+
return d.FunctionName, string(byteMap), nil
57+
}

sdk/evm/decoded_operation_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package evm
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestNewDecodedOperation(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
name string
16+
functionName string
17+
inputKeys []string
18+
inputArgs []any
19+
wantErr string
20+
}{
21+
{
22+
name: "success",
23+
functionName: "functionName",
24+
inputKeys: []string{"inputKey1", "inputKey2"},
25+
inputArgs: []any{"inputArg1", "inputArg2"},
26+
},
27+
{
28+
name: "success with empty input keys and args",
29+
functionName: "functionName",
30+
inputKeys: []string{},
31+
inputArgs: []any{},
32+
},
33+
{
34+
name: "error with mismatched input keys and args",
35+
functionName: "functionName",
36+
inputKeys: []string{"inputKey1", "inputKey2"},
37+
inputArgs: []any{"inputArg1"},
38+
wantErr: "input keys and input args must have the same length",
39+
},
40+
}
41+
42+
for _, tt := range tests {
43+
t.Run(tt.name, func(t *testing.T) {
44+
t.Parallel()
45+
46+
got, err := NewDecodedOperation(tt.functionName, tt.inputKeys, tt.inputArgs)
47+
if tt.wantErr != "" {
48+
require.Error(t, err)
49+
assert.EqualError(t, err, tt.wantErr)
50+
} else {
51+
require.NoError(t, err)
52+
assert.Equal(t, tt.functionName, got.FunctionName)
53+
assert.Equal(t, tt.inputKeys, got.InputKeys)
54+
assert.Equal(t, tt.inputArgs, got.InputArgs)
55+
56+
// Test member functions
57+
assert.Equal(t, tt.functionName, got.MethodName())
58+
assert.Equal(t, tt.inputKeys, got.Keys())
59+
assert.Equal(t, tt.inputArgs, got.Args())
60+
61+
// Test String()
62+
fn, inputs, err := got.String()
63+
require.NoError(t, err)
64+
assert.Equal(t, tt.functionName, fn)
65+
for i := range tt.inputKeys {
66+
assert.Contains(t, inputs, fmt.Sprintf(`"%s": "%v"`, tt.inputKeys[i], tt.inputArgs[i]))
67+
}
68+
}
69+
})
70+
}
71+
}

sdk/evm/decoder.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package evm
2+
3+
import (
4+
"strings"
5+
6+
geth_abi "github.com/ethereum/go-ethereum/accounts/abi"
7+
8+
"github.com/smartcontractkit/mcms/sdk"
9+
"github.com/smartcontractkit/mcms/types"
10+
)
11+
12+
type Decoder struct{}
13+
14+
var _ sdk.Decoder = &Decoder{}
15+
16+
func NewDecoder() *Decoder {
17+
return &Decoder{}
18+
}
19+
20+
func (d *Decoder) Decode(tx types.Transaction, contractInterfaces string) (sdk.DecodedOperation, error) {
21+
return ParseFunctionCall(contractInterfaces, tx.Data)
22+
}
23+
24+
// ParseFunctionCall parses a full data payload (with function selector at the front of it) and a full contract ABI
25+
// into a function name and an array of inputs.
26+
func ParseFunctionCall(fullAbi string, data []byte) (*DecodedOperation, error) {
27+
// Parse the ABI
28+
parsedAbi, err := geth_abi.JSON(strings.NewReader(fullAbi))
29+
if err != nil {
30+
return &DecodedOperation{}, err
31+
}
32+
33+
// Extract the method from the data
34+
method, err := parsedAbi.MethodById(data[:4])
35+
if err != nil {
36+
return &DecodedOperation{}, err
37+
}
38+
39+
// Unpack the data
40+
inputs, err := method.Inputs.UnpackValues(data[4:])
41+
if err != nil {
42+
return &DecodedOperation{}, err
43+
}
44+
45+
// Get the keys of the inputs
46+
methodKeys := make([]string, len(method.Inputs))
47+
for i, input := range method.Inputs {
48+
methodKeys[i] = input.Name
49+
}
50+
51+
return &DecodedOperation{
52+
FunctionName: method.Name,
53+
InputKeys: methodKeys,
54+
InputArgs: inputs,
55+
}, nil
56+
}

sdk/evm/decoder_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package evm
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/crypto"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/smartcontractkit/mcms/sdk/evm/bindings"
13+
"github.com/smartcontractkit/mcms/types"
14+
)
15+
16+
func TestDecoder(t *testing.T) {
17+
t.Parallel()
18+
19+
// Get ABI
20+
timelockAbi, err := bindings.RBACTimelockMetaData.GetAbi()
21+
require.NoError(t, err)
22+
exampleRole := crypto.Keccak256Hash([]byte("EXAMPLE_ROLE"))
23+
24+
// Grant role data
25+
grantRoleData, err := timelockAbi.Pack("grantRole", [32]byte(exampleRole), common.HexToAddress("0x123"))
26+
require.NoError(t, err)
27+
28+
tests := []struct {
29+
name string
30+
give types.Operation
31+
contractInterfaces string
32+
want *DecodedOperation
33+
wantErr string
34+
}{
35+
{
36+
name: "success",
37+
give: types.Operation{
38+
ChainSelector: 1,
39+
Transaction: NewTransaction(
40+
common.HexToAddress("0xTestTarget"),
41+
grantRoleData,
42+
big.NewInt(0),
43+
"RBACTimelock",
44+
[]string{"grantRole"},
45+
),
46+
},
47+
contractInterfaces: bindings.RBACTimelockABI,
48+
want: &DecodedOperation{
49+
FunctionName: "grantRole",
50+
InputKeys: []string{"role", "account"},
51+
InputArgs: []any{[32]byte(exampleRole.Bytes()), common.HexToAddress("0x0000000000000000000000000000000000000123")},
52+
},
53+
wantErr: "",
54+
},
55+
}
56+
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
t.Parallel()
60+
61+
d := NewDecoder()
62+
got, err := d.Decode(tt.give.Transaction, tt.contractInterfaces)
63+
if tt.wantErr != "" {
64+
require.Error(t, err)
65+
assert.EqualError(t, err, tt.wantErr)
66+
} else {
67+
require.NoError(t, err)
68+
assert.Equal(t, tt.want, got)
69+
}
70+
})
71+
}
72+
}

0 commit comments

Comments
 (0)