diff --git a/fvm/evm/impl/abi.go b/fvm/evm/impl/abi.go index f199255a714..1d034f84f1a 100644 --- a/fvm/evm/impl/abi.go +++ b/fvm/evm/impl/abi.go @@ -516,6 +516,7 @@ func gethABIType( } func goType( + context abiEncodingContext, staticType interpreter.StaticType, evmTypeIDs *evmSpecialTypeIDs, ) (reflect.Type, bool) { @@ -558,7 +559,7 @@ func goType( switch staticType := staticType.(type) { case *interpreter.ConstantSizedStaticType: - elementType, ok := goType(staticType.ElementType(), evmTypeIDs) + elementType, ok := goType(context, staticType.ElementType(), evmTypeIDs) if !ok { break } @@ -566,7 +567,7 @@ func goType( return reflect.ArrayOf(int(staticType.Size), elementType), true case *interpreter.VariableSizedStaticType: - elementType, ok := goType(staticType.ElementType(), evmTypeIDs) + elementType, ok := goType(context, staticType.ElementType(), evmTypeIDs) if !ok { break } @@ -585,6 +586,22 @@ func goType( return reflect.ArrayOf(stdlib.EVMBytes32Length, reflect.TypeOf(byte(0))), true } + gethABIType, ok := gethABIType( + context, + staticType, + evmTypeIDs, + ) + // All user-defined Cadence structs, are ABI encoded/decoded as Solidity tuples. + // Except for the structs defined in the EVM system contract: + // - `EVM.EVMAddress` + // - `EVM.EVMBytes` + // - `EVM.EVMBytes4` + // - `EVM.EVMBytes32` + // These have their own ABI encoding/decoding format. + if ok && gethABIType.T == gethABI.TupleTy { + return gethABIType.TupleType, true + } + return nil, false } @@ -793,7 +810,7 @@ func encodeABI( elementStaticType := arrayStaticType.ElementType() - elementGoType, ok := goType(elementStaticType, evmTypeIDs) + elementGoType, ok := goType(context, elementStaticType, evmTypeIDs) if !ok { break } @@ -810,6 +827,9 @@ func encodeABI( result = reflect.MakeSlice(reflect.SliceOf(elementGoType), size, size) } + semaType := interpreter.MustConvertStaticToSemaType(elementStaticType, context) + isTuple := asTupleEncodableCompositeType(semaType) != nil + var index int value.Iterate( context, @@ -825,7 +845,16 @@ func encodeABI( panic(err) } - result.Index(index).Set(reflect.ValueOf(arrayElement)) + if isTuple { + // For tuples, the underlying `arrayElement` is a value of + // type *struct { X,Y,Z fields }, so we need to indirect + // the pointer + result.Index(index).Set( + reflect.Indirect(reflect.ValueOf(arrayElement)), + ) + } else { + result.Index(index).Set(reflect.ValueOf(arrayElement)) + } index++ diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index 19329f53d60..6e03e8a2940 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -1289,6 +1289,74 @@ func TestEVMEncodeABIBytesRoundtrip(t *testing.T) { assert.Equal(t, uint64(64), gauge.TotalComputationUsed()) }) + + t.Run("ABI encode array of structs into tuple Solidity type", func(t *testing.T) { + script := []byte(` + import EVM from 0x1 + + access(all) + struct S { + access(all) let x: UInt8 + access(all) let y: Int16 + + init(x: UInt8, y: Int16) { + self.x = x + self.y = y + } + + access(all) fun toString(): String { + return "S(x: \(self.x), y: \(self.y))" + } + } + + access(all) + fun main() { + let s1 = S(x: 4, y: 2) + let s2 = S(x: 5, y: 9) + let structArray = [s1, s2] + let encodedData = EVM.encodeABI([structArray]) + assert(encodedData == [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x20, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x4, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x5, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x9 + ], message: String.encodeHex(encodedData)) + + let values = EVM.decodeABI(types: [Type<[S]>()], data: encodedData) + assert(values.length == 1) + let decodedStructArray = values[0] as! [S] + assert(decodedStructArray.length == 2) + + assert(decodedStructArray[0].x == 4) + assert(decodedStructArray[0].y == 2) + assert(decodedStructArray[1].x == 5) + assert(decodedStructArray[1].y == 9) + } + `) + + gauge := meter.NewMeter(meter.DefaultParameters().WithComputationWeights(meter.ExecutionEffortWeights{ + environment.ComputationKindEVMEncodeABI: 1 << meter.MeterExecutionInternalPrecisionBytes, + })) + + // Run script + _, err := rt.ExecuteScript( + runtime.Script{ + Source: script, + }, + runtime.Context{ + Interface: runtimeInterface, + Environment: scriptEnvironment, + Location: nextScriptLocation(), + MemoryGauge: gauge, + ComputationGauge: gauge, + }, + ) + require.NoError(t, err) + + assert.Equal(t, uint64(192), gauge.TotalComputationUsed()) + }) } func TestEVMEncodeABIComputation(t *testing.T) {