Skip to content

Commit f480193

Browse files
chore: Add ABI matching utility
1 parent db67941 commit f480193

File tree

2 files changed

+373
-0
lines changed

2 files changed

+373
-0
lines changed

op-chain-ops/script/abi.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package script
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/ethereum/go-ethereum/accounts/abi"
8+
)
9+
10+
// matchTypes is a runtime ABI type check utility that ensures that compile-time structs
11+
// match the ABI definition loaded from an artifact at runtime
12+
//
13+
// This verification is important since even with abigen-generated types the ABI can deviate
14+
// which would cause a lot of headache and e.g. partially successful deployments or configurations
15+
func matchTypes(abiType abi.Type, goType reflect.Type) error {
16+
// If the types are convertible, we're good
17+
if goType.AssignableTo(abiType.GetType()) {
18+
return nil
19+
}
20+
21+
// We check for arrays first (i.e. fixed length slices like uint256[2])
22+
if abiType.T == abi.ArrayTy {
23+
// First a basic check
24+
if goType.Kind() != reflect.Array {
25+
return abiTypeErr(abiType, goType)
26+
}
27+
28+
// Now make sure the lengths match
29+
if abiType.Size != goType.Len() {
30+
return fmt.Errorf("%w: expected an array of length %d, got length %d", abiTypeErr(abiType, goType), abiType.Size, goType.Len())
31+
}
32+
33+
// Finally we check the element types
34+
err := matchTypes(*abiType.Elem, goType.Elem())
35+
if err != nil {
36+
return fmt.Errorf("%w: %w", abiTypeErr(abiType, goType), err)
37+
}
38+
39+
// If all the checks above succeeded, it means the array is safe to be used
40+
return nil
41+
}
42+
43+
// Now we check for slice type (i.e. variable length slices like uint256[])
44+
if abiType.T == abi.SliceTy {
45+
// First a basic check
46+
if goType.Kind() != reflect.Slice {
47+
return abiTypeErr(abiType, goType)
48+
}
49+
50+
// Then check the element types
51+
err := matchTypes(*abiType.Elem, goType.Elem())
52+
if err != nil {
53+
return fmt.Errorf("%w: %w", abiTypeErr(abiType, goType), err)
54+
}
55+
56+
// If all the checks above succeeded, it means the slice is safe to be used
57+
return nil
58+
}
59+
60+
// Finally the most complex ones, tuples
61+
if abiType.T == abi.TupleTy {
62+
// First a basic check
63+
if goType.Kind() != reflect.Struct {
64+
return abiTypeErr(abiType, goType)
65+
}
66+
67+
// Then we compare the number of fields
68+
numAbiFields := abiType.TupleType.NumField()
69+
numGoFields := goType.NumField()
70+
if numAbiFields != numGoFields {
71+
return fmt.Errorf("%w: the number of struct fields doesn't match: ABI type has %d, Go type has %d", abiTypeErr(abiType, goType), numAbiFields, numGoFields)
72+
}
73+
74+
// And finally we check each field
75+
for index := range numAbiFields {
76+
field := abiType.TupleType.Field(index)
77+
goField := goType.Field(index)
78+
79+
// First we make sure that the names are sorted in the correct order
80+
//
81+
// This is important since ABI encoding and decoding specifically has issues
82+
// with misordered fields and can place values in wrong places
83+
if field.Name != goField.Name {
84+
return fmt.Errorf("%w: ABI field name %s at index %d does not match Go field name %s. Please make sure to match the Go structs with Solidity structs", abiTypeErr(abiType, goType), field.Name, index, goField.Name)
85+
}
86+
87+
// Now we ensure that the types match
88+
err := matchTypes(*abiType.TupleElems[index], goField.Type)
89+
if err != nil {
90+
return fmt.Errorf("%w: ABI field %s does not match Go field %s: %w", abiTypeErr(abiType, goType), field.Name, goField.Name, err)
91+
}
92+
}
93+
94+
// If all the checks above succeeded, it means the tuple is safe to be used
95+
return nil
96+
}
97+
98+
// We'll return a default error
99+
return abiTypeErr(abiType, goType)
100+
}
101+
102+
// matchArguments ensures that an argument list (e.g. function argument or return values)
103+
// match the provided Go types
104+
func matchArguments(args abi.Arguments, goTypes ...reflect.Type) error {
105+
// First we make sure that the argument lengths match
106+
numAbiArgs := len(args)
107+
numGoTypes := len(goTypes)
108+
if numAbiArgs != numGoTypes {
109+
return fmt.Errorf("ABI arguments don't match Go types: ABI has %d arguments, Go has %d", numAbiArgs, numGoTypes)
110+
}
111+
112+
for index, abiArg := range args {
113+
goType := goTypes[index]
114+
115+
err := matchTypes(abiArg.Type, goType)
116+
if err != nil {
117+
return fmt.Errorf("ABI argument %s at index %d doesn't match Go type: %w", abiArg.Name, index, err)
118+
}
119+
}
120+
121+
return nil
122+
}
123+
124+
func abiTypeErr(abiType abi.Type, goType reflect.Type) error {
125+
return fmt.Errorf("ABI type %s (represented by %s) is not assignable to Go type %s", abiType, abiType.GetType(), goType)
126+
}

op-chain-ops/script/abi_test.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package script
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"math/big"
7+
"reflect"
8+
"testing"
9+
10+
"github.com/ethereum/go-ethereum/accounts/abi"
11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func die[O any](value O, err error) O {
16+
if err != nil {
17+
panic(err)
18+
}
19+
20+
return value
21+
}
22+
23+
func TestMatchTypes(t *testing.T) {
24+
type matchTypesTest struct {
25+
abiType abi.Type
26+
goType reflect.Type
27+
err string
28+
}
29+
30+
type StructWithPrimitiveFields struct {
31+
AddressField common.Address
32+
BoolField bool
33+
UintField *big.Int
34+
}
35+
36+
type StructWithPrimitiveFieldsWrapper struct {
37+
Nested StructWithPrimitiveFields
38+
}
39+
40+
structWithPrimitiveFieldsMarshalling := []abi.ArgumentMarshaling{{Name: "addressField", Type: "address"}, {Name: "boolField", Type: "bool"}, {Name: "uintField", Type: "uint256"}}
41+
42+
matchTypesTests := []matchTypesTest{
43+
{
44+
abiType: die(abi.NewType("uint256", "", []abi.ArgumentMarshaling{})),
45+
goType: reflect.TypeOf(new(big.Int)),
46+
err: ``,
47+
},
48+
{
49+
abiType: die(abi.NewType("uint128", "", []abi.ArgumentMarshaling{})),
50+
goType: reflect.TypeOf(new(big.Int)),
51+
err: ``,
52+
},
53+
{
54+
abiType: die(abi.NewType("uint64", "", []abi.ArgumentMarshaling{})),
55+
goType: reflect.TypeOf(*new(uint64)),
56+
err: ``,
57+
},
58+
{
59+
abiType: die(abi.NewType("uint8", "", []abi.ArgumentMarshaling{})),
60+
goType: reflect.TypeOf(*new(uint8)),
61+
err: ``,
62+
},
63+
{
64+
abiType: die(abi.NewType("string", "", []abi.ArgumentMarshaling{})),
65+
goType: reflect.TypeOf(*new(string)),
66+
err: ``,
67+
},
68+
{
69+
abiType: die(abi.NewType("bool", "", []abi.ArgumentMarshaling{})),
70+
goType: reflect.TypeOf(*new(bool)),
71+
err: ``,
72+
},
73+
{
74+
abiType: die(abi.NewType("bytes", "", []abi.ArgumentMarshaling{})),
75+
goType: reflect.TypeOf(*new([]byte)),
76+
err: ``,
77+
},
78+
{
79+
abiType: die(abi.NewType("bytes", "", []abi.ArgumentMarshaling{})),
80+
goType: reflect.TypeOf(*new([32]byte)),
81+
err: `ABI type bytes (represented by []uint8) is not assignable to Go type [32]uint8`,
82+
},
83+
{
84+
abiType: die(abi.NewType("bytes32", "", []abi.ArgumentMarshaling{})),
85+
goType: reflect.TypeOf(*new([32]byte)),
86+
err: ``,
87+
},
88+
{
89+
abiType: die(abi.NewType("bytes32", "", []abi.ArgumentMarshaling{})),
90+
goType: reflect.TypeOf(*new([]byte)),
91+
err: `ABI type bytes32 (represented by [32]uint8) is not assignable to Go type []uint8`,
92+
},
93+
{
94+
abiType: die(abi.NewType("bytes32", "", []abi.ArgumentMarshaling{})),
95+
goType: reflect.TypeOf(*new([64]byte)),
96+
err: `ABI type bytes32 (represented by [32]uint8) is not assignable to Go type [64]uint8`,
97+
},
98+
{
99+
abiType: die(abi.NewType("address", "", []abi.ArgumentMarshaling{})),
100+
goType: reflect.TypeOf(*new(common.Address)),
101+
err: ``,
102+
},
103+
{
104+
abiType: die(abi.NewType("address", "", []abi.ArgumentMarshaling{})),
105+
goType: reflect.TypeOf(*new([]byte)),
106+
err: `ABI type address (represented by common.Address) is not assignable to Go type []uint8`,
107+
},
108+
{
109+
abiType: die(abi.NewType("tuple", "", []abi.ArgumentMarshaling{})),
110+
goType: reflect.TypeOf(*new(struct{})),
111+
err: ``,
112+
},
113+
{
114+
abiType: die(abi.NewType("tuple", "", structWithPrimitiveFieldsMarshalling)),
115+
goType: reflect.TypeOf(*new(StructWithPrimitiveFields)),
116+
err: ``,
117+
},
118+
{
119+
abiType: die(abi.NewType("tuple", "", []abi.ArgumentMarshaling{{Name: "boolField", Type: "bool"}, {Name: "addressField", Type: "address"}, {Name: "uintField", Type: "uint256"}})),
120+
goType: reflect.TypeOf(*new(StructWithPrimitiveFields)),
121+
err: `ABI type (bool,address,uint256) (represented by struct { BoolField bool "json:\"boolField\""; AddressField common.Address "json:\"addressField\""; UintField *big.Int "json:\"uintField\"" }) is not assignable to Go type script.StructWithPrimitiveFields: ABI field name BoolField at index 0 does not match Go field name AddressField. Please make sure to match the Go structs with Solidity structs`,
122+
},
123+
{
124+
abiType: die(abi.NewType("tuple", "", []abi.ArgumentMarshaling{{Name: "addressField", Type: "bool"}, {Name: "boolField", Type: "bool"}, {Name: "uintField", Type: "uint256"}})),
125+
goType: reflect.TypeOf(*new(StructWithPrimitiveFields)),
126+
err: `ABI type (bool,bool,uint256) (represented by struct { AddressField bool "json:\"addressField\""; BoolField bool "json:\"boolField\""; UintField *big.Int "json:\"uintField\"" }) is not assignable to Go type script.StructWithPrimitiveFields: ABI field AddressField does not match Go field AddressField: ABI type bool (represented by bool) is not assignable to Go type common.Address`,
127+
},
128+
{
129+
abiType: die(abi.NewType("tuple", "", []abi.ArgumentMarshaling{{Name: "nested", Type: "tuple", Components: structWithPrimitiveFieldsMarshalling}})),
130+
goType: reflect.TypeOf(*new(StructWithPrimitiveFieldsWrapper)),
131+
err: ``,
132+
},
133+
{
134+
abiType: die(abi.NewType("tuple", "", []abi.ArgumentMarshaling{{Name: "nested", Type: "tuple", Components: []abi.ArgumentMarshaling{{Name: "addressField", Type: "bool"}, {Name: "boolField", Type: "bool"}, {Name: "uintField", Type: "uint256"}}}})),
135+
goType: reflect.TypeOf(*new(StructWithPrimitiveFieldsWrapper)),
136+
err: `ABI type ((bool,bool,uint256)) (represented by struct { Nested struct { AddressField bool "json:\"addressField\""; BoolField bool "json:\"boolField\""; UintField *big.Int "json:\"uintField\"" } "json:\"nested\"" }) is not assignable to Go type script.StructWithPrimitiveFieldsWrapper: ABI field Nested does not match Go field Nested: ABI type (bool,bool,uint256) (represented by struct { AddressField bool "json:\"addressField\""; BoolField bool "json:\"boolField\""; UintField *big.Int "json:\"uintField\"" }) is not assignable to Go type script.StructWithPrimitiveFields: ABI field AddressField does not match Go field AddressField: ABI type bool (represented by bool) is not assignable to Go type common.Address`,
137+
},
138+
{
139+
abiType: die(abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{})),
140+
goType: reflect.TypeOf(*new([]struct{})),
141+
err: ``,
142+
},
143+
{
144+
abiType: die(abi.NewType("tuple[]", "", structWithPrimitiveFieldsMarshalling)),
145+
goType: reflect.TypeOf(*new([]StructWithPrimitiveFields)),
146+
err: ``,
147+
},
148+
{
149+
abiType: die(abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{{Name: "boolField", Type: "bool"}, {Name: "addressField", Type: "address"}, {Name: "uintField", Type: "uint256"}})),
150+
goType: reflect.TypeOf(*new([]StructWithPrimitiveFields)),
151+
err: `ABI type (bool,address,uint256)[] (represented by []struct { BoolField bool "json:\"boolField\""; AddressField common.Address "json:\"addressField\""; UintField *big.Int "json:\"uintField\"" }) is not assignable to Go type []script.StructWithPrimitiveFields: ABI type (bool,address,uint256) (represented by struct { BoolField bool "json:\"boolField\""; AddressField common.Address "json:\"addressField\""; UintField *big.Int "json:\"uintField\"" }) is not assignable to Go type script.StructWithPrimitiveFields: ABI field name BoolField at index 0 does not match Go field name AddressField. Please make sure to match the Go structs with Solidity structs`,
152+
},
153+
{
154+
abiType: die(abi.NewType("tuple[2]", "", structWithPrimitiveFieldsMarshalling)),
155+
goType: reflect.TypeOf(*new([3]StructWithPrimitiveFields)),
156+
err: `ABI type (address,bool,uint256)[2] (represented by [2]struct { AddressField common.Address "json:\"addressField\""; BoolField bool "json:\"boolField\""; UintField *big.Int "json:\"uintField\"" }) is not assignable to Go type [3]script.StructWithPrimitiveFields: expected an array of length 2, got length 3`,
157+
},
158+
}
159+
160+
for _, test := range matchTypesTests {
161+
t.Run(fmt.Sprintf("%s <-> %s", test.abiType, test.goType), func(t *testing.T) {
162+
err := matchTypes(test.abiType, test.goType)
163+
164+
if test.err == "" {
165+
require.NoError(t, err)
166+
} else {
167+
require.EqualError(t, err, test.err)
168+
}
169+
})
170+
}
171+
}
172+
173+
func TestMatchArguments(t *testing.T) {
174+
type matchArgumentsTest struct {
175+
abiArguments abi.Arguments
176+
goTypes []reflect.Type
177+
err string
178+
}
179+
180+
matchArgumentsTests := []matchArgumentsTest{
181+
{
182+
abiArguments: abi.Arguments{
183+
{
184+
Name: "",
185+
Type: die(abi.NewType("uint256", "", []abi.ArgumentMarshaling{})),
186+
},
187+
},
188+
goTypes: []reflect.Type{reflect.TypeOf(new(big.Int))},
189+
},
190+
{
191+
abiArguments: abi.Arguments{
192+
{
193+
Name: "",
194+
Type: die(abi.NewType("uint256[]", "", []abi.ArgumentMarshaling{})),
195+
},
196+
},
197+
goTypes: []reflect.Type{reflect.TypeOf(*new([]*big.Int))},
198+
},
199+
{
200+
abiArguments: abi.Arguments{
201+
{
202+
Name: "",
203+
Type: die(abi.NewType("uint256[]", "", []abi.ArgumentMarshaling{})),
204+
},
205+
},
206+
goTypes: []reflect.Type{},
207+
err: `ABI arguments don't match Go types: ABI has 1 arguments, Go has 0`,
208+
},
209+
{
210+
abiArguments: abi.Arguments{},
211+
goTypes: []reflect.Type{reflect.TypeOf(*new([]*big.Int))},
212+
err: `ABI arguments don't match Go types: ABI has 0 arguments, Go has 1`,
213+
},
214+
{
215+
abiArguments: abi.Arguments{
216+
{
217+
Name: "",
218+
Type: die(abi.NewType("uint256[2]", "", []abi.ArgumentMarshaling{})),
219+
},
220+
},
221+
goTypes: []reflect.Type{reflect.TypeOf(*new([]*big.Int))},
222+
err: `ABI argument at index 0 doesn't match Go type: ABI type uint256[2] (represented by [2]*big.Int) is not assignable to Go type []*big.Int`,
223+
},
224+
{
225+
abiArguments: abi.Arguments{
226+
{
227+
Name: "",
228+
Type: die(abi.NewType("tuple", "", []abi.ArgumentMarshaling{{Name: "field", Type: "address"}})),
229+
},
230+
},
231+
goTypes: []reflect.Type{reflect.TypeOf(*new(struct{ Field *big.Int }))},
232+
err: `ABI argument at index 0 doesn't match Go type: ABI type (address) (represented by struct { Field common.Address "json:\"field\"" }) is not assignable to Go type struct { Field *big.Int }: ABI field Field does not match Go field Field: ABI type address (represented by common.Address) is not assignable to Go type *big.Int`,
233+
},
234+
}
235+
236+
for _, test := range matchArgumentsTests {
237+
t.Run(fmt.Sprintf("%v <-> %v", test.abiArguments, test.goTypes), func(t *testing.T) {
238+
err := matchArguments(test.abiArguments, test.goTypes...)
239+
240+
if test.err == "" {
241+
require.NoError(t, err)
242+
} else {
243+
require.EqualError(t, err, test.err)
244+
}
245+
})
246+
}
247+
}

0 commit comments

Comments
 (0)