From 8137bb0d0b7ea3d0f4f7dadd2251f81934a4fa6f Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Fri, 6 Oct 2023 10:15:25 -0700 Subject: [PATCH] refactor: call `onCrossChainCall` when depositing to a contract (#1226) * add contract verification * mocks generation * fix lint = * fix tests * evm deposit tests * check contract call * add test contracts * deposit tests * test evm deposit * remove asset usage in parsed address * prevent calling non contract address * add comment --- testutil/contracts/Example.abi | 98 ++++++ testutil/contracts/Example.bin | 1 + testutil/contracts/Example.go | 346 +++++++++++++++++++ testutil/contracts/Example.json | 101 ++++++ testutil/contracts/Example.sol | 44 +++ testutil/contracts/Reverter.abi | 52 +++ testutil/contracts/Reverter.bin | 1 + testutil/contracts/Reverter.go | 231 +++++++++++++ testutil/contracts/Reverter.json | 55 +++ testutil/contracts/Reverter.sol | 22 ++ testutil/contracts/bindings.go | 13 + testutil/keeper/mocks/crosschain/account.go | 2 +- testutil/keeper/mocks/crosschain/bank.go | 2 +- testutil/keeper/mocks/crosschain/fungible.go | 33 +- testutil/keeper/mocks/crosschain/observer.go | 2 +- testutil/keeper/mocks/crosschain/staking.go | 2 +- testutil/keeper/mocks/fungible/account.go | 2 +- testutil/keeper/mocks/fungible/bank.go | 2 +- testutil/keeper/mocks/fungible/evm.go | 2 +- testutil/keeper/mocks/fungible/observer.go | 2 +- x/crosschain/keeper/evm_deposit.go | 66 ++-- x/crosschain/keeper/evm_deposit_test.go | 165 ++++++++- x/crosschain/types/errors.go | 2 +- x/crosschain/types/expected_keepers.go | 4 +- x/fungible/keeper/deposits.go | 46 +-- x/fungible/keeper/deposits_test.go | 129 +++++-- x/fungible/keeper/evm.go | 21 +- x/fungible/keeper/evm_test.go | 31 ++ x/fungible/types/errors.go | 1 + 29 files changed, 1373 insertions(+), 105 deletions(-) create mode 100644 testutil/contracts/Example.abi create mode 100644 testutil/contracts/Example.bin create mode 100644 testutil/contracts/Example.go create mode 100644 testutil/contracts/Example.json create mode 100644 testutil/contracts/Example.sol create mode 100644 testutil/contracts/Reverter.abi create mode 100644 testutil/contracts/Reverter.bin create mode 100644 testutil/contracts/Reverter.go create mode 100644 testutil/contracts/Reverter.json create mode 100644 testutil/contracts/Reverter.sol create mode 100644 testutil/contracts/bindings.go diff --git a/testutil/contracts/Example.abi b/testutil/contracts/Example.abi new file mode 100644 index 0000000000..26031cc7cf --- /dev/null +++ b/testutil/contracts/Example.abi @@ -0,0 +1,98 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "Foo", + "type": "error" + }, + { + "inputs": [], + "name": "bar", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "doRevert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "doRevertWithMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "doRevertWithRequire", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "doSucceed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Example.zContext", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "onCrossChainCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/testutil/contracts/Example.bin b/testutil/contracts/Example.bin new file mode 100644 index 0000000000..459380a475 --- /dev/null +++ b/testutil/contracts/Example.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506000808190555061043f806100276000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c8063afc874d214610067578063d720cb4514610071578063dd8e556c1461007b578063de43156e14610085578063fd5ad965146100a1578063febb0f7e146100ab575b600080fd5b61006f6100c9565b005b6100796100fb565b005b610083610136565b005b61009f600480360381019061009a91906102be565b610179565b005b6100a9610187565b005b6100b3610191565b6040516100c09190610371565b60405180910390f35b6040517fbfb4ebcf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161012d906103e9565b60405180910390fd5b6000610177576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161016e906103e9565b60405180910390fd5b565b826000819055505050505050565b6001600081905550565b60005481565b600080fd5b600080fd5b600080fd5b6000606082840312156101bc576101bb6101a1565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101f0826101c5565b9050919050565b610200816101e5565b811461020b57600080fd5b50565b60008135905061021d816101f7565b92915050565b6000819050919050565b61023681610223565b811461024157600080fd5b50565b6000813590506102538161022d565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261027e5761027d610259565b5b8235905067ffffffffffffffff81111561029b5761029a61025e565b5b6020830191508360018202830111156102b7576102b6610263565b5b9250929050565b6000806000806000608086880312156102da576102d9610197565b5b600086013567ffffffffffffffff8111156102f8576102f761019c565b5b610304888289016101a6565b95505060206103158882890161020e565b945050604061032688828901610244565b935050606086013567ffffffffffffffff8111156103475761034661019c565b5b61035388828901610268565b92509250509295509295909350565b61036b81610223565b82525050565b60006020820190506103866000830184610362565b92915050565b600082825260208201905092915050565b7f666f6f0000000000000000000000000000000000000000000000000000000000600082015250565b60006103d360038361038c565b91506103de8261039d565b602082019050919050565b60006020820190508181036000830152610402816103c6565b905091905056fea26469706673582212208285945d697ed6679c1d0595f68c015d717c2361d7e218ec27209cbbf18984bf64736f6c63430008150033 diff --git a/testutil/contracts/Example.go b/testutil/contracts/Example.go new file mode 100644 index 0000000000..5fc27ee606 --- /dev/null +++ b/testutil/contracts/Example.go @@ -0,0 +1,346 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package contracts + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// ExamplezContext is an auto generated low-level Go binding around an user-defined struct. +type ExamplezContext struct { + Origin []byte + Sender common.Address + ChainID *big.Int +} + +// ExampleMetaData contains all meta data concerning the Example contract. +var ExampleMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"Foo\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"bar\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"doRevert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"doRevertWithMessage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"doRevertWithRequire\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"doSucceed\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"origin\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"chainID\",\"type\":\"uint256\"}],\"internalType\":\"structExample.zContext\",\"name\":\"context\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"name\":\"onCrossChainCall\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b506000808190555061043f806100276000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c8063afc874d214610067578063d720cb4514610071578063dd8e556c1461007b578063de43156e14610085578063fd5ad965146100a1578063febb0f7e146100ab575b600080fd5b61006f6100c9565b005b6100796100fb565b005b610083610136565b005b61009f600480360381019061009a91906102be565b610179565b005b6100a9610187565b005b6100b3610191565b6040516100c09190610371565b60405180910390f35b6040517fbfb4ebcf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161012d906103e9565b60405180910390fd5b6000610177576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161016e906103e9565b60405180910390fd5b565b826000819055505050505050565b6001600081905550565b60005481565b600080fd5b600080fd5b600080fd5b6000606082840312156101bc576101bb6101a1565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101f0826101c5565b9050919050565b610200816101e5565b811461020b57600080fd5b50565b60008135905061021d816101f7565b92915050565b6000819050919050565b61023681610223565b811461024157600080fd5b50565b6000813590506102538161022d565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261027e5761027d610259565b5b8235905067ffffffffffffffff81111561029b5761029a61025e565b5b6020830191508360018202830111156102b7576102b6610263565b5b9250929050565b6000806000806000608086880312156102da576102d9610197565b5b600086013567ffffffffffffffff8111156102f8576102f761019c565b5b610304888289016101a6565b95505060206103158882890161020e565b945050604061032688828901610244565b935050606086013567ffffffffffffffff8111156103475761034661019c565b5b61035388828901610268565b92509250509295509295909350565b61036b81610223565b82525050565b60006020820190506103866000830184610362565b92915050565b600082825260208201905092915050565b7f666f6f0000000000000000000000000000000000000000000000000000000000600082015250565b60006103d360038361038c565b91506103de8261039d565b602082019050919050565b60006020820190508181036000830152610402816103c6565b905091905056fea26469706673582212208285945d697ed6679c1d0595f68c015d717c2361d7e218ec27209cbbf18984bf64736f6c63430008150033", +} + +// ExampleABI is the input ABI used to generate the binding from. +// Deprecated: Use ExampleMetaData.ABI instead. +var ExampleABI = ExampleMetaData.ABI + +// ExampleBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use ExampleMetaData.Bin instead. +var ExampleBin = ExampleMetaData.Bin + +// DeployExample deploys a new Ethereum contract, binding an instance of Example to it. +func DeployExample(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Example, error) { + parsed, err := ExampleMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ExampleBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Example{ExampleCaller: ExampleCaller{contract: contract}, ExampleTransactor: ExampleTransactor{contract: contract}, ExampleFilterer: ExampleFilterer{contract: contract}}, nil +} + +// Example is an auto generated Go binding around an Ethereum contract. +type Example struct { + ExampleCaller // Read-only binding to the contract + ExampleTransactor // Write-only binding to the contract + ExampleFilterer // Log filterer for contract events +} + +// ExampleCaller is an auto generated read-only Go binding around an Ethereum contract. +type ExampleCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ExampleTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ExampleTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ExampleFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ExampleFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ExampleSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ExampleSession struct { + Contract *Example // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ExampleCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ExampleCallerSession struct { + Contract *ExampleCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ExampleTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ExampleTransactorSession struct { + Contract *ExampleTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ExampleRaw is an auto generated low-level Go binding around an Ethereum contract. +type ExampleRaw struct { + Contract *Example // Generic contract binding to access the raw methods on +} + +// ExampleCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ExampleCallerRaw struct { + Contract *ExampleCaller // Generic read-only contract binding to access the raw methods on +} + +// ExampleTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ExampleTransactorRaw struct { + Contract *ExampleTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewExample creates a new instance of Example, bound to a specific deployed contract. +func NewExample(address common.Address, backend bind.ContractBackend) (*Example, error) { + contract, err := bindExample(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Example{ExampleCaller: ExampleCaller{contract: contract}, ExampleTransactor: ExampleTransactor{contract: contract}, ExampleFilterer: ExampleFilterer{contract: contract}}, nil +} + +// NewExampleCaller creates a new read-only instance of Example, bound to a specific deployed contract. +func NewExampleCaller(address common.Address, caller bind.ContractCaller) (*ExampleCaller, error) { + contract, err := bindExample(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ExampleCaller{contract: contract}, nil +} + +// NewExampleTransactor creates a new write-only instance of Example, bound to a specific deployed contract. +func NewExampleTransactor(address common.Address, transactor bind.ContractTransactor) (*ExampleTransactor, error) { + contract, err := bindExample(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ExampleTransactor{contract: contract}, nil +} + +// NewExampleFilterer creates a new log filterer instance of Example, bound to a specific deployed contract. +func NewExampleFilterer(address common.Address, filterer bind.ContractFilterer) (*ExampleFilterer, error) { + contract, err := bindExample(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ExampleFilterer{contract: contract}, nil +} + +// bindExample binds a generic wrapper to an already deployed contract. +func bindExample(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ExampleMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Example *ExampleRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Example.Contract.ExampleCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Example *ExampleRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Example.Contract.ExampleTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Example *ExampleRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Example.Contract.ExampleTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Example *ExampleCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Example.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Example *ExampleTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Example.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Example *ExampleTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Example.Contract.contract.Transact(opts, method, params...) +} + +// Bar is a free data retrieval call binding the contract method 0xfebb0f7e. +// +// Solidity: function bar() view returns(uint256) +func (_Example *ExampleCaller) Bar(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Example.contract.Call(opts, &out, "bar") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Bar is a free data retrieval call binding the contract method 0xfebb0f7e. +// +// Solidity: function bar() view returns(uint256) +func (_Example *ExampleSession) Bar() (*big.Int, error) { + return _Example.Contract.Bar(&_Example.CallOpts) +} + +// Bar is a free data retrieval call binding the contract method 0xfebb0f7e. +// +// Solidity: function bar() view returns(uint256) +func (_Example *ExampleCallerSession) Bar() (*big.Int, error) { + return _Example.Contract.Bar(&_Example.CallOpts) +} + +// DoRevert is a paid mutator transaction binding the contract method 0xafc874d2. +// +// Solidity: function doRevert() returns() +func (_Example *ExampleTransactor) DoRevert(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Example.contract.Transact(opts, "doRevert") +} + +// DoRevert is a paid mutator transaction binding the contract method 0xafc874d2. +// +// Solidity: function doRevert() returns() +func (_Example *ExampleSession) DoRevert() (*types.Transaction, error) { + return _Example.Contract.DoRevert(&_Example.TransactOpts) +} + +// DoRevert is a paid mutator transaction binding the contract method 0xafc874d2. +// +// Solidity: function doRevert() returns() +func (_Example *ExampleTransactorSession) DoRevert() (*types.Transaction, error) { + return _Example.Contract.DoRevert(&_Example.TransactOpts) +} + +// DoRevertWithMessage is a paid mutator transaction binding the contract method 0xd720cb45. +// +// Solidity: function doRevertWithMessage() returns() +func (_Example *ExampleTransactor) DoRevertWithMessage(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Example.contract.Transact(opts, "doRevertWithMessage") +} + +// DoRevertWithMessage is a paid mutator transaction binding the contract method 0xd720cb45. +// +// Solidity: function doRevertWithMessage() returns() +func (_Example *ExampleSession) DoRevertWithMessage() (*types.Transaction, error) { + return _Example.Contract.DoRevertWithMessage(&_Example.TransactOpts) +} + +// DoRevertWithMessage is a paid mutator transaction binding the contract method 0xd720cb45. +// +// Solidity: function doRevertWithMessage() returns() +func (_Example *ExampleTransactorSession) DoRevertWithMessage() (*types.Transaction, error) { + return _Example.Contract.DoRevertWithMessage(&_Example.TransactOpts) +} + +// DoRevertWithRequire is a paid mutator transaction binding the contract method 0xdd8e556c. +// +// Solidity: function doRevertWithRequire() returns() +func (_Example *ExampleTransactor) DoRevertWithRequire(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Example.contract.Transact(opts, "doRevertWithRequire") +} + +// DoRevertWithRequire is a paid mutator transaction binding the contract method 0xdd8e556c. +// +// Solidity: function doRevertWithRequire() returns() +func (_Example *ExampleSession) DoRevertWithRequire() (*types.Transaction, error) { + return _Example.Contract.DoRevertWithRequire(&_Example.TransactOpts) +} + +// DoRevertWithRequire is a paid mutator transaction binding the contract method 0xdd8e556c. +// +// Solidity: function doRevertWithRequire() returns() +func (_Example *ExampleTransactorSession) DoRevertWithRequire() (*types.Transaction, error) { + return _Example.Contract.DoRevertWithRequire(&_Example.TransactOpts) +} + +// DoSucceed is a paid mutator transaction binding the contract method 0xfd5ad965. +// +// Solidity: function doSucceed() returns() +func (_Example *ExampleTransactor) DoSucceed(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Example.contract.Transact(opts, "doSucceed") +} + +// DoSucceed is a paid mutator transaction binding the contract method 0xfd5ad965. +// +// Solidity: function doSucceed() returns() +func (_Example *ExampleSession) DoSucceed() (*types.Transaction, error) { + return _Example.Contract.DoSucceed(&_Example.TransactOpts) +} + +// DoSucceed is a paid mutator transaction binding the contract method 0xfd5ad965. +// +// Solidity: function doSucceed() returns() +func (_Example *ExampleTransactorSession) DoSucceed() (*types.Transaction, error) { + return _Example.Contract.DoSucceed(&_Example.TransactOpts) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 amount, bytes message) returns() +func (_Example *ExampleTransactor) OnCrossChainCall(opts *bind.TransactOpts, context ExamplezContext, zrc20 common.Address, amount *big.Int, message []byte) (*types.Transaction, error) { + return _Example.contract.Transact(opts, "onCrossChainCall", context, zrc20, amount, message) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 amount, bytes message) returns() +func (_Example *ExampleSession) OnCrossChainCall(context ExamplezContext, zrc20 common.Address, amount *big.Int, message []byte) (*types.Transaction, error) { + return _Example.Contract.OnCrossChainCall(&_Example.TransactOpts, context, zrc20, amount, message) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 amount, bytes message) returns() +func (_Example *ExampleTransactorSession) OnCrossChainCall(context ExamplezContext, zrc20 common.Address, amount *big.Int, message []byte) (*types.Transaction, error) { + return _Example.Contract.OnCrossChainCall(&_Example.TransactOpts, context, zrc20, amount, message) +} diff --git a/testutil/contracts/Example.json b/testutil/contracts/Example.json new file mode 100644 index 0000000000..a3b927e24e --- /dev/null +++ b/testutil/contracts/Example.json @@ -0,0 +1,101 @@ +{ + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "Foo", + "type": "error" + }, + { + "inputs": [], + "name": "bar", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "doRevert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "doRevertWithMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "doRevertWithRequire", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "doSucceed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Example.zContext", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "onCrossChainCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bin": "608060405234801561001057600080fd5b506000808190555061043f806100276000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c8063afc874d214610067578063d720cb4514610071578063dd8e556c1461007b578063de43156e14610085578063fd5ad965146100a1578063febb0f7e146100ab575b600080fd5b61006f6100c9565b005b6100796100fb565b005b610083610136565b005b61009f600480360381019061009a91906102be565b610179565b005b6100a9610187565b005b6100b3610191565b6040516100c09190610371565b60405180910390f35b6040517fbfb4ebcf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161012d906103e9565b60405180910390fd5b6000610177576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161016e906103e9565b60405180910390fd5b565b826000819055505050505050565b6001600081905550565b60005481565b600080fd5b600080fd5b600080fd5b6000606082840312156101bc576101bb6101a1565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101f0826101c5565b9050919050565b610200816101e5565b811461020b57600080fd5b50565b60008135905061021d816101f7565b92915050565b6000819050919050565b61023681610223565b811461024157600080fd5b50565b6000813590506102538161022d565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261027e5761027d610259565b5b8235905067ffffffffffffffff81111561029b5761029a61025e565b5b6020830191508360018202830111156102b7576102b6610263565b5b9250929050565b6000806000806000608086880312156102da576102d9610197565b5b600086013567ffffffffffffffff8111156102f8576102f761019c565b5b610304888289016101a6565b95505060206103158882890161020e565b945050604061032688828901610244565b935050606086013567ffffffffffffffff8111156103475761034661019c565b5b61035388828901610268565b92509250509295509295909350565b61036b81610223565b82525050565b60006020820190506103866000830184610362565b92915050565b600082825260208201905092915050565b7f666f6f0000000000000000000000000000000000000000000000000000000000600082015250565b60006103d360038361038c565b91506103de8261039d565b602082019050919050565b60006020820190508181036000830152610402816103c6565b905091905056fea26469706673582212208285945d697ed6679c1d0595f68c015d717c2361d7e218ec27209cbbf18984bf64736f6c63430008150033" +} diff --git a/testutil/contracts/Example.sol b/testutil/contracts/Example.sol new file mode 100644 index 0000000000..1b36e7a15d --- /dev/null +++ b/testutil/contracts/Example.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +// Sample contract for evm tests +contract Example { + error Foo(); + + struct zContext { + bytes origin; + address sender; + uint256 chainID; + } + + uint256 public bar; + + constructor() { + bar = 0; + } + + function doRevert() external { + revert Foo(); + } + + function doRevertWithMessage() external { + revert("foo"); + } + + function doRevertWithRequire() external { + require(false, "foo"); + } + + function doSucceed() external { + bar = 1; + } + + function onCrossChainCall( + zContext calldata context, + address zrc20, + uint256 amount, + bytes calldata message + ) external { + bar = amount; + } +} \ No newline at end of file diff --git a/testutil/contracts/Reverter.abi b/testutil/contracts/Reverter.abi new file mode 100644 index 0000000000..7183768beb --- /dev/null +++ b/testutil/contracts/Reverter.abi @@ -0,0 +1,52 @@ +[ + { + "inputs": [], + "name": "Foo", + "type": "error" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Reverter.zContext", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "onCrossChainCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/testutil/contracts/Reverter.bin b/testutil/contracts/Reverter.bin new file mode 100644 index 0000000000..b342cf93cd --- /dev/null +++ b/testutil/contracts/Reverter.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b5061027f806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063de43156e14610030575b600080fd5b61004a600480360381019061004591906101a5565b61004c565b005b6040517fbfb4ebcf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600080fd5b600080fd5b600080fd5b6000606082840312156100a3576100a2610088565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100d7826100ac565b9050919050565b6100e7816100cc565b81146100f257600080fd5b50565b600081359050610104816100de565b92915050565b6000819050919050565b61011d8161010a565b811461012857600080fd5b50565b60008135905061013a81610114565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261016557610164610140565b5b8235905067ffffffffffffffff81111561018257610181610145565b5b60208301915083600182028301111561019e5761019d61014a565b5b9250929050565b6000806000806000608086880312156101c1576101c061007e565b5b600086013567ffffffffffffffff8111156101df576101de610083565b5b6101eb8882890161008d565b95505060206101fc888289016100f5565b945050604061020d8882890161012b565b935050606086013567ffffffffffffffff81111561022e5761022d610083565b5b61023a8882890161014f565b9250925050929550929590935056fea2646970667358221220793398f92335fd817a4bf49381e3e4cbdfa00223720869567e31300bf71f0c9e64736f6c63430008150033 diff --git a/testutil/contracts/Reverter.go b/testutil/contracts/Reverter.go new file mode 100644 index 0000000000..80936f0933 --- /dev/null +++ b/testutil/contracts/Reverter.go @@ -0,0 +1,231 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package contracts + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// ReverterzContext is an auto generated low-level Go binding around an user-defined struct. +type ReverterzContext struct { + Origin []byte + Sender common.Address + ChainID *big.Int +} + +// ReverterMetaData contains all meta data concerning the Reverter contract. +var ReverterMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"name\":\"Foo\",\"type\":\"error\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"origin\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"chainID\",\"type\":\"uint256\"}],\"internalType\":\"structReverter.zContext\",\"name\":\"context\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"name\":\"onCrossChainCall\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b5061027f806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063de43156e14610030575b600080fd5b61004a600480360381019061004591906101a5565b61004c565b005b6040517fbfb4ebcf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600080fd5b600080fd5b600080fd5b6000606082840312156100a3576100a2610088565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100d7826100ac565b9050919050565b6100e7816100cc565b81146100f257600080fd5b50565b600081359050610104816100de565b92915050565b6000819050919050565b61011d8161010a565b811461012857600080fd5b50565b60008135905061013a81610114565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261016557610164610140565b5b8235905067ffffffffffffffff81111561018257610181610145565b5b60208301915083600182028301111561019e5761019d61014a565b5b9250929050565b6000806000806000608086880312156101c1576101c061007e565b5b600086013567ffffffffffffffff8111156101df576101de610083565b5b6101eb8882890161008d565b95505060206101fc888289016100f5565b945050604061020d8882890161012b565b935050606086013567ffffffffffffffff81111561022e5761022d610083565b5b61023a8882890161014f565b9250925050929550929590935056fea2646970667358221220793398f92335fd817a4bf49381e3e4cbdfa00223720869567e31300bf71f0c9e64736f6c63430008150033", +} + +// ReverterABI is the input ABI used to generate the binding from. +// Deprecated: Use ReverterMetaData.ABI instead. +var ReverterABI = ReverterMetaData.ABI + +// ReverterBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use ReverterMetaData.Bin instead. +var ReverterBin = ReverterMetaData.Bin + +// DeployReverter deploys a new Ethereum contract, binding an instance of Reverter to it. +func DeployReverter(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Reverter, error) { + parsed, err := ReverterMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ReverterBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Reverter{ReverterCaller: ReverterCaller{contract: contract}, ReverterTransactor: ReverterTransactor{contract: contract}, ReverterFilterer: ReverterFilterer{contract: contract}}, nil +} + +// Reverter is an auto generated Go binding around an Ethereum contract. +type Reverter struct { + ReverterCaller // Read-only binding to the contract + ReverterTransactor // Write-only binding to the contract + ReverterFilterer // Log filterer for contract events +} + +// ReverterCaller is an auto generated read-only Go binding around an Ethereum contract. +type ReverterCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReverterTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ReverterTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReverterFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ReverterFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReverterSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ReverterSession struct { + Contract *Reverter // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ReverterCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ReverterCallerSession struct { + Contract *ReverterCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ReverterTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ReverterTransactorSession struct { + Contract *ReverterTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ReverterRaw is an auto generated low-level Go binding around an Ethereum contract. +type ReverterRaw struct { + Contract *Reverter // Generic contract binding to access the raw methods on +} + +// ReverterCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ReverterCallerRaw struct { + Contract *ReverterCaller // Generic read-only contract binding to access the raw methods on +} + +// ReverterTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ReverterTransactorRaw struct { + Contract *ReverterTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewReverter creates a new instance of Reverter, bound to a specific deployed contract. +func NewReverter(address common.Address, backend bind.ContractBackend) (*Reverter, error) { + contract, err := bindReverter(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Reverter{ReverterCaller: ReverterCaller{contract: contract}, ReverterTransactor: ReverterTransactor{contract: contract}, ReverterFilterer: ReverterFilterer{contract: contract}}, nil +} + +// NewReverterCaller creates a new read-only instance of Reverter, bound to a specific deployed contract. +func NewReverterCaller(address common.Address, caller bind.ContractCaller) (*ReverterCaller, error) { + contract, err := bindReverter(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ReverterCaller{contract: contract}, nil +} + +// NewReverterTransactor creates a new write-only instance of Reverter, bound to a specific deployed contract. +func NewReverterTransactor(address common.Address, transactor bind.ContractTransactor) (*ReverterTransactor, error) { + contract, err := bindReverter(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ReverterTransactor{contract: contract}, nil +} + +// NewReverterFilterer creates a new log filterer instance of Reverter, bound to a specific deployed contract. +func NewReverterFilterer(address common.Address, filterer bind.ContractFilterer) (*ReverterFilterer, error) { + contract, err := bindReverter(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ReverterFilterer{contract: contract}, nil +} + +// bindReverter binds a generic wrapper to an already deployed contract. +func bindReverter(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ReverterMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Reverter *ReverterRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Reverter.Contract.ReverterCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Reverter *ReverterRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Reverter.Contract.ReverterTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Reverter *ReverterRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Reverter.Contract.ReverterTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Reverter *ReverterCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Reverter.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Reverter *ReverterTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Reverter.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Reverter *ReverterTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Reverter.Contract.contract.Transact(opts, method, params...) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 amount, bytes message) returns() +func (_Reverter *ReverterTransactor) OnCrossChainCall(opts *bind.TransactOpts, context ReverterzContext, zrc20 common.Address, amount *big.Int, message []byte) (*types.Transaction, error) { + return _Reverter.contract.Transact(opts, "onCrossChainCall", context, zrc20, amount, message) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 amount, bytes message) returns() +func (_Reverter *ReverterSession) OnCrossChainCall(context ReverterzContext, zrc20 common.Address, amount *big.Int, message []byte) (*types.Transaction, error) { + return _Reverter.Contract.OnCrossChainCall(&_Reverter.TransactOpts, context, zrc20, amount, message) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 amount, bytes message) returns() +func (_Reverter *ReverterTransactorSession) OnCrossChainCall(context ReverterzContext, zrc20 common.Address, amount *big.Int, message []byte) (*types.Transaction, error) { + return _Reverter.Contract.OnCrossChainCall(&_Reverter.TransactOpts, context, zrc20, amount, message) +} diff --git a/testutil/contracts/Reverter.json b/testutil/contracts/Reverter.json new file mode 100644 index 0000000000..e6e1cacadd --- /dev/null +++ b/testutil/contracts/Reverter.json @@ -0,0 +1,55 @@ +{ + "abi": [ + { + "inputs": [], + "name": "Foo", + "type": "error" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Reverter.zContext", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "onCrossChainCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bin": "608060405234801561001057600080fd5b5061027f806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063de43156e14610030575b600080fd5b61004a600480360381019061004591906101a5565b61004c565b005b6040517fbfb4ebcf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600080fd5b600080fd5b600080fd5b6000606082840312156100a3576100a2610088565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100d7826100ac565b9050919050565b6100e7816100cc565b81146100f257600080fd5b50565b600081359050610104816100de565b92915050565b6000819050919050565b61011d8161010a565b811461012857600080fd5b50565b60008135905061013a81610114565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261016557610164610140565b5b8235905067ffffffffffffffff81111561018257610181610145565b5b60208301915083600182028301111561019e5761019d61014a565b5b9250929050565b6000806000806000608086880312156101c1576101c061007e565b5b600086013567ffffffffffffffff8111156101df576101de610083565b5b6101eb8882890161008d565b95505060206101fc888289016100f5565b945050604061020d8882890161012b565b935050606086013567ffffffffffffffff81111561022e5761022d610083565b5b61023a8882890161014f565b9250925050929550929590935056fea2646970667358221220793398f92335fd817a4bf49381e3e4cbdfa00223720869567e31300bf71f0c9e64736f6c63430008150033" +} diff --git a/testutil/contracts/Reverter.sol b/testutil/contracts/Reverter.sol new file mode 100644 index 0000000000..d84eaa0b30 --- /dev/null +++ b/testutil/contracts/Reverter.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +// Sample contract for evm tests +contract Reverter { + error Foo(); + + struct zContext { + bytes origin; + address sender; + uint256 chainID; + } + + function onCrossChainCall( + zContext calldata context, + address zrc20, + uint256 amount, + bytes calldata message + ) external { + revert Foo(); + } +} \ No newline at end of file diff --git a/testutil/contracts/bindings.go b/testutil/contracts/bindings.go new file mode 100644 index 0000000000..bb64d01778 --- /dev/null +++ b/testutil/contracts/bindings.go @@ -0,0 +1,13 @@ +// Example +//go:generate sh -c "solc --evm-version paris Example.sol --combined-json abi,bin | jq '.contracts.\"Example.sol:Example\"' > Example.json" +//go:generate sh -c "cat Example.json | jq .abi > Example.abi" +//go:generate sh -c "cat Example.json | jq .bin | tr -d '\"' > Example.bin" +//go:generate sh -c "abigen --abi Example.abi --bin Example.bin --pkg contracts --type Example --out Example.go" + +// Reverter +//go:generate sh -c "solc --evm-version paris Reverter.sol --combined-json abi,bin | jq '.contracts.\"Reverter.sol:Reverter\"' > Reverter.json" +//go:generate sh -c "cat Reverter.json | jq .abi > Reverter.abi" +//go:generate sh -c "cat Reverter.json | jq .bin | tr -d '\"' > Reverter.bin" +//go:generate sh -c "abigen --abi Reverter.abi --bin Reverter.bin --pkg contracts --type Reverter --out Reverter.go" + +package contracts diff --git a/testutil/keeper/mocks/crosschain/account.go b/testutil/keeper/mocks/crosschain/account.go index 626900da83..bde82aa1df 100644 --- a/testutil/keeper/mocks/crosschain/account.go +++ b/testutil/keeper/mocks/crosschain/account.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks diff --git a/testutil/keeper/mocks/crosschain/bank.go b/testutil/keeper/mocks/crosschain/bank.go index cdf1e371ef..bf4e73987b 100644 --- a/testutil/keeper/mocks/crosschain/bank.go +++ b/testutil/keeper/mocks/crosschain/bank.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks diff --git a/testutil/keeper/mocks/crosschain/fungible.go b/testutil/keeper/mocks/crosschain/fungible.go index a6b763b8a3..ba803b9a52 100644 --- a/testutil/keeper/mocks/crosschain/fungible.go +++ b/testutil/keeper/mocks/crosschain/fungible.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks @@ -511,30 +511,37 @@ func (_m *CrosschainFungibleKeeper) WithdrawFromGasStabilityPool(ctx types.Conte return r0 } -// ZRC20DepositAndCallContract provides a mock function with given fields: ctx, from, to, amount, senderChain, message, contract, data, coinType, asset -func (_m *CrosschainFungibleKeeper) ZRC20DepositAndCallContract(ctx types.Context, from []byte, to common.Address, amount *big.Int, senderChain *zetacorecommon.Chain, message string, contract common.Address, data []byte, coinType zetacorecommon.CoinType, asset string) (*evmtypes.MsgEthereumTxResponse, error) { - ret := _m.Called(ctx, from, to, amount, senderChain, message, contract, data, coinType, asset) +// ZRC20DepositAndCallContract provides a mock function with given fields: ctx, from, to, amount, senderChain, data, coinType, asset +func (_m *CrosschainFungibleKeeper) ZRC20DepositAndCallContract(ctx types.Context, from []byte, to common.Address, amount *big.Int, senderChain *zetacorecommon.Chain, data []byte, coinType zetacorecommon.CoinType, asset string) (*evmtypes.MsgEthereumTxResponse, bool, error) { + ret := _m.Called(ctx, from, to, amount, senderChain, data, coinType, asset) var r0 *evmtypes.MsgEthereumTxResponse - var r1 error - if rf, ok := ret.Get(0).(func(types.Context, []byte, common.Address, *big.Int, *zetacorecommon.Chain, string, common.Address, []byte, zetacorecommon.CoinType, string) (*evmtypes.MsgEthereumTxResponse, error)); ok { - return rf(ctx, from, to, amount, senderChain, message, contract, data, coinType, asset) + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(types.Context, []byte, common.Address, *big.Int, *zetacorecommon.Chain, []byte, zetacorecommon.CoinType, string) (*evmtypes.MsgEthereumTxResponse, bool, error)); ok { + return rf(ctx, from, to, amount, senderChain, data, coinType, asset) } - if rf, ok := ret.Get(0).(func(types.Context, []byte, common.Address, *big.Int, *zetacorecommon.Chain, string, common.Address, []byte, zetacorecommon.CoinType, string) *evmtypes.MsgEthereumTxResponse); ok { - r0 = rf(ctx, from, to, amount, senderChain, message, contract, data, coinType, asset) + if rf, ok := ret.Get(0).(func(types.Context, []byte, common.Address, *big.Int, *zetacorecommon.Chain, []byte, zetacorecommon.CoinType, string) *evmtypes.MsgEthereumTxResponse); ok { + r0 = rf(ctx, from, to, amount, senderChain, data, coinType, asset) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*evmtypes.MsgEthereumTxResponse) } } - if rf, ok := ret.Get(1).(func(types.Context, []byte, common.Address, *big.Int, *zetacorecommon.Chain, string, common.Address, []byte, zetacorecommon.CoinType, string) error); ok { - r1 = rf(ctx, from, to, amount, senderChain, message, contract, data, coinType, asset) + if rf, ok := ret.Get(1).(func(types.Context, []byte, common.Address, *big.Int, *zetacorecommon.Chain, []byte, zetacorecommon.CoinType, string) bool); ok { + r1 = rf(ctx, from, to, amount, senderChain, data, coinType, asset) } else { - r1 = ret.Error(1) + r1 = ret.Get(1).(bool) } - return r0, r1 + if rf, ok := ret.Get(2).(func(types.Context, []byte, common.Address, *big.Int, *zetacorecommon.Chain, []byte, zetacorecommon.CoinType, string) error); ok { + r2 = rf(ctx, from, to, amount, senderChain, data, coinType, asset) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } // NewCrosschainFungibleKeeper creates a new instance of CrosschainFungibleKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. diff --git a/testutil/keeper/mocks/crosschain/observer.go b/testutil/keeper/mocks/crosschain/observer.go index 5eba61daaa..0f1d2b310c 100644 --- a/testutil/keeper/mocks/crosschain/observer.go +++ b/testutil/keeper/mocks/crosschain/observer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks diff --git a/testutil/keeper/mocks/crosschain/staking.go b/testutil/keeper/mocks/crosschain/staking.go index 3027f516b1..0faf55a58a 100644 --- a/testutil/keeper/mocks/crosschain/staking.go +++ b/testutil/keeper/mocks/crosschain/staking.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks diff --git a/testutil/keeper/mocks/fungible/account.go b/testutil/keeper/mocks/fungible/account.go index 8ca9535cad..81f510c6d3 100644 --- a/testutil/keeper/mocks/fungible/account.go +++ b/testutil/keeper/mocks/fungible/account.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks diff --git a/testutil/keeper/mocks/fungible/bank.go b/testutil/keeper/mocks/fungible/bank.go index a061b091f4..da56574169 100644 --- a/testutil/keeper/mocks/fungible/bank.go +++ b/testutil/keeper/mocks/fungible/bank.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks diff --git a/testutil/keeper/mocks/fungible/evm.go b/testutil/keeper/mocks/fungible/evm.go index 81653bd4a4..e0ac00173c 100644 --- a/testutil/keeper/mocks/fungible/evm.go +++ b/testutil/keeper/mocks/fungible/evm.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks diff --git a/testutil/keeper/mocks/fungible/observer.go b/testutil/keeper/mocks/fungible/observer.go index 13f419d80a..fc7ec327b6 100644 --- a/testutil/keeper/mocks/fungible/observer.go +++ b/testutil/keeper/mocks/fungible/observer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.33.3. DO NOT EDIT. +// Code generated by mockery v2.34.2. DO NOT EDIT. package mocks diff --git a/x/crosschain/keeper/evm_deposit.go b/x/crosschain/keeper/evm_deposit.go index ddc18bc291..cf5e8f952a 100644 --- a/x/crosschain/keeper/evm_deposit.go +++ b/x/crosschain/keeper/evm_deposit.go @@ -4,8 +4,6 @@ import ( "encoding/hex" "fmt" - fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" - sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" evmtypes "github.com/evmos/ethermint/x/evm/types" @@ -14,12 +12,18 @@ import ( tmtypes "github.com/tendermint/tendermint/types" "github.com/zeta-chain/zetacore/common" "github.com/zeta-chain/zetacore/x/crosschain/types" + fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" ) // HandleEVMDeposit handles a deposit from an inbound tx // returns (isContractReverted, err) // (true, non-nil) means CallEVM() reverted -func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx, msg types.MsgVoteOnObservedInboundTx, senderChain *common.Chain) (bool, error) { +func (k Keeper) HandleEVMDeposit( + ctx sdk.Context, + cctx *types.CrossChainTx, + msg types.MsgVoteOnObservedInboundTx, + senderChain *common.Chain, +) (bool, error) { to := ethcommon.HexToAddress(msg.Receiver) var ethTxHash ethcommon.Hash if len(ctx.TxBytes()) > 0 { @@ -39,21 +43,36 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx, msg } } else { // cointype is Gas or ERC20; then it could be a ZRC20 deposit/depositAndCall cctx. - contract, data, err := parseContractAndData(msg.Message, msg.Asset) + parsedAddress, data, err := parseAddressAndData(msg.Message) if err != nil { - return false, errors.Wrap(types.ErrUnableToParseContract, err.Error()) + return false, errors.Wrap(types.ErrUnableToParseAddress, err.Error()) } + if parsedAddress != (ethcommon.Address{}) { + to = parsedAddress + } + from, err := senderChain.DecodeAddress(msg.Sender) if err != nil { return false, fmt.Errorf("HandleEVMDeposit: unable to decode address: %s", err.Error()) } - evmTxResponse, err := k.fungibleKeeper.ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + evmTxResponse, contractCall, err := k.fungibleKeeper.ZRC20DepositAndCallContract( + ctx, + from, + to, + msg.Amount.BigInt(), + senderChain, + data, + msg.CoinType, + msg.Asset, + ) if err != nil { isContractReverted := false - // consider the contract as reverted if foreign coin liquidity cap is reached - if (evmTxResponse != nil && evmTxResponse.Failed()) || errors.Is(err, fungibletypes.ErrForeignCoinCapReached) { + // consider the contract as reverted if foreign coin liquidity cap is reached or calling a non-contract address + if (evmTxResponse != nil && evmTxResponse.Failed()) || + errors.Is(err, fungibletypes.ErrForeignCoinCapReached) || + errors.Is(err, fungibletypes.ErrCallNonContract) { isContractReverted = true } @@ -62,7 +81,7 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx, msg // non-empty msg.Message means this is a contract call; therefore the logs should be processed. // a withdrawal event in the logs could generate cctxs for outbound transactions. - if !evmTxResponse.Failed() && len(msg.Message) > 0 { + if !evmTxResponse.Failed() && contractCall { logs := evmtypes.LogsToEthereum(evmTxResponse.Logs) if len(logs) > 0 { ctx = ctx.WithValue("inCctxIndex", cctx.Index) @@ -71,7 +90,7 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx, msg txOrigin = msg.Sender } - err = k.ProcessLogs(ctx, logs, contract, txOrigin) + err = k.ProcessLogs(ctx, logs, to, txOrigin) if err != nil { // ProcessLogs should not error; error indicates exception, should abort return false, errors.Wrap(types.ErrCannotProcessWithdrawal, err.Error()) @@ -80,7 +99,7 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx, msg sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute("action", "DepositZRC20AndCallContract"), - sdk.NewAttribute("contract", contract.String()), + sdk.NewAttribute("contract", to.String()), sdk.NewAttribute("data", hex.EncodeToString(data)), sdk.NewAttribute("cctxIndex", cctx.Index), ), @@ -91,26 +110,25 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx, msg return false, nil } +// parseAddressAndData parses the message string into an address and data // message is hex encoded byte array // [ contractAddress calldata ] // [ 20B, variable] -func parseContractAndData(message string, asset string) (contractAddress ethcommon.Address, data []byte, err error) { +func parseAddressAndData(message string) (ethcommon.Address, []byte, error) { if len(message) == 0 { - return contractAddress, nil, nil + return ethcommon.Address{}, nil, nil } - data, err = hex.DecodeString(message) + + data, err := hex.DecodeString(message) if err != nil { - return contractAddress, nil, err + return ethcommon.Address{}, nil, fmt.Errorf("message should be a hex encoded string: " + err.Error()) } + if len(data) < 20 { - if len(asset) != 42 || asset[:2] != "0x" { - err = fmt.Errorf("invalid message length") - return contractAddress, nil, err - } - contractAddress = ethcommon.HexToAddress(asset) - } else { - contractAddress = ethcommon.BytesToAddress(data[:20]) - data = data[20:] + return ethcommon.Address{}, data, nil } - return contractAddress, data, nil + + address := ethcommon.BytesToAddress(data[:20]) + data = data[20:] + return address, data, nil } diff --git a/x/crosschain/keeper/evm_deposit_test.go b/x/crosschain/keeper/evm_deposit_test.go index f5ada00f5b..0a0626af5d 100644 --- a/x/crosschain/keeper/evm_deposit_test.go +++ b/x/crosschain/keeper/evm_deposit_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "encoding/hex" "errors" "math/big" "testing" @@ -95,11 +96,9 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount, senderChain, mock.Anything, - mock.Anything, - mock.Anything, common.CoinType_ERC20, mock.Anything, - ).Return(&evmtypes.MsgEthereumTxResponse{}, nil) + ).Return(&evmtypes.MsgEthereumTxResponse{}, false, nil) // call HandleEVMDeposit reverted, err := k.HandleEVMDeposit( @@ -142,11 +141,9 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount, senderChain, mock.Anything, - mock.Anything, - mock.Anything, common.CoinType_ERC20, mock.Anything, - ).Return(&evmtypes.MsgEthereumTxResponse{}, errDeposit) + ).Return(&evmtypes.MsgEthereumTxResponse{}, false, errDeposit) // call HandleEVMDeposit reverted, err := k.HandleEVMDeposit( @@ -189,11 +186,9 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount, senderChain, mock.Anything, - mock.Anything, - mock.Anything, common.CoinType_ERC20, mock.Anything, - ).Return(&evmtypes.MsgEthereumTxResponse{VmError: "reverted"}, errDeposit) + ).Return(&evmtypes.MsgEthereumTxResponse{VmError: "reverted"}, false, errDeposit) // call HandleEVMDeposit reverted, err := k.HandleEVMDeposit( @@ -235,11 +230,51 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { amount, senderChain, mock.Anything, + common.CoinType_ERC20, + mock.Anything, + ).Return(&evmtypes.MsgEthereumTxResponse{}, false, fungibletypes.ErrForeignCoinCapReached) + + // call HandleEVMDeposit + reverted, err := k.HandleEVMDeposit( + ctx, + sample.CrossChainTx(t, "foo"), + types.MsgVoteOnObservedInboundTx{ + Sender: sample.EthAddress().String(), + Receiver: receiver.String(), + Amount: math.NewUintFromBigInt(amount), + CoinType: common.CoinType_ERC20, + Message: "", + Asset: "", + }, + senderChain, + ) + require.ErrorIs(t, err, fungibletypes.ErrForeignCoinCapReached) + require.True(t, reverted) + fungibleMock.AssertExpectations(t) + }) + + t.Run("should return error with reverted if deposit ERC20 fails with calling a non-contract address", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + senderChain := getValidEthChain(t) + + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + + fungibleMock.On( + "ZRC20DepositAndCallContract", + ctx, mock.Anything, + receiver, + amount, + senderChain, mock.Anything, common.CoinType_ERC20, mock.Anything, - ).Return(&evmtypes.MsgEthereumTxResponse{}, fungibletypes.ErrForeignCoinCapReached) + ).Return(&evmtypes.MsgEthereumTxResponse{}, false, fungibletypes.ErrCallNonContract) // call HandleEVMDeposit reverted, err := k.HandleEVMDeposit( @@ -255,11 +290,119 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { }, senderChain, ) - require.ErrorIs(t, err, fungibletypes.ErrForeignCoinCapReached) + require.ErrorIs(t, err, fungibletypes.ErrCallNonContract) require.True(t, reverted) fungibleMock.AssertExpectations(t) }) + t.Run("should fail if can't parse address and data", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + senderChain := getValidEthChain(t) + + _, err := k.HandleEVMDeposit( + ctx, + sample.CrossChainTx(t, "foo"), + types.MsgVoteOnObservedInboundTx{ + Sender: sample.EthAddress().String(), + Receiver: sample.EthAddress().String(), + Amount: math.NewUint(42), + CoinType: common.CoinType_Gas, + Message: "not_hex", + Asset: "", + }, + senderChain, + ) + require.ErrorIs(t, err, types.ErrUnableToParseAddress) + }) + + t.Run("should deposit into address if address is parsed", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + senderChain := getValidEthChain(t) + + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + + data, err := hex.DecodeString("DEADBEEF") + require.NoError(t, err) + fungibleMock.On( + "ZRC20DepositAndCallContract", + ctx, + mock.Anything, + receiver, + amount, + senderChain, + data, + common.CoinType_ERC20, + mock.Anything, + ).Return(&evmtypes.MsgEthereumTxResponse{}, false, nil) + + reverted, err := k.HandleEVMDeposit( + ctx, + sample.CrossChainTx(t, "foo"), + types.MsgVoteOnObservedInboundTx{ + Sender: sample.EthAddress().String(), + Receiver: sample.EthAddress().String(), + Amount: math.NewUintFromBigInt(amount), + CoinType: common.CoinType_ERC20, + Message: receiver.Hex()[2:] + "DEADBEEF", + Asset: "", + }, + senderChain, + ) + require.NoError(t, err) + require.False(t, reverted) + fungibleMock.AssertExpectations(t) + }) + + t.Run("should deposit into receiver with specified data if no address parsed with data", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + senderChain := getValidEthChain(t) + + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + + data, err := hex.DecodeString("DEADBEEF") + require.NoError(t, err) + fungibleMock.On( + "ZRC20DepositAndCallContract", + ctx, + mock.Anything, + receiver, + amount, + senderChain, + data, + common.CoinType_ERC20, + mock.Anything, + ).Return(&evmtypes.MsgEthereumTxResponse{}, false, nil) + + reverted, err := k.HandleEVMDeposit( + ctx, + sample.CrossChainTx(t, "foo"), + types.MsgVoteOnObservedInboundTx{ + Sender: sample.EthAddress().String(), + Receiver: receiver.String(), + Amount: math.NewUintFromBigInt(amount), + CoinType: common.CoinType_ERC20, + Message: "DEADBEEF", + Asset: "", + }, + senderChain, + ) + require.NoError(t, err) + require.False(t, reverted) + fungibleMock.AssertExpectations(t) + }) + // TODO: add test cases for testing logs process // https://github.com/zeta-chain/node/issues/1207 } diff --git a/x/crosschain/types/errors.go b/x/crosschain/types/errors.go index b8191ef9fa..ac6a1497f5 100644 --- a/x/crosschain/types/errors.go +++ b/x/crosschain/types/errors.go @@ -13,7 +13,7 @@ var ( ErrCannotFindReceiverNonce = errorsmod.Register(ModuleName, 1110, "cannot find receiver chain nonce") ErrGasCoinNotFound = errorsmod.Register(ModuleName, 1113, "gas coin not found for SenderChain") - ErrUnableToParseContract = errorsmod.Register(ModuleName, 1115, "cannot parse contract and data") + ErrUnableToParseAddress = errorsmod.Register(ModuleName, 1115, "cannot parse address and data") ErrCannotProcessWithdrawal = errorsmod.Register(ModuleName, 1116, "cannot process withdrawal event") ErrForeignCoinNotFound = errorsmod.Register(ModuleName, 1118, "gas coin not found for SenderChain") ErrNotEnoughPermissions = errorsmod.Register(ModuleName, 1119, "not enough permissions for current actions") diff --git a/x/crosschain/types/expected_keepers.go b/x/crosschain/types/expected_keepers.go index 0ddb010735..fc5960b5e5 100644 --- a/x/crosschain/types/expected_keepers.go +++ b/x/crosschain/types/expected_keepers.go @@ -97,12 +97,10 @@ type FungibleKeeper interface { to eth.Address, amount *big.Int, senderChain *common.Chain, - message string, - contract eth.Address, data []byte, coinType common.CoinType, asset string, - ) (*evmtypes.MsgEthereumTxResponse, error) + ) (*evmtypes.MsgEthereumTxResponse, bool, error) CallUniswapV2RouterSwapExactTokensForTokens( ctx sdk.Context, sender eth.Address, diff --git a/x/fungible/keeper/deposits.go b/x/fungible/keeper/deposits.go index cb6a493211..fa197a953b 100644 --- a/x/fungible/keeper/deposits.go +++ b/x/fungible/keeper/deposits.go @@ -19,18 +19,18 @@ func (k Keeper) DepositCoinZeta(ctx sdk.Context, to eth.Address, amount *big.Int } // ZRC20DepositAndCallContract deposits ZRC20 to the EVM account and calls the contract +// returns [txResponse, isContractCall, error] +// isContractCall is true if the receiver is a contract and a contract call was made func (k Keeper) ZRC20DepositAndCallContract( ctx sdk.Context, from []byte, to eth.Address, amount *big.Int, senderChain *common.Chain, - message string, - contract eth.Address, data []byte, coinType common.CoinType, asset string, -) (*evmtypes.MsgEthereumTxResponse, error) { +) (*evmtypes.MsgEthereumTxResponse, bool, error) { var ZRC20Contract eth.Address var coin types.ForeignCoins var found bool @@ -39,15 +39,14 @@ func (k Keeper) ZRC20DepositAndCallContract( if coinType == common.CoinType_Gas { coin, found = k.GetGasCoinForForeignCoin(ctx, senderChain.ChainId) if !found { - return nil, crosschaintypes.ErrGasCoinNotFound + return nil, false, crosschaintypes.ErrGasCoinNotFound } } else { coin, found = k.GetForeignCoinFromAsset(ctx, asset, senderChain.ChainId) if !found { - return nil, crosschaintypes.ErrForeignCoinNotFound + return nil, false, crosschaintypes.ErrForeignCoinNotFound } } - ZRC20Contract = eth.HexToAddress(coin.Zrc20ContractAddress) // check foreign coins cap if it has a cap @@ -55,30 +54,33 @@ func (k Keeper) ZRC20DepositAndCallContract( liquidityCap := coin.LiquidityCap.BigInt() totalSupply, err := k.TotalSupplyZRC4(ctx, ZRC20Contract) if err != nil { - return nil, err + return nil, false, err } newSupply := new(big.Int).Add(totalSupply, amount) if newSupply.Cmp(liquidityCap) > 0 { - return nil, types.ErrForeignCoinCapReached + return nil, false, types.ErrForeignCoinCapReached } } - // no message: transfer to EVM account - if len(message) == 0 { - return k.DepositZRC20(ctx, ZRC20Contract, to, amount) - } - - // non-empty message with empty data: deposit to contract - if len(data) == 0 { - return k.DepositZRC20(ctx, ZRC20Contract, contract, amount) + // check if the receiver is a contract + // if it is, then the hook onCrossChainCall() will be called + // if not, the zrc20 are simply transferred to the receiver + acc := k.evmKeeper.GetAccount(ctx, to) + if acc != nil && acc.IsContract() { + context := systemcontract.ZContext{ + Origin: from, + Sender: eth.Address{}, + ChainID: big.NewInt(senderChain.ChainId), + } + res, err := k.DepositZRC20AndCallContract(ctx, context, ZRC20Contract, to, amount, data) + return res, true, err } - // non-empty message with non-empty data: contract call - context := systemcontract.ZContext{ - Origin: from, - Sender: eth.Address{}, - ChainID: big.NewInt(senderChain.ChainId), + // if the account is a EOC, no contract call can be made with the data + if len(data) > 0 { + return nil, false, types.ErrCallNonContract } - return k.DepositZRC20AndCallContract(ctx, context, ZRC20Contract, contract, amount, data) + res, err := k.DepositZRC20(ctx, ZRC20Contract, to, amount) + return res, false, err } diff --git a/x/fungible/keeper/deposits_test.go b/x/fungible/keeper/deposits_test.go index 5a241d6dfc..2e357cd42c 100644 --- a/x/fungible/keeper/deposits_test.go +++ b/x/fungible/keeper/deposits_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/testutil/contracts" testkeeper "github.com/zeta-chain/zetacore/testutil/keeper" "github.com/zeta-chain/zetacore/testutil/sample" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" @@ -29,19 +30,18 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { // deposit to := sample.EthAddress() - _, err := k.ZRC20DepositAndCallContract( + _, contractCall, err := k.ZRC20DepositAndCallContract( ctx, sample.EthAddress().Bytes(), to, big.NewInt(42), chain, - "", - sample.EthAddress(), []byte{}, common.CoinType_Gas, sample.EthAddress().String(), ) require.NoError(t, err) + require.False(t, contractCall) balance, err := k.BalanceOfZRC4(ctx, zrc20, to) require.NoError(t, err) @@ -62,25 +62,51 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { // deposit to := sample.EthAddress() - _, err := k.ZRC20DepositAndCallContract( + _, contractCall, err := k.ZRC20DepositAndCallContract( ctx, sample.EthAddress().Bytes(), to, big.NewInt(42), chain, - "", - sample.EthAddress(), []byte{}, common.CoinType_ERC20, assetAddress, ) require.NoError(t, err) + require.False(t, contractCall) balance, err := k.BalanceOfZRC4(ctx, zrc20, to) require.NoError(t, err) require.Equal(t, big.NewInt(42), balance) }) + t.Run("should fail if trying to call a contract with data to a EOC", func(t *testing.T) { + k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) + _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) + + chainList := common.DefaultChainsList() + chain := chainList[0] + assetAddress := sample.EthAddress().String() + + // deploy the system contracts + deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) + deployZRC20(t, ctx, k, sdkk.EvmKeeper, chain.ChainId, "foobar", assetAddress, "foobar") + + // deposit + to := sample.EthAddress() + _, _, err := k.ZRC20DepositAndCallContract( + ctx, + sample.EthAddress().Bytes(), + to, + big.NewInt(42), + chain, + []byte("DEADBEEF"), + common.CoinType_ERC20, + assetAddress, + ) + require.ErrorIs(t, err, types.ErrCallNonContract) + }) + t.Run("can deposit coin for transfers with liquidity cap not reached", func(t *testing.T) { // setup gas coin k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) @@ -109,19 +135,18 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { // deposit to := sample.EthAddress() - _, err = k.ZRC20DepositAndCallContract( + _, contractCall, err := k.ZRC20DepositAndCallContract( ctx, sample.EthAddress().Bytes(), to, big.NewInt(500), chain, - "", - sample.EthAddress(), []byte{}, common.CoinType_Gas, sample.EthAddress().String(), ) require.NoError(t, err) + require.False(t, contractCall) balance, err := k.BalanceOfZRC4(ctx, zrc20, to) require.NoError(t, err) @@ -156,14 +181,12 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { // deposit (500 + 501 > 1000) to := sample.EthAddress() - _, err = k.ZRC20DepositAndCallContract( + _, _, err = k.ZRC20DepositAndCallContract( ctx, sample.EthAddress().Bytes(), to, big.NewInt(501), chain, - "", - sample.EthAddress(), []byte{}, common.CoinType_Gas, sample.EthAddress().String(), @@ -184,14 +207,12 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { // deposit to := sample.EthAddress() - _, err := k.ZRC20DepositAndCallContract( + _, _, err := k.ZRC20DepositAndCallContract( ctx, sample.EthAddress().Bytes(), to, big.NewInt(42), chain, - "", - sample.EthAddress(), []byte{}, common.CoinType_Gas, sample.EthAddress().String(), @@ -212,14 +233,12 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { // deposit to := sample.EthAddress() - _, err := k.ZRC20DepositAndCallContract( + _, _, err := k.ZRC20DepositAndCallContract( ctx, sample.EthAddress().Bytes(), to, big.NewInt(42), chain, - "", - sample.EthAddress(), []byte{}, common.CoinType_ERC20, assetAddress, @@ -227,6 +246,76 @@ func TestKeeper_ZRC20DepositAndCallContract(t *testing.T) { require.ErrorIs(t, err, crosschaintypes.ErrForeignCoinNotFound) }) - // TODO: add test cases checking DepositZRC20AndCallContract - // https://github.com/zeta-chain/node/issues/1206 + t.Run("should return contract call if receiver is a contract", func(t *testing.T) { + // setup gas coin + k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) + _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) + + chainList := common.DefaultChainsList() + chain := chainList[0] + + // deploy the system contracts + deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) + zrc20 := setupGasCoin(t, ctx, k, sdkk.EvmKeeper, chain.ChainId, "foobar", "foobar") + + example, err := k.DeployContract(ctx, contracts.ExampleMetaData) + require.NoError(t, err) + assertContractDeployment(t, sdkk.EvmKeeper, ctx, example) + + // deposit + _, contractCall, err := k.ZRC20DepositAndCallContract( + ctx, + sample.EthAddress().Bytes(), + example, + big.NewInt(42), + chain, + []byte{}, + common.CoinType_Gas, + sample.EthAddress().String(), + ) + require.NoError(t, err) + require.True(t, contractCall) + + balance, err := k.BalanceOfZRC4(ctx, zrc20, example) + require.NoError(t, err) + require.Equal(t, big.NewInt(42), balance) + + // check onCrossChainCall() hook was called + assertExampleBarValue(t, ctx, k, example, 42) + }) + + t.Run("should fail if call contract fails", func(t *testing.T) { + // setup gas coin + k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) + _ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) + + chainList := common.DefaultChainsList() + chain := chainList[0] + + // deploy the system contracts + deploySystemContracts(t, ctx, k, sdkk.EvmKeeper) + zrc20 := setupGasCoin(t, ctx, k, sdkk.EvmKeeper, chain.ChainId, "foobar", "foobar") + + reverter, err := k.DeployContract(ctx, contracts.ReverterMetaData) + require.NoError(t, err) + assertContractDeployment(t, sdkk.EvmKeeper, ctx, reverter) + + // deposit + _, contractCall, err := k.ZRC20DepositAndCallContract( + ctx, + sample.EthAddress().Bytes(), + reverter, + big.NewInt(42), + chain, + []byte{}, + common.CoinType_Gas, + sample.EthAddress().String(), + ) + require.Error(t, err) + require.True(t, contractCall) + + balance, err := k.BalanceOfZRC4(ctx, zrc20, reverter) + require.NoError(t, err) + require.EqualValues(t, int64(0), balance.Int64()) + }) } diff --git a/x/fungible/keeper/evm.go b/x/fungible/keeper/evm.go index 92a87cc1d3..f176cf71c3 100644 --- a/x/fungible/keeper/evm.go +++ b/x/fungible/keeper/evm.go @@ -212,7 +212,8 @@ func (k Keeper) DepositZRC20AndCallContract(ctx sdk.Context, zrc20Addr common.Address, targetContract common.Address, amount *big.Int, - message []byte) (*evmtypes.MsgEthereumTxResponse, error) { + message []byte, +) (*evmtypes.MsgEthereumTxResponse, error) { system, found := k.GetSystemContract(ctx) if !found { return nil, cosmoserrors.Wrapf(types.ErrContractNotFound, "GetSystemContract address not found") @@ -224,8 +225,22 @@ func (k Keeper) DepositZRC20AndCallContract(ctx sdk.Context, return nil, err } - return k.CallEVM(ctx, *sysConABI, types.ModuleAddressEVM, systemAddress, BigIntZero, ZEVMGasLimitDepositAndCall, true, false, - "depositAndCall", context, zrc20Addr, amount, targetContract, message) + return k.CallEVM( + ctx, + *sysConABI, + types.ModuleAddressEVM, + systemAddress, + BigIntZero, + ZEVMGasLimitDepositAndCall, + true, + false, + "depositAndCall", + context, + zrc20Addr, + amount, + targetContract, + message, + ) } // QueryWithdrawGasFee returns the gas fee for a withdrawal transaction associated with a given zrc20 diff --git a/x/fungible/keeper/evm_test.go b/x/fungible/keeper/evm_test.go index 63559fc249..2d5be1e8a8 100644 --- a/x/fungible/keeper/evm_test.go +++ b/x/fungible/keeper/evm_test.go @@ -15,8 +15,10 @@ import ( zetacommon "github.com/zeta-chain/zetacore/common" "github.com/zeta-chain/zetacore/server/config" + "github.com/zeta-chain/zetacore/testutil/contracts" testkeeper "github.com/zeta-chain/zetacore/testutil/keeper" "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/x/fungible/keeper" fungiblekeeper "github.com/zeta-chain/zetacore/x/fungible/keeper" "github.com/zeta-chain/zetacore/x/fungible/types" ) @@ -76,6 +78,35 @@ func deploySystemContracts( return } +// assertExampleBarValue asserts value Bar of the contract Example, used to test onCrossChainCall +func assertExampleBarValue( + t *testing.T, + ctx sdk.Context, + k *keeper.Keeper, + address common.Address, + expected int64, +) { + exampleABI, err := contracts.ExampleMetaData.GetAbi() + require.NoError(t, err) + res, err := k.CallEVM( + ctx, + *exampleABI, + types.ModuleAddressEVM, + address, + big.NewInt(0), + nil, + false, + false, + "bar", + ) + unpacked, err := exampleABI.Unpack("bar", res.Ret) + require.NoError(t, err) + require.NotZero(t, len(unpacked)) + bar, ok := unpacked[0].(*big.Int) + require.True(t, ok) + require.Equal(t, big.NewInt(expected), bar) +} + func TestKeeper_DeployZRC20Contract(t *testing.T) { t.Run("can deploy the zrc20 contract", func(t *testing.T) { k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) diff --git a/x/fungible/types/errors.go b/x/fungible/types/errors.go index 262fca0b68..6bfec4c107 100644 --- a/x/fungible/types/errors.go +++ b/x/fungible/types/errors.go @@ -32,4 +32,5 @@ var ( ErrPausedZRC20 = sdkerrors.Register(ModuleName, 1121, "ZRC20 is paused") ErrForeignCoinNotFound = sdkerrors.Register(ModuleName, 1122, "foreign coin not found") ErrForeignCoinCapReached = sdkerrors.Register(ModuleName, 1123, "foreign coin cap reached") + ErrCallNonContract = sdkerrors.Register(ModuleName, 1124, "can't call a non-contract address") )