diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index 5b902379f42..e8946f214cf 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -157,4 +157,36 @@ contract EVM { fun decodeABI(types: [Type], data: [UInt8]): [AnyStruct] { return InternalEVM.decodeABI(types: types, data: data) } + + access(all) + fun encodeABIWithSignature( + _ signature: String, + _ values: [AnyStruct] + ): [UInt8] { + let methodID = HashAlgorithm.KECCAK_256.hash( + signature.utf8 + ).slice(from: 0, upTo: 4) + let arguments = InternalEVM.encodeABI(values) + + return methodID.concat(arguments) + } + + access(all) + fun decodeABIWithSignature( + _ signature: String, + types: [Type], + data: [UInt8] + ): [AnyStruct] { + let methodID = HashAlgorithm.KECCAK_256.hash( + signature.utf8 + ).slice(from: 0, upTo: 4) + + for byte in methodID { + if byte != data.removeFirst() { + panic("signature mismatch") + } + } + + return InternalEVM.decodeABI(types: types, data: data) + } } diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index a10f90913de..54a68488b6d 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "testing" + "github.com/ethereum/go-ethereum/crypto" "github.com/onflow/cadence" "github.com/onflow/cadence/encoding/json" "github.com/onflow/cadence/runtime" @@ -2130,6 +2131,374 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { }) } +func TestEVMEncodeABIWithSignature(t *testing.T) { + + t.Parallel() + + handler := &testContractHandler{} + + contractsAddress := flow.BytesToAddress([]byte{0x1}) + + transactionEnvironment := newEVMTransactionEnvironment(handler, contractsAddress) + scriptEnvironment := newEVMScriptEnvironment(handler, contractsAddress) + + rt := runtime.NewInterpreterRuntime(runtime.Config{}) + + script := []byte(` + import EVM from 0x1 + + access(all) + fun main(): [UInt8] { + // bytes for address 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71 + let address = EVM.EVMAddress( + bytes: [ + 122, 88, 192, 190, 114, 190, 33, 139, 65, 198, + 8, 183, 254, 124, 91, 182, 48, 115, 108, 113 + ] + ) + + return EVM.encodeABIWithSignature( + "withdraw(address,uint256)", + [address, UInt256(250)] + ) + } + `) + + accountCodes := map[common.Location][]byte{} + var events []cadence.Event + + computation := uint(0) + runtimeInterface := &TestRuntimeInterface{ + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]runtime.Address, error) { + return []runtime.Address{runtime.Address(contractsAddress)}, nil + }, + OnResolveLocation: SingleIdentifierLocationResolver(t), + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + code = accountCodes[location] + return code, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (cadence.Value, error) { + return json.Decode(nil, b) + }, + OnMeterComputation: func(compKind common.ComputationKind, intensity uint) error { + if compKind == environment.ComputationKindEVMEncodeABI { + computation += intensity + } + return nil + }, + OnHash: func( + data []byte, + tag string, + hashAlgorithm runtime.HashAlgorithm, + ) ([]byte, error) { + return crypto.Keccak256(data), nil + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + nextScriptLocation := NewScriptLocationGenerator() + + // Deploy contracts + + deployContracts( + t, + rt, + contractsAddress, + runtimeInterface, + transactionEnvironment, + nextTransactionLocation, + ) + + // Run script + + result, err := rt.ExecuteScript( + runtime.Script{ + Source: script, + Arguments: [][]byte{}, + }, + runtime.Context{ + Interface: runtimeInterface, + Environment: scriptEnvironment, + Location: nextScriptLocation(), + }, + ) + require.NoError(t, err) + + abiBytes := []byte{ + 0xf3, 0xfe, 0xf3, 0xa3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x7a, 0x58, 0xc0, 0xbe, 0x72, 0xbe, 0x21, 0x8b, 0x41, + 0xc6, 0x8, 0xb7, 0xfe, 0x7c, 0x5b, 0xb6, 0x30, 0x73, 0x6c, 0x71, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0xfa, + } + cdcBytes := make([]cadence.Value, 0) + for _, bt := range abiBytes { + cdcBytes = append(cdcBytes, cadence.UInt8(bt)) + } + encodedABI := cadence.NewArray( + cdcBytes, + ).WithType(cadence.NewVariableSizedArrayType(cadence.TheUInt8Type)) + + assert.Equal(t, + encodedABI, + result, + ) + // The method ID is a byte array of length 4 + assert.Equal(t, computation+4, uint(len(cdcBytes))) +} + +func TestEVMDecodeABIWithSignature(t *testing.T) { + + t.Parallel() + + handler := &testContractHandler{} + + contractsAddress := flow.BytesToAddress([]byte{0x1}) + + transactionEnvironment := newEVMTransactionEnvironment(handler, contractsAddress) + scriptEnvironment := newEVMScriptEnvironment(handler, contractsAddress) + + rt := runtime.NewInterpreterRuntime(runtime.Config{}) + + script := []byte(` + import EVM from 0x1 + + access(all) + fun main(data: [UInt8]): Bool { + let values = EVM.decodeABIWithSignature( + "withdraw(address,uint256)", + types: [Type(), Type()], + data: data + ) + + // bytes for address 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71 + let address = EVM.EVMAddress( + bytes: [ + 122, 88, 192, 190, 114, 190, 33, 139, 65, 198, + 8, 183, 254, 124, 91, 182, 48, 115, 108, 113 + ] + ) + + assert(values.length == 2) + assert((values[0] as! EVM.EVMAddress).bytes == address.bytes) + assert((values[1] as! UInt256) == UInt256(250)) + + return true + } + `) + + accountCodes := map[common.Location][]byte{} + var events []cadence.Event + + computation := uint(0) + runtimeInterface := &TestRuntimeInterface{ + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]runtime.Address, error) { + return []runtime.Address{runtime.Address(contractsAddress)}, nil + }, + OnResolveLocation: SingleIdentifierLocationResolver(t), + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + code = accountCodes[location] + return code, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (cadence.Value, error) { + return json.Decode(nil, b) + }, + OnMeterComputation: func(compKind common.ComputationKind, intensity uint) error { + if compKind == environment.ComputationKindEVMDecodeABI { + computation += intensity + } + return nil + }, + OnHash: func( + data []byte, + tag string, + hashAlgorithm runtime.HashAlgorithm, + ) ([]byte, error) { + return crypto.Keccak256(data), nil + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + nextScriptLocation := NewScriptLocationGenerator() + + // Deploy contracts + + deployContracts( + t, + rt, + contractsAddress, + runtimeInterface, + transactionEnvironment, + nextTransactionLocation, + ) + + // Run script + abiBytes := []byte{ + 0xf3, 0xfe, 0xf3, 0xa3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x7a, 0x58, 0xc0, 0xbe, 0x72, 0xbe, 0x21, 0x8b, 0x41, + 0xc6, 0x8, 0xb7, 0xfe, 0x7c, 0x5b, 0xb6, 0x30, 0x73, 0x6c, 0x71, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0xfa, + } + cdcBytes := make([]cadence.Value, 0) + for _, bt := range abiBytes { + cdcBytes = append(cdcBytes, cadence.UInt8(bt)) + } + encodedABI := cadence.NewArray( + cdcBytes, + ).WithType(cadence.NewVariableSizedArrayType(cadence.TheUInt8Type)) + + result, err := rt.ExecuteScript( + runtime.Script{ + Source: script, + Arguments: EncodeArgs([]cadence.Value{ + encodedABI, + }), + }, + runtime.Context{ + Interface: runtimeInterface, + Environment: scriptEnvironment, + Location: nextScriptLocation(), + }, + ) + require.NoError(t, err) + + assert.Equal(t, cadence.NewBool(true), result) + // The method ID is a byte array of length 4 + assert.Equal(t, computation+4, uint(len(cdcBytes))) +} + +func TestEVMDecodeABIWithSignatureMismatch(t *testing.T) { + + t.Parallel() + + handler := &testContractHandler{} + + contractsAddress := flow.BytesToAddress([]byte{0x1}) + + transactionEnvironment := newEVMTransactionEnvironment(handler, contractsAddress) + scriptEnvironment := newEVMScriptEnvironment(handler, contractsAddress) + + rt := runtime.NewInterpreterRuntime(runtime.Config{}) + + script := []byte(` + import EVM from 0x1 + + access(all) + fun main(data: [UInt8]): Bool { + // The data was encoded for the function "withdraw(address,uint256)", + // but we pass a different function signature + let values = EVM.decodeABIWithSignature( + "deposit(uint256, address)", + types: [Type(), Type()], + data: data + ) + + return true + } + `) + + accountCodes := map[common.Location][]byte{} + var events []cadence.Event + + runtimeInterface := &TestRuntimeInterface{ + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]runtime.Address, error) { + return []runtime.Address{runtime.Address(contractsAddress)}, nil + }, + OnResolveLocation: SingleIdentifierLocationResolver(t), + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + code = accountCodes[location] + return code, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (cadence.Value, error) { + return json.Decode(nil, b) + }, + OnHash: func( + data []byte, + tag string, + hashAlgorithm runtime.HashAlgorithm, + ) ([]byte, error) { + return crypto.Keccak256(data), nil + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + nextScriptLocation := NewScriptLocationGenerator() + + // Deploy contracts + + deployContracts( + t, + rt, + contractsAddress, + runtimeInterface, + transactionEnvironment, + nextTransactionLocation, + ) + + // Run script + abiBytes := []byte{ + 0xf3, 0xfe, 0xf3, 0xa3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x7a, 0x58, 0xc0, 0xbe, 0x72, 0xbe, 0x21, 0x8b, 0x41, + 0xc6, 0x8, 0xb7, 0xfe, 0x7c, 0x5b, 0xb6, 0x30, 0x73, 0x6c, 0x71, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0xfa, + } + cdcBytes := make([]cadence.Value, 0) + for _, bt := range abiBytes { + cdcBytes = append(cdcBytes, cadence.UInt8(bt)) + } + encodedABI := cadence.NewArray( + cdcBytes, + ).WithType(cadence.NewVariableSizedArrayType(cadence.TheUInt8Type)) + + _, err := rt.ExecuteScript( + runtime.Script{ + Source: script, + Arguments: EncodeArgs([]cadence.Value{ + encodedABI, + }), + }, + runtime.Context{ + Interface: runtimeInterface, + Environment: scriptEnvironment, + Location: nextScriptLocation(), + }, + ) + require.Error(t, err) + assert.ErrorContains(t, err, "panic: signature mismatch") +} + func TestEVMAddressConstructionAndReturn(t *testing.T) { t.Parallel()