From d06485f12db28fcc81ef094fb4aa61a3e61d08a1 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 10 Sep 2024 18:56:55 +0200 Subject: [PATCH 01/33] feat: bank precompile --- app/app.go | 1 + precompiles/bank/IBank.abi | 74 ++++++++++ precompiles/bank/IBank.go | 254 ++++++++++++++++++++++++++++++++ precompiles/bank/IBank.json | 76 ++++++++++ precompiles/bank/IBank.sol | 43 ++++++ precompiles/bank/bank.go | 193 ++++++++++++++++++++++++ precompiles/bank/bindings.go | 7 + precompiles/precompiles.go | 13 ++ precompiles/precompiles_test.go | 2 +- precompiles/types/errors.go | 23 ++- 10 files changed, 682 insertions(+), 4 deletions(-) create mode 100644 precompiles/bank/IBank.abi create mode 100644 precompiles/bank/IBank.go create mode 100644 precompiles/bank/IBank.json create mode 100644 precompiles/bank/IBank.sol create mode 100644 precompiles/bank/bank.go create mode 100644 precompiles/bank/bindings.go diff --git a/app/app.go b/app/app.go index 7c4186af5c..cc271fc35f 100644 --- a/app/app.go +++ b/app/app.go @@ -570,6 +570,7 @@ func New( precompiles.StatefulContracts( &app.FungibleKeeper, app.StakingKeeper, + app.BankKeeper, appCodec, storetypes.TransientGasConfig(), ), diff --git a/precompiles/bank/IBank.abi b/precompiles/bank/IBank.abi new file mode 100644 index 0000000000..03eee6d210 --- /dev/null +++ b/precompiles/bank/IBank.abi @@ -0,0 +1,74 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/precompiles/bank/IBank.go b/precompiles/bank/IBank.go new file mode 100644 index 0000000000..8f95c51c08 --- /dev/null +++ b/precompiles/bank/IBank.go @@ -0,0 +1,254 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bank + +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 +) + +// IBankMetaData contains all meta data concerning the IBank contract. +var IBankMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// IBankABI is the input ABI used to generate the binding from. +// Deprecated: Use IBankMetaData.ABI instead. +var IBankABI = IBankMetaData.ABI + +// IBank is an auto generated Go binding around an Ethereum contract. +type IBank struct { + IBankCaller // Read-only binding to the contract + IBankTransactor // Write-only binding to the contract + IBankFilterer // Log filterer for contract events +} + +// IBankCaller is an auto generated read-only Go binding around an Ethereum contract. +type IBankCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IBankTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IBankTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IBankFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IBankFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IBankSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IBankSession struct { + Contract *IBank // 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 +} + +// IBankCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IBankCallerSession struct { + Contract *IBankCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IBankTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IBankTransactorSession struct { + Contract *IBankTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IBankRaw is an auto generated low-level Go binding around an Ethereum contract. +type IBankRaw struct { + Contract *IBank // Generic contract binding to access the raw methods on +} + +// IBankCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IBankCallerRaw struct { + Contract *IBankCaller // Generic read-only contract binding to access the raw methods on +} + +// IBankTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IBankTransactorRaw struct { + Contract *IBankTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIBank creates a new instance of IBank, bound to a specific deployed contract. +func NewIBank(address common.Address, backend bind.ContractBackend) (*IBank, error) { + contract, err := bindIBank(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IBank{IBankCaller: IBankCaller{contract: contract}, IBankTransactor: IBankTransactor{contract: contract}, IBankFilterer: IBankFilterer{contract: contract}}, nil +} + +// NewIBankCaller creates a new read-only instance of IBank, bound to a specific deployed contract. +func NewIBankCaller(address common.Address, caller bind.ContractCaller) (*IBankCaller, error) { + contract, err := bindIBank(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IBankCaller{contract: contract}, nil +} + +// NewIBankTransactor creates a new write-only instance of IBank, bound to a specific deployed contract. +func NewIBankTransactor(address common.Address, transactor bind.ContractTransactor) (*IBankTransactor, error) { + contract, err := bindIBank(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IBankTransactor{contract: contract}, nil +} + +// NewIBankFilterer creates a new log filterer instance of IBank, bound to a specific deployed contract. +func NewIBankFilterer(address common.Address, filterer bind.ContractFilterer) (*IBankFilterer, error) { + contract, err := bindIBank(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IBankFilterer{contract: contract}, nil +} + +// bindIBank binds a generic wrapper to an already deployed contract. +func bindIBank(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IBankMetaData.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 (_IBank *IBankRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IBank.Contract.IBankCaller.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 (_IBank *IBankRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IBank.Contract.IBankTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IBank *IBankRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IBank.Contract.IBankTransactor.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 (_IBank *IBankCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IBank.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 (_IBank *IBankTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IBank.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IBank *IBankTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IBank.Contract.contract.Transact(opts, method, params...) +} + +// BalanceOf is a free data retrieval call binding the contract method 0xf7888aec. +// +// Solidity: function balanceOf(address zrc20, address user) view returns(uint256 balance) +func (_IBank *IBankCaller) BalanceOf(opts *bind.CallOpts, zrc20 common.Address, user common.Address) (*big.Int, error) { + var out []interface{} + err := _IBank.contract.Call(opts, &out, "balanceOf", zrc20, user) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOf is a free data retrieval call binding the contract method 0xf7888aec. +// +// Solidity: function balanceOf(address zrc20, address user) view returns(uint256 balance) +func (_IBank *IBankSession) BalanceOf(zrc20 common.Address, user common.Address) (*big.Int, error) { + return _IBank.Contract.BalanceOf(&_IBank.CallOpts, zrc20, user) +} + +// BalanceOf is a free data retrieval call binding the contract method 0xf7888aec. +// +// Solidity: function balanceOf(address zrc20, address user) view returns(uint256 balance) +func (_IBank *IBankCallerSession) BalanceOf(zrc20 common.Address, user common.Address) (*big.Int, error) { + return _IBank.Contract.BalanceOf(&_IBank.CallOpts, zrc20, user) +} + +// Deposit is a paid mutator transaction binding the contract method 0x47e7ef24. +// +// Solidity: function deposit(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankTransactor) Deposit(opts *bind.TransactOpts, zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.contract.Transact(opts, "deposit", zrc20, amount) +} + +// Deposit is a paid mutator transaction binding the contract method 0x47e7ef24. +// +// Solidity: function deposit(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankSession) Deposit(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.Contract.Deposit(&_IBank.TransactOpts, zrc20, amount) +} + +// Deposit is a paid mutator transaction binding the contract method 0x47e7ef24. +// +// Solidity: function deposit(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankTransactorSession) Deposit(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.Contract.Deposit(&_IBank.TransactOpts, zrc20, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0xf3fef3a3. +// +// Solidity: function withdraw(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankTransactor) Withdraw(opts *bind.TransactOpts, zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.contract.Transact(opts, "withdraw", zrc20, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0xf3fef3a3. +// +// Solidity: function withdraw(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankSession) Withdraw(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.Contract.Withdraw(&_IBank.TransactOpts, zrc20, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0xf3fef3a3. +// +// Solidity: function withdraw(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankTransactorSession) Withdraw(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.Contract.Withdraw(&_IBank.TransactOpts, zrc20, amount) +} diff --git a/precompiles/bank/IBank.json b/precompiles/bank/IBank.json new file mode 100644 index 0000000000..808e0039c0 --- /dev/null +++ b/precompiles/bank/IBank.json @@ -0,0 +1,76 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} diff --git a/precompiles/bank/IBank.sol b/precompiles/bank/IBank.sol new file mode 100644 index 0000000000..7926d6e695 --- /dev/null +++ b/precompiles/bank/IBank.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +/// @title IBank Interface for Cross-chain Token Deposits and Withdrawals +/// @notice This interface defines the functions for depositing ZRC20 tokens and withdrawing Cosmos tokens, +/// as well as querying the balance of Cosmos tokens corresponding to a given ZRC20 token. +/// @dev This contract interacts with a precompiled contract at a fixed address. + +/// @dev The IBank contract's precompiled address. +address constant IBANK_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000067; // Address 103 + +/// @dev The IBank contract instance using the precompiled address. +IBank constant IBANK_CONTRACT = IBank(IBANK_PRECOMPILE_ADDRESS); + +/// @dev Interface for the IBank contract. +interface IBank { + /// @notice Deposit a ZRC20 token and mint the corresponding Cosmos token to the user's account. + /// @param zrc20 The ZRC20 token address to be deposited. + /// @param amount The amount of ZRC20 tokens to deposit. + /// @return success Boolean indicating whether the deposit was successful. + function deposit( + address zrc20, + uint256 amount + ) external returns (bool success); + + /// @notice Withdraw Cosmos tokens and convert them back to the corresponding ZRC20 token for the user. + /// @param zrc20 The ZRC20 token address for the corresponding Cosmos token. + /// @param amount The amount of Cosmos tokens to withdraw. + /// @return success Boolean indicating whether the withdrawal was successful. + function withdraw( + address zrc20, + uint256 amount + ) external returns (bool success); + + /// @notice Retrieve the Cosmos token balance corresponding to a specific ZRC20 token for a given user. + /// @param zrc20 The ZRC20 cosmos token denomination to check the balance for. + /// @param user The address of the user to retrieve the balance for. + /// @return balance The balance of the Cosmos token for the specified ZRC20 token and user. + function balanceOf( + address zrc20, + address user + ) external view returns (uint256 balance); +} diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go new file mode 100644 index 0000000000..279dc81378 --- /dev/null +++ b/precompiles/bank/bank.go @@ -0,0 +1,193 @@ +package bank + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + ptypes "github.com/zeta-chain/node/precompiles/types" +) + +const ( + // Write methods. + DepositMethodName = "deposit" + WithdrawMethodName = "withdraw" + + // Read methods. + BalanceOfMethodName = "balanceOf" +) + +var ( + ABI abi.ABI + ContractAddress = common.HexToAddress("0x0000000000000000000000000000000000000067") + GasRequiredByMethod = map[[4]byte]uint64{} + ViewMethod = map[[4]byte]bool{} +) + +func init() { + initABI() +} + +func initABI() { + if err := ABI.UnmarshalJSON([]byte(IBankMetaData.ABI)); err != nil { + panic(err) + } + + GasRequiredByMethod = map[[4]byte]uint64{} + for methodName := range ABI.Methods { + var methodID [4]byte + copy(methodID[:], ABI.Methods[methodName].ID[:4]) + switch methodName { + case DepositMethodName: + GasRequiredByMethod[methodID] = 200000 + case WithdrawMethodName: + GasRequiredByMethod[methodID] = 200000 + case BalanceOfMethodName: + GasRequiredByMethod[methodID] = 10000 + default: + GasRequiredByMethod[methodID] = 0 + } + } +} + +type Contract struct { + ptypes.BaseContract + + bankKeeper bank.Keeper + cdc codec.Codec + kvGasConfig storetypes.GasConfig +} + +func NewIBankContract( + bankKeeper bank.Keeper, + cdc codec.Codec, + kvGasConfig storetypes.GasConfig, +) *Contract { + return &Contract{ + BaseContract: ptypes.NewBaseContract(ContractAddress), + bankKeeper: bankKeeper, + cdc: cdc, + kvGasConfig: kvGasConfig, + } +} + +// Address() is required to implement the PrecompiledContract interface. +func (c *Contract) Address() common.Address { + return ContractAddress +} + +// Abi() is required to implement the PrecompiledContract interface. +func (c *Contract) Abi() abi.ABI { + return ABI +} + +// RequiredGas is required to implement the PrecompiledContract interface. +// The gas has to be calculated deterministically based on the input. +func (c *Contract) RequiredGas(input []byte) uint64 { + // get methodID (first 4 bytes) + var methodID [4]byte + copy(methodID[:], input[:4]) + // base cost to prevent large input size + baseCost := uint64(len(input)) * c.kvGasConfig.WriteCostPerByte + if ViewMethod[methodID] { + baseCost = uint64(len(input)) * c.kvGasConfig.ReadCostPerByte + } + + if requiredGas, ok := GasRequiredByMethod[methodID]; ok { + return requiredGas + baseCost + } + + // Can not happen, but return 0 if the method is not found. + return 0 +} + +// Run is the entrypoint of the precompiled contract, it switches over the input method, +// and execute them accordingly. +func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byte, error) { + method, err := ABI.MethodById(contract.Input[:4]) + if err != nil { + return nil, err + } + + args, err := method.Inputs.Unpack(contract.Input[4:]) + if err != nil { + return nil, err + } + + stateDB := evm.StateDB.(ptypes.ExtStateDB) + + switch method.Name { + case DepositMethodName: + if readOnly { + return nil, nil + } + + return nil, nil + // TODO + + case WithdrawMethodName: + if readOnly { + return nil, nil + } + + return nil, nil + // TODO + + case BalanceOfMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.balanceOf(ctx, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + + default: + return nil, ptypes.ErrInvalidMethod{ + Method: method.Name, + } + } +} + +func (c *Contract) balanceOf( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) (result []byte, err error) { + if len(args) != 2 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + + tokenAddr, addr := args[0].(common.Address), args[1].(common.Address) + + accAddr, err := sdk.AccAddressFromHexUnsafe(addr.String()) + if err != nil { + return nil, err + } + + // TODO: Create a function to handle token denoms. + tokenDenom := fmt.Sprintf("evm/%s", tokenAddr.String()) + + coin := c.bankKeeper.GetBalance(ctx, accAddr, tokenDenom) + + if !coin.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coin.GetDenom(), + Negative: coin.IsNegative(), + Nil: coin.IsNil(), + } + } + + return method.Outputs.Pack(coin.Amount) +} diff --git a/precompiles/bank/bindings.go b/precompiles/bank/bindings.go new file mode 100644 index 0000000000..d127657566 --- /dev/null +++ b/precompiles/bank/bindings.go @@ -0,0 +1,7 @@ +//go:generate sh -c "solc IBank.sol --combined-json abi | jq '.contracts.\"IBank.sol:IBank\"' > IBank.json" +//go:generate sh -c "cat IBank.json | jq .abi > IBank.abi" +//go:generate sh -c "abigen --abi IBank.abi --pkg bank --type IBank --out IBank.go" + +package bank + +var _ Contract diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index 1adf65005f..ff4ca288ac 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -4,12 +4,14 @@ import ( "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdktypes "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" ethparams "github.com/ethereum/go-ethereum/params" evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" + "github.com/zeta-chain/node/precompiles/bank" "github.com/zeta-chain/node/precompiles/prototype" "github.com/zeta-chain/node/precompiles/staking" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" @@ -21,12 +23,14 @@ import ( var EnabledStatefulContracts = map[common.Address]bool{ prototype.ContractAddress: true, staking.ContractAddress: true, + bank.ContractAddress: true, } // StatefulContracts returns all the registered precompiled contracts. func StatefulContracts( fungibleKeeper *fungiblekeeper.Keeper, stakingKeeper *stakingkeeper.Keeper, + bankKeeper bankkeeper.Keeper, cdc codec.Codec, gasConfig storetypes.GasConfig, ) (precompiledContracts []evmkeeper.CustomContractFn) { @@ -53,5 +57,14 @@ func StatefulContracts( precompiledContracts = append(precompiledContracts, stakingContract) } + if EnabledStatefulContracts[bank.ContractAddress] { + bankContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { + return bank.NewIBankContract(bankKeeper, cdc, gasConfig) + } + + // Append the staking contract to the precompiledContracts slice. + precompiledContracts = append(precompiledContracts, bankContract) + } + return precompiledContracts } diff --git a/precompiles/precompiles_test.go b/precompiles/precompiles_test.go index 998240e644..a0b55572ea 100644 --- a/precompiles/precompiles_test.go +++ b/precompiles/precompiles_test.go @@ -25,7 +25,7 @@ func Test_StatefulContracts(t *testing.T) { } // StatefulContracts() should return all the enabled contracts. - contracts := StatefulContracts(k, &sdkk.StakingKeeper, appCodec, gasConfig) + contracts := StatefulContracts(k, &sdkk.StakingKeeper, sdkk.BankKeeper, appCodec, gasConfig) require.NotNil(t, contracts, "StatefulContracts() should not return a nil slice") require.Len(t, contracts, expectedContracts, "StatefulContracts() should return all the enabled contracts") diff --git a/precompiles/types/errors.go b/precompiles/types/errors.go index 0cc6928541..f6df4542ee 100644 --- a/precompiles/types/errors.go +++ b/precompiles/types/errors.go @@ -3,8 +3,9 @@ package types import "fmt" /* -Address related errors + Address related errors */ + type ErrInvalidAddr struct { Got string } @@ -14,8 +15,9 @@ func (e ErrInvalidAddr) Error() string { } /* -Argument related errors + Argument related errors */ + type ErrInvalidNumberOfArgs struct { Got, Expect int } @@ -33,8 +35,23 @@ func (e ErrInvalidArgument) Error() string { } /* -Method related errors + Coin related errors +*/ + +type ErrInvalidCoin struct { + Got string + Negative bool + Nil bool +} + +func (e ErrInvalidCoin) Error() string { + return fmt.Sprintf("invalid coin: denom: %s, is negative: %v, is nil: %v", e.Got, e.Negative, e.Nil) +} + +/* + Method related errors */ + type ErrInvalidMethod struct { Method string } From 969d2d236e2ceae9e35f44515ddce839518c4ff7 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 12:42:22 +0200 Subject: [PATCH 02/33] feat: add deposit --- precompiles/bank/bank.go | 125 +++++++++++++++++++++++++++++++++--- precompiles/types/errors.go | 14 +++- 2 files changed, 127 insertions(+), 12 deletions(-) diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 279dc81378..9510017411 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -1,8 +1,9 @@ package bank import ( - "fmt" + "math/big" + "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,11 +11,15 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" + "github.com/zeta-chain/ethermint/x/evm/types" ptypes "github.com/zeta-chain/node/precompiles/types" ) const ( + // ZEVM cosmos coins prefix. + ZEVMDenom = "zevm/" + // Write methods. DepositMethodName = "deposit" WithdrawMethodName = "withdraw" @@ -128,8 +133,15 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return nil, nil } - return nil, nil - // TODO + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.deposit(ctx, method, contract.CallerAddress, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil case WithdrawMethodName: if readOnly { @@ -157,6 +169,94 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt } } +func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { + return ZEVMDenom + ZRC20Address.String() +} + +func (c *Contract) deposit( + ctx sdk.Context, + method *abi.Method, + caller common.Address, + args []interface{}, +) (result []byte, err error) { + if len(args) != 2 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + + // function deposit(address zrc20, uint256 amount) external returns (bool success); + ZRC20Addr, amount := args[0].(common.Address), args[1].(*big.Int) + + // Handle the toAddr: + // check it's valid and not blocked. + toAddr := sdk.AccAddress(caller.Bytes()) + if toAddr.Empty() { + return nil, &ptypes.ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "empty address", + } + } + + if c.bankKeeper.BlockedAddr(toAddr) { + return nil, &ptypes.ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "blocked by bank keeper", + } + } + + // The process of creating a new cosmos coin is: + // - Generate the new coin denom using ZRC20 address, + // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". + // - Mint coins. + // - Send coins to the caller. + tokenDenom := ZRC20ToCosmosDenom(ZRC20Addr) + coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) + if !coin.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coin.GetDenom(), + Negative: coin.IsNegative(), + Nil: coin.IsNil(), + } + } + + // A sdk.Coins (type []sdk.Coin) has to be created because it's the type expected by MintCoins + // and SendCoinsFromModuleToAccount. + // But sdk.Coins will only contain one coin, always. + coinSet := sdk.NewCoins(coin) + if !coinSet.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coinSet.Sort().GetDenomByIndex(0), + Negative: coinSet.IsAnyNegative(), + Nil: coinSet.IsAnyNil(), + } + } + + if !c.bankKeeper.IsSendEnabledCoin(ctx, coin) { + return nil, &ptypes.ErrUnexpected{ + When: "IsSendEnabledCoins", + Got: "coin not enabled to be sent", + } + } + + if err := c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "MintCoins", + Got: err.Error(), + } + } + + if err := c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "SendCoinsFromModuleToAccount", + Got: err.Error(), + } + } + + return method.Outputs.Pack(true) +} + func (c *Contract) balanceOf( ctx sdk.Context, method *abi.Method, @@ -169,18 +269,23 @@ func (c *Contract) balanceOf( }) } + // function balanceOf(address zrc20, address user) external view returns (uint256 balance); tokenAddr, addr := args[0].(common.Address), args[1].(common.Address) - accAddr, err := sdk.AccAddressFromHexUnsafe(addr.String()) - if err != nil { - return nil, err + // common.Address has to be converted to AccAddress. + accAddr := sdk.AccAddress(addr.Bytes()) + if accAddr.Empty() { + return nil, &ptypes.ErrInvalidAddr{ + Got: accAddr.String(), + } } - // TODO: Create a function to handle token denoms. - tokenDenom := fmt.Sprintf("evm/%s", tokenAddr.String()) + // Convert ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". + tokenDenom := ZRC20ToCosmosDenom(tokenAddr) + // Bank Keeper GetBalance returns the specified Cosmos coin balance for a given address. + // Check explicitly the balance is a non-negative non-nil value. coin := c.bankKeeper.GetBalance(ctx, accAddr, tokenDenom) - if !coin.IsValid() { return nil, &ptypes.ErrInvalidCoin{ Got: coin.GetDenom(), @@ -189,5 +294,5 @@ func (c *Contract) balanceOf( } } - return method.Outputs.Pack(coin.Amount) + return method.Outputs.Pack(coin.Amount.BigInt()) } diff --git a/precompiles/types/errors.go b/precompiles/types/errors.go index f6df4542ee..a4e8a86b0b 100644 --- a/precompiles/types/errors.go +++ b/precompiles/types/errors.go @@ -7,11 +7,12 @@ import "fmt" */ type ErrInvalidAddr struct { - Got string + Got string + Reason string } func (e ErrInvalidAddr) Error() string { - return fmt.Sprintf("invalid address %s", e.Got) + return fmt.Sprintf("invalid address %s, reason: %s", e.Got, e.Reason) } /* @@ -59,3 +60,12 @@ type ErrInvalidMethod struct { func (e ErrInvalidMethod) Error() string { return fmt.Sprintf("invalid method: %s", e.Method) } + +type ErrUnexpected struct { + When string + Got string +} + +func (e ErrUnexpected) Error() string { + return fmt.Sprintf("unexpected error in %s: %s", e.When, e.Got) +} From 71aa8d5cdcd61c00c29bd88d164808b42c692744 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 13:10:01 +0200 Subject: [PATCH 03/33] feat: extend deposit --- precompiles/bank/bank.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 9510017411..39bb629831 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -186,6 +186,11 @@ func (c *Contract) deposit( }) } + // TODO: The origin tokens have to be: + // 1. Checked the caller has the right amount of original tokens. + // 2. burned or locked. + // Otherwise this deposit functions has the ability to infinite mint coins. + // function deposit(address zrc20, uint256 amount) external returns (bool success); ZRC20Addr, amount := args[0].(common.Address), args[1].(*big.Int) From 322618bb2e6f15b54af33c1747ac75d26a86c407 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 14:23:51 +0200 Subject: [PATCH 04/33] PoC: spend amount on behalf of EOA --- precompiles/bank/bank.go | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 39bb629831..af3649101f 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -9,11 +9,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" "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/vm" - "github.com/zeta-chain/ethermint/x/evm/types" - + "github.com/ethereum/go-ethereum/ethclient" ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/x/fungible/types" + zrc20 "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" ) const ( @@ -65,18 +67,21 @@ type Contract struct { ptypes.BaseContract bankKeeper bank.Keeper + ZEVMClient *ethclient.Client cdc codec.Codec kvGasConfig storetypes.GasConfig } func NewIBankContract( bankKeeper bank.Keeper, + ZEVMClient *ethclient.Client, cdc codec.Codec, kvGasConfig storetypes.GasConfig, ) *Contract { return &Contract{ BaseContract: ptypes.NewBaseContract(ContractAddress), bankKeeper: bankKeeper, + ZEVMClient: ZEVMClient, cdc: cdc, kvGasConfig: kvGasConfig, } @@ -194,6 +199,31 @@ func (c *Contract) deposit( // function deposit(address zrc20, uint256 amount) external returns (bool success); ZRC20Addr, amount := args[0].(common.Address), args[1].(*big.Int) + + // Does ZRC20 exist? + ZRC20, err := zrc20.NewZRC20Caller(ZRC20Addr, c.ZEVMClient) + if err != nil { + return nil, err + } + + // Is caller balance greater or equal to the amount it wants to spend? + balance, err := ZRC20.BalanceOf(&bind.CallOpts{Context: ctx}, caller) + if err != nil { + return nil, err + } + + if balance.Cmp(amount) < 0 { + return nil, err + } + + // Is the bank allowed to spend the specified amount? + allowance, err := ZRC20.Allowance(&bind.CallOpts{Context: ctx}, caller, ContractAddress) + if allowance.Cmp(amount) < 0 { + return nil, err + } + + // TODO: lock or burn + // Handle the toAddr: // check it's valid and not blocked. toAddr := sdk.AccAddress(caller.Bytes()) From 7e859253e58c97eef9c3ce39d3805dbdc899a67c Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 15:16:26 +0200 Subject: [PATCH 05/33] feat: expand deposit with transferFrom --- precompiles/bank/bank.go | 56 ++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index af3649101f..181740d9f7 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -1,6 +1,7 @@ package bank import ( + "fmt" "math/big" "cosmossdk.io/math" @@ -184,6 +185,9 @@ func (c *Contract) deposit( caller common.Address, args []interface{}, ) (result []byte, err error) { + // This function is developed using the + // Check - Effects - Interactions pattern: + // 1. Check everything is correct. if len(args) != 2 { return nil, &(ptypes.ErrInvalidNumberOfArgs{ Got: len(args), @@ -191,39 +195,40 @@ func (c *Contract) deposit( }) } - // TODO: The origin tokens have to be: - // 1. Checked the caller has the right amount of original tokens. - // 2. burned or locked. - // Otherwise this deposit functions has the ability to infinite mint coins. - // function deposit(address zrc20, uint256 amount) external returns (bool success); ZRC20Addr, amount := args[0].(common.Address), args[1].(*big.Int) - - // Does ZRC20 exist? - ZRC20, err := zrc20.NewZRC20Caller(ZRC20Addr, c.ZEVMClient) + ZRC20, err := zrc20.NewZRC20(ZRC20Addr, c.ZEVMClient) if err != nil { - return nil, err + return nil, &ptypes.ErrUnexpected{ + When: "NewZRC20", + Got: err.Error(), + } } - // Is caller balance greater or equal to the amount it wants to spend? balance, err := ZRC20.BalanceOf(&bind.CallOpts{Context: ctx}, caller) if err != nil { - return nil, err + return nil, &ptypes.ErrUnexpected{ + When: "BalanceOf", + Got: err.Error(), + } } if balance.Cmp(amount) < 0 { - return nil, err + return nil, &ptypes.ErrUnexpected{ + When: "Balance0f", + Got: "not enough balance", + } } - // Is the bank allowed to spend the specified amount? allowance, err := ZRC20.Allowance(&bind.CallOpts{Context: ctx}, caller, ContractAddress) if allowance.Cmp(amount) < 0 { - return nil, err + return nil, &ptypes.ErrUnexpected{ + When: "Allowance", + Got: "not enough allowance", + } } - // TODO: lock or burn - // Handle the toAddr: // check it's valid and not blocked. toAddr := sdk.AccAddress(caller.Bytes()) @@ -275,6 +280,25 @@ func (c *Contract) deposit( } } + // 2. Effect: subtract balance. + tx, err := ZRC20.TransferFrom(&bind.TransactOpts{Context: ctx}, caller, ContractAddress, amount) + if err != nil { + return nil, err + } + + r, err := bind.WaitMined(ctx, c.ZEVMClient, tx) + if err != nil { + return nil, err + } + + if r.Status != 1 { + return nil, &ptypes.ErrUnexpected{ + When: "ZRC20 transaction failed", + Got: fmt.Sprintf("from: %s, to: %s", caller, ContractAddress), + } + } + + // 3. Interactions: create cosmos coin and send. if err := c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet); err != nil { return nil, &ptypes.ErrUnexpected{ When: "MintCoins", From 5fe30cf13eba549343a0a4553dce4c52138e98c2 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 17:15:34 +0200 Subject: [PATCH 06/33] use CallEVM instead on ZRC20 bindings --- precompiles/bank/bank.go | 138 ++++++++++++++++++++++++++++--------- precompiles/precompiles.go | 2 +- 2 files changed, 105 insertions(+), 35 deletions(-) diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 181740d9f7..ba3bd305a3 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -1,7 +1,6 @@ package bank import ( - "fmt" "math/big" "cosmossdk.io/math" @@ -10,13 +9,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" "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/vm" - "github.com/ethereum/go-ethereum/ethclient" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" + ptypes "github.com/zeta-chain/node/precompiles/types" + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" "github.com/zeta-chain/node/x/fungible/types" - zrc20 "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" ) const ( @@ -67,24 +66,24 @@ func initABI() { type Contract struct { ptypes.BaseContract - bankKeeper bank.Keeper - ZEVMClient *ethclient.Client - cdc codec.Codec - kvGasConfig storetypes.GasConfig + bankKeeper bank.Keeper + fungibleKeeper fungiblekeeper.Keeper + cdc codec.Codec + kvGasConfig storetypes.GasConfig } func NewIBankContract( bankKeeper bank.Keeper, - ZEVMClient *ethclient.Client, + fungibleKeeper fungiblekeeper.Keeper, cdc codec.Codec, kvGasConfig storetypes.GasConfig, ) *Contract { return &Contract{ - BaseContract: ptypes.NewBaseContract(ContractAddress), - bankKeeper: bankKeeper, - ZEVMClient: ZEVMClient, - cdc: cdc, - kvGasConfig: kvGasConfig, + BaseContract: ptypes.NewBaseContract(ContractAddress), + bankKeeper: bankKeeper, + fungibleKeeper: fungibleKeeper, + cdc: cdc, + kvGasConfig: kvGasConfig, } } @@ -198,33 +197,48 @@ func (c *Contract) deposit( // function deposit(address zrc20, uint256 amount) external returns (bool success); ZRC20Addr, amount := args[0].(common.Address), args[1].(*big.Int) - ZRC20, err := zrc20.NewZRC20(ZRC20Addr, c.ZEVMClient) + // Initialize the ZRC20 ABI, as we need to call the balanceOf and allowance methods. + ZRC20ABI, err := zrc20.ZRC20MetaData.GetAbi() if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "NewZRC20", - Got: err.Error(), - } + return nil, err } - balance, err := ZRC20.BalanceOf(&bind.CallOpts{Context: ctx}, caller) + // Check for enough balance. + // function balanceOf(address account) public view virtual override returns (uint256) + argsBalanceOf := []interface{}{caller} + + resBalanceOf, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "balanceOf", argsBalanceOf) if err != nil { return nil, &ptypes.ErrUnexpected{ - When: "BalanceOf", + When: "CallContract", Got: err.Error(), } } + balance := resBalanceOf[0].(*big.Int) if balance.Cmp(amount) < 0 { return nil, &ptypes.ErrUnexpected{ - When: "Balance0f", + When: "balance0f", Got: "not enough balance", } } - allowance, err := ZRC20.Allowance(&bind.CallOpts{Context: ctx}, caller, ContractAddress) + // Check for enough allowance. + // function allowance(address owner, address spender) public view virtual override returns (uint256) + argsAllowance := []interface{}{caller, ContractAddress} + + resAllowance, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "allowance", argsAllowance) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "CallContract", + Got: err.Error(), + } + } + + allowance := resAllowance[0].(*big.Int) if allowance.Cmp(amount) < 0 { return nil, &ptypes.ErrUnexpected{ - When: "Allowance", + When: "allowance", Got: "not enough allowance", } } @@ -281,21 +295,23 @@ func (c *Contract) deposit( } // 2. Effect: subtract balance. - tx, err := ZRC20.TransferFrom(&bind.TransactOpts{Context: ctx}, caller, ContractAddress, amount) - if err != nil { - return nil, err - } + // function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) + argsTransferFrom := []interface{}{caller, ContractAddress, amount} - r, err := bind.WaitMined(ctx, c.ZEVMClient, tx) + resTransferFrom, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "transferFrom", argsTransferFrom) if err != nil { - return nil, err + return nil, &ptypes.ErrUnexpected{ + When: "CallContract", + Got: err.Error(), + } } - if r.Status != 1 { + transferred := resTransferFrom[0].(bool) + if !transferred { return nil, &ptypes.ErrUnexpected{ - When: "ZRC20 transaction failed", - Got: fmt.Sprintf("from: %s, to: %s", caller, ContractAddress), - } + When: "TransferFrom", + Got: "transfer not successful", + } } // 3. Interactions: create cosmos coin and send. @@ -355,3 +371,57 @@ func (c *Contract) balanceOf( return method.Outputs.Pack(coin.Amount.BigInt()) } + +// CallContract calls a given contract on behalf of the precompiled contract. +// Note that the precompile contract address is hardcoded. +func (c *Contract) CallContract( + ctx sdk.Context, + abi *abi.ABI, + dst common.Address, + method string, + args []interface{}, +) ([]interface{}, error) { + input, err := abi.Methods[method].Inputs.Pack(args) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "Pack " + method, + Got: err.Error(), + } + } + + res, err := c.fungibleKeeper.CallEVM( + ctx, + *abi, + ContractAddress, + dst, + big.NewInt(0), + nil, + true, + false, + method, + input, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "CallEVM " + method, + Got: err.Error(), + } + } + + if res.VmError != "" { + return nil, &ptypes.ErrUnexpected{ + When: "VmError " + method, + Got: res.VmError, + } + } + + ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "Unpack " + method, + Got: err.Error(), + } + } + + return ret, nil +} diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index ff4ca288ac..b67df7e76c 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -59,7 +59,7 @@ func StatefulContracts( if EnabledStatefulContracts[bank.ContractAddress] { bankContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { - return bank.NewIBankContract(bankKeeper, cdc, gasConfig) + return bank.NewIBankContract(bankKeeper, *fungibleKeeper, cdc, gasConfig) } // Append the staking contract to the precompiledContracts slice. From f69c6950e8cbdd833d61d412948bbea035f2c6fc Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 17:42:13 +0200 Subject: [PATCH 07/33] divide the contract into different files --- precompiles/bank/bank.go | 277 +-------------------------- precompiles/bank/call_contract.go | 65 +++++++ precompiles/bank/coin.go | 7 + precompiles/bank/const.go | 20 ++ precompiles/bank/method_balanceof.go | 49 +++++ precompiles/bank/method_deposit.go | 176 +++++++++++++++++ precompiles/types/errors.go | 10 +- 7 files changed, 330 insertions(+), 274 deletions(-) create mode 100644 precompiles/bank/call_contract.go create mode 100644 precompiles/bank/coin.go create mode 100644 precompiles/bank/const.go create mode 100644 precompiles/bank/method_balanceof.go create mode 100644 precompiles/bank/method_deposit.go diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index ba3bd305a3..fae3d8fbec 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -1,9 +1,6 @@ package bank import ( - "math/big" - - "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -11,23 +8,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" ptypes "github.com/zeta-chain/node/precompiles/types" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" - "github.com/zeta-chain/node/x/fungible/types" -) - -const ( - // ZEVM cosmos coins prefix. - ZEVMDenom = "zevm/" - - // Write methods. - DepositMethodName = "deposit" - WithdrawMethodName = "withdraw" - - // Read methods. - BalanceOfMethodName = "balanceOf" ) var ( @@ -52,13 +35,13 @@ func initABI() { copy(methodID[:], ABI.Methods[methodName].ID[:4]) switch methodName { case DepositMethodName: - GasRequiredByMethod[methodID] = 200000 + GasRequiredByMethod[methodID] = DepositMethodGas case WithdrawMethodName: - GasRequiredByMethod[methodID] = 200000 + GasRequiredByMethod[methodID] = WithdrawMethodGas case BalanceOfMethodName: - GasRequiredByMethod[methodID] = 10000 + GasRequiredByMethod[methodID] = BalanceOfGas default: - GasRequiredByMethod[methodID] = 0 + GasRequiredByMethod[methodID] = DefaultGas } } } @@ -173,255 +156,3 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt } } } - -func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { - return ZEVMDenom + ZRC20Address.String() -} - -func (c *Contract) deposit( - ctx sdk.Context, - method *abi.Method, - caller common.Address, - args []interface{}, -) (result []byte, err error) { - // This function is developed using the - // Check - Effects - Interactions pattern: - // 1. Check everything is correct. - if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 2, - }) - } - - // function deposit(address zrc20, uint256 amount) external returns (bool success); - ZRC20Addr, amount := args[0].(common.Address), args[1].(*big.Int) - - // Initialize the ZRC20 ABI, as we need to call the balanceOf and allowance methods. - ZRC20ABI, err := zrc20.ZRC20MetaData.GetAbi() - if err != nil { - return nil, err - } - - // Check for enough balance. - // function balanceOf(address account) public view virtual override returns (uint256) - argsBalanceOf := []interface{}{caller} - - resBalanceOf, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "balanceOf", argsBalanceOf) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "CallContract", - Got: err.Error(), - } - } - - balance := resBalanceOf[0].(*big.Int) - if balance.Cmp(amount) < 0 { - return nil, &ptypes.ErrUnexpected{ - When: "balance0f", - Got: "not enough balance", - } - } - - // Check for enough allowance. - // function allowance(address owner, address spender) public view virtual override returns (uint256) - argsAllowance := []interface{}{caller, ContractAddress} - - resAllowance, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "allowance", argsAllowance) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "CallContract", - Got: err.Error(), - } - } - - allowance := resAllowance[0].(*big.Int) - if allowance.Cmp(amount) < 0 { - return nil, &ptypes.ErrUnexpected{ - When: "allowance", - Got: "not enough allowance", - } - } - - // Handle the toAddr: - // check it's valid and not blocked. - toAddr := sdk.AccAddress(caller.Bytes()) - if toAddr.Empty() { - return nil, &ptypes.ErrInvalidAddr{ - Got: toAddr.String(), - Reason: "empty address", - } - } - - if c.bankKeeper.BlockedAddr(toAddr) { - return nil, &ptypes.ErrInvalidAddr{ - Got: toAddr.String(), - Reason: "blocked by bank keeper", - } - } - - // The process of creating a new cosmos coin is: - // - Generate the new coin denom using ZRC20 address, - // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". - // - Mint coins. - // - Send coins to the caller. - tokenDenom := ZRC20ToCosmosDenom(ZRC20Addr) - coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) - if !coin.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ - Got: coin.GetDenom(), - Negative: coin.IsNegative(), - Nil: coin.IsNil(), - } - } - - // A sdk.Coins (type []sdk.Coin) has to be created because it's the type expected by MintCoins - // and SendCoinsFromModuleToAccount. - // But sdk.Coins will only contain one coin, always. - coinSet := sdk.NewCoins(coin) - if !coinSet.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ - Got: coinSet.Sort().GetDenomByIndex(0), - Negative: coinSet.IsAnyNegative(), - Nil: coinSet.IsAnyNil(), - } - } - - if !c.bankKeeper.IsSendEnabledCoin(ctx, coin) { - return nil, &ptypes.ErrUnexpected{ - When: "IsSendEnabledCoins", - Got: "coin not enabled to be sent", - } - } - - // 2. Effect: subtract balance. - // function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) - argsTransferFrom := []interface{}{caller, ContractAddress, amount} - - resTransferFrom, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "transferFrom", argsTransferFrom) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "CallContract", - Got: err.Error(), - } - } - - transferred := resTransferFrom[0].(bool) - if !transferred { - return nil, &ptypes.ErrUnexpected{ - When: "TransferFrom", - Got: "transfer not successful", - } - } - - // 3. Interactions: create cosmos coin and send. - if err := c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet); err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "MintCoins", - Got: err.Error(), - } - } - - if err := c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet); err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "SendCoinsFromModuleToAccount", - Got: err.Error(), - } - } - - return method.Outputs.Pack(true) -} - -func (c *Contract) balanceOf( - ctx sdk.Context, - method *abi.Method, - args []interface{}, -) (result []byte, err error) { - if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 2, - }) - } - - // function balanceOf(address zrc20, address user) external view returns (uint256 balance); - tokenAddr, addr := args[0].(common.Address), args[1].(common.Address) - - // common.Address has to be converted to AccAddress. - accAddr := sdk.AccAddress(addr.Bytes()) - if accAddr.Empty() { - return nil, &ptypes.ErrInvalidAddr{ - Got: accAddr.String(), - } - } - - // Convert ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". - tokenDenom := ZRC20ToCosmosDenom(tokenAddr) - - // Bank Keeper GetBalance returns the specified Cosmos coin balance for a given address. - // Check explicitly the balance is a non-negative non-nil value. - coin := c.bankKeeper.GetBalance(ctx, accAddr, tokenDenom) - if !coin.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ - Got: coin.GetDenom(), - Negative: coin.IsNegative(), - Nil: coin.IsNil(), - } - } - - return method.Outputs.Pack(coin.Amount.BigInt()) -} - -// CallContract calls a given contract on behalf of the precompiled contract. -// Note that the precompile contract address is hardcoded. -func (c *Contract) CallContract( - ctx sdk.Context, - abi *abi.ABI, - dst common.Address, - method string, - args []interface{}, -) ([]interface{}, error) { - input, err := abi.Methods[method].Inputs.Pack(args) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "Pack " + method, - Got: err.Error(), - } - } - - res, err := c.fungibleKeeper.CallEVM( - ctx, - *abi, - ContractAddress, - dst, - big.NewInt(0), - nil, - true, - false, - method, - input, - ) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "CallEVM " + method, - Got: err.Error(), - } - } - - if res.VmError != "" { - return nil, &ptypes.ErrUnexpected{ - When: "VmError " + method, - Got: res.VmError, - } - } - - ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "Unpack " + method, - Got: err.Error(), - } - } - - return ret, nil -} diff --git a/precompiles/bank/call_contract.go b/precompiles/bank/call_contract.go new file mode 100644 index 0000000000..9b7e825491 --- /dev/null +++ b/precompiles/bank/call_contract.go @@ -0,0 +1,65 @@ +package bank + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + ptypes "github.com/zeta-chain/node/precompiles/types" +) + +// CallContract calls a given contract on behalf of the precompiled contract. +// Note that the precompile contract address is hardcoded. +func (c *Contract) CallContract( + ctx sdk.Context, + abi *abi.ABI, + dst common.Address, + method string, + args []interface{}, +) ([]interface{}, error) { + input, err := abi.Methods[method].Inputs.Pack(args) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "Pack " + method, + Got: err.Error(), + } + } + + res, err := c.fungibleKeeper.CallEVM( + ctx, + *abi, + ContractAddress, + dst, + big.NewInt(0), + nil, + true, + false, + method, + input, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "CallEVM " + method, + Got: err.Error(), + } + } + + if res.VmError != "" { + return nil, &ptypes.ErrUnexpected{ + When: "VmError " + method, + Got: res.VmError, + } + } + + ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "Unpack " + method, + Got: err.Error(), + } + } + + return ret, nil +} diff --git a/precompiles/bank/coin.go b/precompiles/bank/coin.go new file mode 100644 index 0000000000..c6ff0a262e --- /dev/null +++ b/precompiles/bank/coin.go @@ -0,0 +1,7 @@ +package bank + +import "github.com/ethereum/go-ethereum/common" + +func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { + return ZEVMDenom + ZRC20Address.String() +} diff --git a/precompiles/bank/const.go b/precompiles/bank/const.go new file mode 100644 index 0000000000..e3c5084652 --- /dev/null +++ b/precompiles/bank/const.go @@ -0,0 +1,20 @@ +package bank + +const ( + // ZEVM cosmos coins prefix. + ZEVMDenom = "zevm/" + + // Write methods. + DepositMethodName = "deposit" + DepositMethodGas = 200_000 + + WithdrawMethodName = "withdraw" + WithdrawMethodGas = 200_000 + + // Read methods. + BalanceOfMethodName = "balanceOf" + BalanceOfGas = 10_000 + + // Default gas for unknown methods. + DefaultGas = 0 +) diff --git a/precompiles/bank/method_balanceof.go b/precompiles/bank/method_balanceof.go new file mode 100644 index 0000000000..637dd76cf4 --- /dev/null +++ b/precompiles/bank/method_balanceof.go @@ -0,0 +1,49 @@ +package bank + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + ptypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) balanceOf( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) (result []byte, err error) { + if len(args) != 2 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + + // function balanceOf(address zrc20, address user) external view returns (uint256 balance); + tokenAddr, addr := args[0].(common.Address), args[1].(common.Address) + + // common.Address has to be converted to AccAddress. + accAddr := sdk.AccAddress(addr.Bytes()) + if accAddr.Empty() { + return nil, &ptypes.ErrInvalidAddr{ + Got: accAddr.String(), + } + } + + // Convert ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". + tokenDenom := ZRC20ToCosmosDenom(tokenAddr) + + // Bank Keeper GetBalance returns the specified Cosmos coin balance for a given address. + // Check explicitly the balance is a non-negative non-nil value. + coin := c.bankKeeper.GetBalance(ctx, accAddr, tokenDenom) + if !coin.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coin.GetDenom(), + Negative: coin.IsNegative(), + Nil: coin.IsNil(), + } + } + + return method.Outputs.Pack(coin.Amount.BigInt()) +} diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go new file mode 100644 index 0000000000..3203c7690e --- /dev/null +++ b/precompiles/bank/method_deposit.go @@ -0,0 +1,176 @@ +package bank + +import ( + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" + + ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/x/fungible/types" +) + +func (c *Contract) deposit( + ctx sdk.Context, + method *abi.Method, + caller common.Address, + args []interface{}, +) (result []byte, err error) { + // This function is developed using the + // Check - Effects - Interactions pattern: + // 1. Check everything is correct. + if len(args) != 2 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + + // function deposit(address zrc20, uint256 amount) external returns (bool success); + ZRC20Addr, amount := args[0].(common.Address), args[1].(*big.Int) + if amount.Sign() < 0 || amount == nil || amount == new(big.Int) { + return nil, &ptypes.ErrInvalidAmount{ + Got: amount.String(), + } + } + + // Initialize the ZRC20 ABI, as we need to call the balanceOf and allowance methods. + ZRC20ABI, err := zrc20.ZRC20MetaData.GetAbi() + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "ZRC20MetaData.GetAbi", + Got: err.Error(), + } + } + + // Check for enough balance. + // function balanceOf(address account) public view virtual override returns (uint256) + argsBalanceOf := []interface{}{caller} + + resBalanceOf, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "balanceOf", argsBalanceOf) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "balanceOf", + Got: err.Error(), + } + } + + balance := resBalanceOf[0].(*big.Int) + if balance.Cmp(amount) < 0 { + return nil, &ptypes.ErrUnexpected{ + When: "balance0f", + Got: "not enough balance", + } + } + + // Check for enough allowance. + // function allowance(address owner, address spender) public view virtual override returns (uint256) + argsAllowance := []interface{}{caller, ContractAddress} + + resAllowance, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "allowance", argsAllowance) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "allowance", + Got: err.Error(), + } + } + + allowance := resAllowance[0].(*big.Int) + if allowance.Cmp(amount) < 0 { + return nil, &ptypes.ErrUnexpected{ + When: "allowance", + Got: "not enough allowance", + } + } + + // Handle the toAddr: + // check it's valid and not blocked. + toAddr := sdk.AccAddress(caller.Bytes()) + if toAddr.Empty() { + return nil, &ptypes.ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "empty address", + } + } + + if c.bankKeeper.BlockedAddr(toAddr) { + return nil, &ptypes.ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "blocked by bank keeper", + } + } + + // The process of creating a new cosmos coin is: + // - Generate the new coin denom using ZRC20 address, + // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". + // - Mint coins. + // - Send coins to the caller. + tokenDenom := ZRC20ToCosmosDenom(ZRC20Addr) + coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) + if !coin.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coin.GetDenom(), + Negative: coin.IsNegative(), + Nil: coin.IsNil(), + } + } + + // A sdk.Coins (type []sdk.Coin) has to be created because it's the type expected by MintCoins + // and SendCoinsFromModuleToAccount. + // But sdk.Coins will only contain one coin, always. + coinSet := sdk.NewCoins(coin) + if !coinSet.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coinSet.Sort().GetDenomByIndex(0), + Negative: coinSet.IsAnyNegative(), + Nil: coinSet.IsAnyNil(), + } + } + + if !c.bankKeeper.IsSendEnabledCoin(ctx, coin) { + return nil, &ptypes.ErrUnexpected{ + When: "IsSendEnabledCoins", + Got: "coin not enabled to be sent", + } + } + + // 2. Effect: subtract balance. + // function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) + argsTransferFrom := []interface{}{caller, ContractAddress, amount} + + resTransferFrom, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "transferFrom", argsTransferFrom) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "transferFrom", + Got: err.Error(), + } + } + + transferred := resTransferFrom[0].(bool) + if !transferred { + return nil, &ptypes.ErrUnexpected{ + When: "transferFrom", + Got: "transaction not successful", + } + } + + // 3. Interactions: create cosmos coin and send. + if err := c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "MintCoins", + Got: err.Error(), + } + } + + if err := c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "SendCoinsFromModuleToAccount", + Got: err.Error(), + } + } + + return method.Outputs.Pack(true) +} diff --git a/precompiles/types/errors.go b/precompiles/types/errors.go index a4e8a86b0b..73b84bd2ee 100644 --- a/precompiles/types/errors.go +++ b/precompiles/types/errors.go @@ -36,7 +36,7 @@ func (e ErrInvalidArgument) Error() string { } /* - Coin related errors + Token related errors */ type ErrInvalidCoin struct { @@ -49,6 +49,14 @@ func (e ErrInvalidCoin) Error() string { return fmt.Sprintf("invalid coin: denom: %s, is negative: %v, is nil: %v", e.Got, e.Negative, e.Nil) } +type ErrInvalidAmount struct { + Got string +} + +func (e ErrInvalidAmount) Error() string { + return fmt.Sprintf("invalid token amount: %s", e.Got) +} + /* Method related errors */ From 5863536dc9b8eb741e31c686524f8ee99671bca5 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 18:12:42 +0200 Subject: [PATCH 08/33] initialize e2e testing --- cmd/zetae2e/local/local.go | 1 + e2e/e2etests/e2etests.go | 8 ++++++++ e2e/e2etests/test_precompiles_bank.go | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 e2e/e2etests/test_precompiles_bank.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 7e31d4a407..7db944ac96 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -321,6 +321,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestPrecompilesPrototypeThroughContractName, e2etests.TestPrecompilesStakingName, e2etests.TestPrecompilesStakingThroughContractName, + e2etests.TestPrecompilesBankName, } } diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index a33737eb41..01872dc37b 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -159,6 +159,8 @@ const ( TestPrecompilesPrototypeThroughContractName = "precompile_contracts_prototype_through_contract" TestPrecompilesStakingName = "precompile_contracts_staking" TestPrecompilesStakingThroughContractName = "precompile_contracts_staking_through_contract" + TestPrecompilesBankName = "precompile_contracts_bank" + TestPrecompilesBankThroughContractName = "precompile_contracts_bank_through_contract" ) // AllE2ETests is an ordered list of all e2e tests @@ -877,4 +879,10 @@ var AllE2ETests = []runner.E2ETest{ []runner.ArgDefinition{}, TestPrecompilesStakingThroughContract, ), + runner.NewE2ETest( + TestPrecompilesBankName, + "test stateful precompiled contracts bank", + []runner.ArgDefinition{}, + TestPrecompilesBank, + ), } diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go new file mode 100644 index 0000000000..5492392539 --- /dev/null +++ b/e2e/e2etests/test_precompiles_bank.go @@ -0,0 +1,23 @@ +package e2etests + +import ( + "math/big" + + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/precompiles/bank" +) + +func TestPrecompilesBank(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create bank contract caller") + + // Get the balance of the user_precompile in coins "zevm/0x12345" + // BalanceOf will convert the ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". + res, err := bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, r.ZEVMAuth.From) + require.NoError(r, err, "Error calling BalanceOf") + require.Equal(r, big.NewInt(0), res, "BalanceOf result has to be 0") +} From 12a51381ca119c741ce2459fbd6cd9ec8b142f3b Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 18:28:12 +0200 Subject: [PATCH 09/33] remove duplicated funding --- .../localnet/orchestrator/start-zetae2e.sh | 5 ---- e2e/e2etests/test_precompiles_bank.go | 23 ++++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 9e72cc15d9..32fa928482 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -136,11 +136,6 @@ address=$(yq -r '.additional_accounts.user_precompile.evm_address' config.yml) echo "funding precompile tester address ${address} with 10000 Ether" geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null -# unlock precompile tests accounts -address=$(yq -r '.additional_accounts.user_precompile.evm_address' config.yml) -echo "funding precompile tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null - # unlock local solana relayer accounts if host solana > /dev/null; then solana_url=$(config_str '.rpcs.solana') diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 5492392539..0c08b57b4d 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/precompiles/bank" ) @@ -17,7 +18,23 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { // Get the balance of the user_precompile in coins "zevm/0x12345" // BalanceOf will convert the ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". - res, err := bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, r.ZEVMAuth.From) - require.NoError(r, err, "Error calling BalanceOf") - require.Equal(r, big.NewInt(0), res, "BalanceOf result has to be 0") + retBalanceOf, err := bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, r.ZEVMAuth.From) + require.NoError(r, err, "Error calling balanceOf") + require.EqualValues(r, uint64(0), retBalanceOf.Uint64(), "balanceOf result has to be 0") + + // Allow the bank contract to spend 100 coins + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, big.NewInt(100)) + require.NoError(r, err, "Error approving bank contract") + + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.EqualValues(r, 1, receipt.Status, "Error approving allowance for bank contract") + + // Call deposit with 100 coins + // _, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(0)) + // require.NoError(r, err, "Error calling deposit") + + // Check the balance of the user_precompile in coins "zevm/0x12345" + + // Check the balance of the user_precompile in r.ERC20ZRC20Addr + } From a4be05e46bbed175f55c51141b722ed1ca757c31 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 18:56:38 +0200 Subject: [PATCH 10/33] add codecov --- codecov.yml | 1 + e2e/e2etests/test_precompiles_bank.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index fedb830848..bface7f7ba 100644 --- a/codecov.yml +++ b/codecov.yml @@ -82,3 +82,4 @@ ignore: - "precompiles/**/*.sol" - "precompiles/prototype/IPrototype.go" - "precompiles/staking/IStaking.go" + - "precompiles/bank/IBank.go" diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 0c08b57b4d..0b2cb0e99c 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -30,8 +30,8 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.EqualValues(r, 1, receipt.Status, "Error approving allowance for bank contract") // Call deposit with 100 coins - // _, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(0)) - // require.NoError(r, err, "Error calling deposit") + _, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(100)) + require.NoError(r, err, "Error calling deposit") // Check the balance of the user_precompile in coins "zevm/0x12345" From 75efde6043323e39a8a874e62f6bd1235b6e35ff Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 19:24:23 +0200 Subject: [PATCH 11/33] expand e2e --- e2e/e2etests/test_precompiles_bank.go | 48 +++++++++++++++++++-------- precompiles/bank/call_contract.go | 28 ++++++---------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 0b2cb0e99c..4da23223cf 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -3,6 +3,7 @@ package e2etests import ( "math/big" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" @@ -13,28 +14,49 @@ import ( func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.Len(r, args, 0, "No arguments expected") + owner, spender := r.ZEVMAuth.From, bank.ContractAddress + bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) require.NoError(r, err, "Failed to create bank contract caller") - // Get the balance of the user_precompile in coins "zevm/0x12345" + // Get the initial balance of the owner in ERC20ZRC20 tokens. + ownerERC20InitialBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{}, owner) + require.NoError(r, err, "Error retrieving initial owner balance") + + // Get the balance of the owner in coins "zevm/0x12345". // BalanceOf will convert the ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". - retBalanceOf, err := bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, r.ZEVMAuth.From) - require.NoError(r, err, "Error calling balanceOf") + retBalanceOf, err := bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, owner) + require.NoError(r, err, "Error calling bank.balanceOf") require.EqualValues(r, uint64(0), retBalanceOf.Uint64(), "balanceOf result has to be 0") - // Allow the bank contract to spend 100 coins - tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, big.NewInt(100)) - require.NoError(r, err, "Error approving bank contract") + // Allow the bank contract to spend 100 ERC20ZRC20 tokens. + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, spender, big.NewInt(100)) + require.NoError(r, err, "Error approving allowance for bank contract") receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - require.EqualValues(r, 1, receipt.Status, "Error approving allowance for bank contract") + require.EqualValues(r, 1, receipt.Status, "Error in the approve allowance transaction") - // Call deposit with 100 coins - _, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(100)) - require.NoError(r, err, "Error calling deposit") + // Check the allowance of the bank in ERC20ZRC20 tokens. Should be 100. + balance, err := r.ERC20ZRC20.Allowance(&bind.CallOpts{}, owner, spender) + require.NoError(r, err, "Error retrieving bank allowance") + require.EqualValues(r, uint64(100), balance.Uint64(), "Error allowance for bank contract") - // Check the balance of the user_precompile in coins "zevm/0x12345" - - // Check the balance of the user_precompile in r.ERC20ZRC20Addr + // Call deposit with 100 coins. + _, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(100)) + require.NoError(r, err, "Error calling bank.deposit") + // Check the balance of the owner in coins "zevm/0x12345". + retBalanceOf, err = bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, owner) + require.NoError(r, err, "Error calling balanceOf") + require.EqualValues(r, uint64(100), retBalanceOf.Uint64(), "balanceOf result has to be 100") + + // Check the balance of the owner in r.ERC20ZRC20Addr, should be 100 less. + ownerERC20FinalBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{}, owner) + require.NoError(r, err, "Error retrieving final owner balance") + require.EqualValues( + r, + ownerERC20InitialBalance.Uint64()-100, // expected + ownerERC20FinalBalance.Uint64(), // actual + "Final balance should be initial - 100", + ) } diff --git a/precompiles/bank/call_contract.go b/precompiles/bank/call_contract.go index 9b7e825491..8277f96b8a 100644 --- a/precompiles/bank/call_contract.go +++ b/precompiles/bank/call_contract.go @@ -19,25 +19,17 @@ func (c *Contract) CallContract( method string, args []interface{}, ) ([]interface{}, error) { - input, err := abi.Methods[method].Inputs.Pack(args) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "Pack " + method, - Got: err.Error(), - } - } - res, err := c.fungibleKeeper.CallEVM( - ctx, - *abi, - ContractAddress, - dst, - big.NewInt(0), - nil, - true, - false, - method, - input, + ctx, // ctx + *abi, // abi + ContractAddress, // from + dst, // to + big.NewInt(0), // value + nil, // gasLimit + true, // commit + false, // noEthereumTxEvent + method, // method + args..., // args ) if err != nil { return nil, &ptypes.ErrUnexpected{ From 6340abbe4627930cfff5f1f4c2f01a7847809b3c Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 11 Sep 2024 19:52:03 +0200 Subject: [PATCH 12/33] fix: wait for deposit tx to be mined --- Dockerfile-localnet | 4 +- .../localnet/orchestrator/start-zetae2e.sh | 4 +- e2e/e2etests/test_precompiles_bank.go | 56 ++++++++++++++----- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/Dockerfile-localnet b/Dockerfile-localnet index 6be3ba3e03..cfe5e19a48 100644 --- a/Dockerfile-localnet +++ b/Dockerfile-localnet @@ -59,7 +59,7 @@ EXPOSE 22 FROM base-runtime AS latest-runtime COPY --from=cosmovisor-build /go/bin/cosmovisor /usr/local/bin -COPY --from=latest-build /go/bin/zetacored /go/bin/zetaclientd /go/bin/zetaclientd-supervisor /go/bin/zetae2e /usr/local/bin +COPY --from=latest-build /go/bin/zetacored /go/bin/zetaclientd /go/bin/zetaclientd-supervisor /go/bin/zetae2e /usr/local/bin/ # Optional old version build (from source). This old build is used as the genesis version in the upgrade tests. # Use --target latest-runtime to skip. @@ -75,7 +75,7 @@ RUN cd node && make install FROM base-runtime AS old-runtime-source COPY --from=cosmovisor-build /go/bin/cosmovisor /usr/local/bin -COPY --from=old-build-source /go/bin/zetacored /go/bin/zetaclientd /usr/local/bin +COPY --from=old-build-source /go/bin/zetacored /go/bin/zetaclientd /usr/local/bin/ COPY --from=latest-build /go/bin/zetaclientd-supervisor /usr/local/bin # Optional old version build (from binary). diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 32fa928482..94fbd28ec9 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -132,9 +132,7 @@ fund_eth_from_config '.additional_accounts.user_v2_ether_revert.evm_address' 100 fund_eth_from_config '.additional_accounts.user_v2_erc20_revert.evm_address' 10000 "V2 ERC20 revert tester" # unlock precompile tests accounts -address=$(yq -r '.additional_accounts.user_precompile.evm_address' config.yml) -echo "funding precompile tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null +fund_eth_from_config '.additional_accounts.user_precompile.evm_address' 10000 "precompiles tester" # unlock local solana relayer accounts if host solana > /dev/null; then diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 4da23223cf..823b1a5942 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -1,6 +1,7 @@ package e2etests import ( + "fmt" "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -14,45 +15,74 @@ import ( func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.Len(r, args, 0, "No arguments expected") - owner, spender := r.ZEVMAuth.From, bank.ContractAddress + previousGasLimit := r.ZEVMAuth.GasLimit + r.ZEVMAuth.GasLimit = 10_000_000 + defer func() { + r.ZEVMAuth.GasLimit = previousGasLimit + }() + // Set owner and spender for legibility. + owner, spender := r.EVMAddress(), bank.ContractAddress + fmt.Println("owner ", owner.String()) + fmt.Println("spender ", spender.String()) + fmt.Println("ERC20ZRC20 ", r.ERC20ZRC20Addr.String()) + + // Fund owner with 200 token. + tx, err := r.ERC20ZRC20.Transfer(r.ZEVMAuth, owner, big.NewInt(200)) + require.NoError(r, err, "Error funding owner with ERC20ZRC20") + + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + fmt.Printf("funding owner tx receipt: %+v\n", receipt) + utils.RequireTxSuccessful(r, receipt, "funding owner tx") + + // Create a bank contract caller. bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) require.NoError(r, err, "Failed to create bank contract caller") - // Get the initial balance of the owner in ERC20ZRC20 tokens. - ownerERC20InitialBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{}, owner) + // Get the initial balance of the owner in ERC20ZRC20 tokens. Should be 200. + ownerERC20InitialBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, owner) require.NoError(r, err, "Error retrieving initial owner balance") + require.EqualValues(r, uint64(0), ownerERC20InitialBalance.Uint64(), "Initial ERC20ZRC20 has to be 200") + fmt.Println("owner balance ERC20: ", ownerERC20InitialBalance) - // Get the balance of the owner in coins "zevm/0x12345". + // Get the balance of the owner in coins "zevm/0x12345". This calls bank.balanceOf(). // BalanceOf will convert the ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". - retBalanceOf, err := bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, owner) - require.NoError(r, err, "Error calling bank.balanceOf") - require.EqualValues(r, uint64(0), retBalanceOf.Uint64(), "balanceOf result has to be 0") + retBalanceOf, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, owner) + require.NoError(r, err, "Error calling bank.balanceOf()") + require.EqualValues(r, uint64(0), retBalanceOf.Uint64(), "Initial cosmos coins balance has to be 0") + fmt.Println("owner balance zevm/coin: ", retBalanceOf) // Allow the bank contract to spend 100 ERC20ZRC20 tokens. - tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, spender, big.NewInt(100)) + tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, spender, big.NewInt(100)) require.NoError(r, err, "Error approving allowance for bank contract") + fmt.Printf("approve allowance tx: %+v\n", tx) - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - require.EqualValues(r, 1, receipt.Status, "Error in the approve allowance transaction") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "approve allowance tx") + fmt.Printf("approve allowance tx receipt: %+v\n", receipt) // Check the allowance of the bank in ERC20ZRC20 tokens. Should be 100. - balance, err := r.ERC20ZRC20.Allowance(&bind.CallOpts{}, owner, spender) + balance, err := r.ERC20ZRC20.Allowance(&bind.CallOpts{Context: r.Ctx}, owner, spender) require.NoError(r, err, "Error retrieving bank allowance") require.EqualValues(r, uint64(100), balance.Uint64(), "Error allowance for bank contract") + fmt.Printf("bank allowance: %v\n", balance) // Call deposit with 100 coins. - _, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(100)) + tx, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(100)) require.NoError(r, err, "Error calling bank.deposit") + utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + fmt.Printf("Deposit tx: %+v\n", tx) // Check the balance of the owner in coins "zevm/0x12345". retBalanceOf, err = bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, owner) require.NoError(r, err, "Error calling balanceOf") require.EqualValues(r, uint64(100), retBalanceOf.Uint64(), "balanceOf result has to be 100") + fmt.Printf("owner balance zevm/coin (should increase): %+v\n", retBalanceOf) // Check the balance of the owner in r.ERC20ZRC20Addr, should be 100 less. - ownerERC20FinalBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{}, owner) + ownerERC20FinalBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, owner) require.NoError(r, err, "Error retrieving final owner balance") + fmt.Printf("owner final ERC20 balance (should decrease): %+v\n", retBalanceOf) require.EqualValues( r, ownerERC20InitialBalance.Uint64()-100, // expected From 0f347f21a42e113c29e7937684a3dc4bfbb07a8d Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 12 Sep 2024 12:50:42 +0200 Subject: [PATCH 13/33] apply first round of reviews --- ...thod_balanceof.go => method_balance_of.go} | 0 precompiles/bank/method_deposit.go | 19 ++++++------------- 2 files changed, 6 insertions(+), 13 deletions(-) rename precompiles/bank/{method_balanceof.go => method_balance_of.go} (100%) diff --git a/precompiles/bank/method_balanceof.go b/precompiles/bank/method_balance_of.go similarity index 100% rename from precompiles/bank/method_balanceof.go rename to precompiles/bank/method_balance_of.go diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 3203c7690e..a1214d0a06 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -30,7 +30,7 @@ func (c *Contract) deposit( } // function deposit(address zrc20, uint256 amount) external returns (bool success); - ZRC20Addr, amount := args[0].(common.Address), args[1].(*big.Int) + zrc20Addr, amount := args[0].(common.Address), args[1].(*big.Int) if amount.Sign() < 0 || amount == nil || amount == new(big.Int) { return nil, &ptypes.ErrInvalidAmount{ Got: amount.String(), @@ -38,7 +38,7 @@ func (c *Contract) deposit( } // Initialize the ZRC20 ABI, as we need to call the balanceOf and allowance methods. - ZRC20ABI, err := zrc20.ZRC20MetaData.GetAbi() + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() if err != nil { return nil, &ptypes.ErrUnexpected{ When: "ZRC20MetaData.GetAbi", @@ -50,7 +50,7 @@ func (c *Contract) deposit( // function balanceOf(address account) public view virtual override returns (uint256) argsBalanceOf := []interface{}{caller} - resBalanceOf, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "balanceOf", argsBalanceOf) + resBalanceOf, err := c.CallContract(ctx, zrc20ABI, zrc20Addr, "balanceOf", argsBalanceOf) if err != nil { return nil, &ptypes.ErrUnexpected{ When: "balanceOf", @@ -70,7 +70,7 @@ func (c *Contract) deposit( // function allowance(address owner, address spender) public view virtual override returns (uint256) argsAllowance := []interface{}{caller, ContractAddress} - resAllowance, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "allowance", argsAllowance) + resAllowance, err := c.CallContract(ctx, zrc20ABI, zrc20Addr, "allowance", argsAllowance) if err != nil { return nil, &ptypes.ErrUnexpected{ When: "allowance", @@ -108,7 +108,7 @@ func (c *Contract) deposit( // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". // - Mint coins. // - Send coins to the caller. - tokenDenom := ZRC20ToCosmosDenom(ZRC20Addr) + tokenDenom := ZRC20ToCosmosDenom(zrc20Addr) coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) if !coin.IsValid() { return nil, &ptypes.ErrInvalidCoin{ @@ -130,18 +130,11 @@ func (c *Contract) deposit( } } - if !c.bankKeeper.IsSendEnabledCoin(ctx, coin) { - return nil, &ptypes.ErrUnexpected{ - When: "IsSendEnabledCoins", - Got: "coin not enabled to be sent", - } - } - // 2. Effect: subtract balance. // function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) argsTransferFrom := []interface{}{caller, ContractAddress, amount} - resTransferFrom, err := c.CallContract(ctx, ZRC20ABI, ZRC20Addr, "transferFrom", argsTransferFrom) + resTransferFrom, err := c.CallContract(ctx, zrc20ABI, zrc20Addr, "transferFrom", argsTransferFrom) if err != nil { return nil, &ptypes.ErrUnexpected{ When: "transferFrom", From e0fa24aab4fb211c4f0c6faab3ab1fc5ab64fcaa Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 12 Sep 2024 13:09:06 +0200 Subject: [PATCH 14/33] cover al error types test --- precompiles/types/errors_test.go | 39 +++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go index 5693a450eb..bee953f1a2 100644 --- a/precompiles/types/errors_test.go +++ b/precompiles/types/errors_test.go @@ -5,9 +5,10 @@ import "testing" func Test_ErrInvalidAddr(t *testing.T) { e := ErrInvalidAddr{ Got: "foo", + Reason: "bar", } got := e.Error() - expect := "invalid address foo" + expect := "invalid address foo, reason: bar" if got != expect { t.Errorf("Expected %v, got %v", expect, got) } @@ -46,3 +47,39 @@ func Test_ErrInvalidMethod(t *testing.T) { t.Errorf("Expected %v, got %v", expect, got) } } + +func Test_ErrInvalidCoin(t *testing.T) { + e := ErrInvalidCoin{ + Got: "foo", + Negative: true, + Nil: false, + } + got := e.Error() + expect := "invalid coin: denom: foo, is negative: true, is nil: false" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} + +func Test_ErrInvalidAmount(t *testing.T) { + e := ErrInvalidAmount{ + Got: "foo", + } + got := e.Error() + expect := "invalid token amount: foo" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} + +func Test_ErrUnexpected(t *testing.T) { + e := ErrUnexpected{ + When: "foo", + Got: "bar", + } + got := e.Error() + expect := "unexpected foo, got: bar" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} \ No newline at end of file From cf17671f304a748d5e51d92505eb33cb77981a17 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 12 Sep 2024 14:10:33 +0200 Subject: [PATCH 15/33] fixes using time.Since --- app/app.go | 6 ++++++ e2e/e2etests/test_rate_limiter.go | 4 ++-- e2e/e2etests/test_stress_btc_deposit.go | 2 +- e2e/e2etests/test_stress_btc_withdraw.go | 2 +- e2e/e2etests/test_stress_eth_deposit.go | 2 +- e2e/e2etests/test_stress_eth_withdraw.go | 2 +- precompiles/types/errors_test.go | 4 ++-- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/app.go b/app/app.go index cc271fc35f..4eeaa16fa7 100644 --- a/app/app.go +++ b/app/app.go @@ -93,6 +93,7 @@ import ( "github.com/zeta-chain/node/docs/openapi" zetamempool "github.com/zeta-chain/node/pkg/mempool" "github.com/zeta-chain/node/precompiles" + bankprecompile "github.com/zeta-chain/node/precompiles/bank" srvflags "github.com/zeta-chain/node/server/flags" authoritymodule "github.com/zeta-chain/node/x/authority" authoritykeeper "github.com/zeta-chain/node/x/authority/keeper" @@ -1065,6 +1066,11 @@ func (app *App) BlockedAddrs() map[string]bool { // Each enabled precompiled stateful contract should be added as a BlockedAddrs. // That way it's marked as non payable by the bank keeper. for addr, enabled := range precompiles.EnabledStatefulContracts { + // bank precompile has to be able to receive funds. + if addr == bankprecompile.ContractAddress { + continue + } + if enabled { blockList[addr.String()] = enabled } diff --git a/e2e/e2etests/test_rate_limiter.go b/e2e/e2etests/test_rate_limiter.go index c2e10551aa..e74691e1bd 100644 --- a/e2e/e2etests/test_rate_limiter.go +++ b/e2e/e2etests/test_rate_limiter.go @@ -122,7 +122,7 @@ func createAndWaitWithdraws(r *runner.E2ERunner, withdrawType withdrawType, with return err } - duration := time.Now().Sub(startTime).Seconds() + duration := time.Since(startTime).Seconds() block, err := r.ZEVMClient.BlockNumber(r.Ctx) if err != nil { return fmt.Errorf("error getting block number: %w", err) @@ -155,7 +155,7 @@ func waitForWithdrawMined( } // record the time for completion - duration := time.Now().Sub(startTime).Seconds() + duration := time.Since(startTime).Seconds() block, err := r.ZEVMClient.BlockNumber(ctx) if err != nil { return err diff --git a/e2e/e2etests/test_stress_btc_deposit.go b/e2e/e2etests/test_stress_btc_deposit.go index 25b89088ac..53caea09df 100644 --- a/e2e/e2etests/test_stress_btc_deposit.go +++ b/e2e/e2etests/test_stress_btc_deposit.go @@ -53,7 +53,7 @@ func monitorBTCDeposit(r *runner.E2ERunner, hash *chainhash.Hash, index int, sta cctx.Index, ) } - timeToComplete := time.Now().Sub(startTime) + timeToComplete := time.Since(startTime) r.Logger.Print("index %d: deposit cctx success in %s", index, timeToComplete.String()) return nil diff --git a/e2e/e2etests/test_stress_btc_withdraw.go b/e2e/e2etests/test_stress_btc_withdraw.go index 6e212c97e3..79cf1ae773 100644 --- a/e2e/e2etests/test_stress_btc_withdraw.go +++ b/e2e/e2etests/test_stress_btc_withdraw.go @@ -66,7 +66,7 @@ func monitorBTCWithdraw(r *runner.E2ERunner, tx *ethtypes.Transaction, index int cctx.Index, ) } - timeToComplete := time.Now().Sub(startTime) + timeToComplete := time.Since(startTime) r.Logger.Print("index %d: withdraw cctx success in %s", index, timeToComplete.String()) return nil diff --git a/e2e/e2etests/test_stress_eth_deposit.go b/e2e/e2etests/test_stress_eth_deposit.go index 19676c9cba..29abb76ba0 100644 --- a/e2e/e2etests/test_stress_eth_deposit.go +++ b/e2e/e2etests/test_stress_eth_deposit.go @@ -52,7 +52,7 @@ func monitorEtherDeposit(r *runner.E2ERunner, hash ethcommon.Hash, index int, st cctx.Index, ) } - timeToComplete := time.Now().Sub(startTime) + timeToComplete := time.Since(startTime) r.Logger.Print("index %d: deposit cctx success in %s", index, timeToComplete.String()) return nil diff --git a/e2e/e2etests/test_stress_eth_withdraw.go b/e2e/e2etests/test_stress_eth_withdraw.go index 98fa8e8783..337a4d416d 100644 --- a/e2e/e2etests/test_stress_eth_withdraw.go +++ b/e2e/e2etests/test_stress_eth_withdraw.go @@ -70,7 +70,7 @@ func monitorEtherWithdraw(r *runner.E2ERunner, tx *ethtypes.Transaction, index i cctx.Index, ) } - timeToComplete := time.Now().Sub(startTime) + timeToComplete := time.Since(startTime) r.Logger.Print("index %d: withdraw cctx success in %s", index, timeToComplete.String()) return nil diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go index bee953f1a2..4fa3bc72e2 100644 --- a/precompiles/types/errors_test.go +++ b/precompiles/types/errors_test.go @@ -4,7 +4,7 @@ import "testing" func Test_ErrInvalidAddr(t *testing.T) { e := ErrInvalidAddr{ - Got: "foo", + Got: "foo", Reason: "bar", } got := e.Error() @@ -82,4 +82,4 @@ func Test_ErrUnexpected(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } -} \ No newline at end of file +} From 02ddce3828ed14a452eb28a29b0af40e6630a49c Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 12 Sep 2024 19:24:31 +0200 Subject: [PATCH 16/33] Include CallContract interface --- cmd/zetae2e/config/localnet.yml | 4 + .../localnet/orchestrator/start-zetae2e.sh | 5 +- contrib/localnet/scripts/start-zetacored.sh | 3 + e2e/e2etests/test_precompiles_bank.go | 93 ++++++++-------- precompiles/bank/bank.go | 16 ++- precompiles/bank/call_contract.go | 57 ---------- precompiles/bank/method_deposit.go | 100 +++++++++++++----- precompiles/types/types.go | 60 +++++++++++ x/fungible/keeper/evm.go | 24 +++++ 9 files changed, 220 insertions(+), 142 deletions(-) delete mode 100644 precompiles/bank/call_contract.go diff --git a/cmd/zetae2e/config/localnet.yml b/cmd/zetae2e/config/localnet.yml index 5bd207a020..799f0ad2c0 100644 --- a/cmd/zetae2e/config/localnet.yml +++ b/cmd/zetae2e/config/localnet.yml @@ -61,6 +61,10 @@ additional_accounts: bech32_address: "zeta1nry9yeg6njhjrp2ctppa8558vqxal9fxk69zxg" evm_address: "0x98c852651A9CAF2185585843d3D287600Ddf9526" private_key: "bf9456c679bb5a952a9a137fcfc920e0413efdb97c36de1e57455763084230cb" + user_bank_precompile: + bech32_address: "zeta1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqr84sgapz" + evm_address: "0x0000000000000000000000000000000000000067" + private_key: "" policy_accounts: emergency_policy_account: bech32_address: "zeta16m2cnrdwtgweq4njc6t470vl325gw4kp6s7tap" diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 94fbd28ec9..6d98df3699 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -117,7 +117,7 @@ fund_eth_from_config '.additional_accounts.user_admin.evm_address' 10000 "admin fund_eth_from_config '.additional_accounts.user_migration.evm_address' 10000 "migration tester" # unlock precompile tests accounts -fund_eth_from_config '.additional_accounts.user_precompile.evm_address' 10000 "precompile tester" +fund_eth_from_config '.additional_accounts.user_precompile.evm_address' 10000 "precompiles tester" # unlock v2 ethers tests accounts fund_eth_from_config '.additional_accounts.user_v2_ether.evm_address' 10000 "V2 ethers tester" @@ -131,9 +131,6 @@ fund_eth_from_config '.additional_accounts.user_v2_ether_revert.evm_address' 100 # unlock v2 erc20 revert tests accounts fund_eth_from_config '.additional_accounts.user_v2_erc20_revert.evm_address' 10000 "V2 ERC20 revert tester" -# unlock precompile tests accounts -fund_eth_from_config '.additional_accounts.user_precompile.evm_address' 10000 "precompiles tester" - # unlock local solana relayer accounts if host solana > /dev/null; then solana_url=$(config_str '.rpcs.solana') diff --git a/contrib/localnet/scripts/start-zetacored.sh b/contrib/localnet/scripts/start-zetacored.sh index 1f5e2fca12..fdad77e632 100755 --- a/contrib/localnet/scripts/start-zetacored.sh +++ b/contrib/localnet/scripts/start-zetacored.sh @@ -269,6 +269,9 @@ then # v2 erc20 revert tester address=$(yq -r '.additional_accounts.user_v2_erc20_revert.bech32_address' /root/config.yml) zetacored add-genesis-account "$address" 100000000000000000000000000azeta +# bank precompile user + address=$(yq -r '.additional_accounts.user_bank_precompile.bech32_address' /root/config.yml) + zetacored add-genesis-account "$address" 100000000000000000000000000azeta # 3. Copy the genesis.json to all the nodes .And use it to create a gentx for every node zetacored gentx operator 1000000000000000000000azeta --chain-id=$CHAINID --keyring-backend=$KEYRING --gas-prices 20000000000azeta diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 823b1a5942..9fda3492ca 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -15,78 +15,71 @@ import ( func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.Len(r, args, 0, "No arguments expected") + // Increase the gasLimit. It's required because of the gas consumed by precompiled functions. previousGasLimit := r.ZEVMAuth.GasLimit r.ZEVMAuth.GasLimit = 10_000_000 defer func() { r.ZEVMAuth.GasLimit = previousGasLimit }() - // Set owner and spender for legibility. - owner, spender := r.EVMAddress(), bank.ContractAddress - fmt.Println("owner ", owner.String()) - fmt.Println("spender ", spender.String()) - fmt.Println("ERC20ZRC20 ", r.ERC20ZRC20Addr.String()) + spender, bankAddr := r.EVMAddress(), bank.ContractAddress - // Fund owner with 200 token. - tx, err := r.ERC20ZRC20.Transfer(r.ZEVMAuth, owner, big.NewInt(200)) - require.NoError(r, err, "Error funding owner with ERC20ZRC20") + // Deposit and approve 50 WZETA for the test. + approveAmount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(50)) + r.DepositAndApproveWZeta(approveAmount) + fmt.Printf("DEBUG: approveAmount %s\n", approveAmount.String()) - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - fmt.Printf("funding owner tx receipt: %+v\n", receipt) - utils.RequireTxSuccessful(r, receipt, "funding owner tx") + initialBalance, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) + fmt.Printf("DEBUG: initialBalance %s\n", initialBalance.String()) + require.NoError(r, err, "Error approving allowance for bank contract") + require.EqualValues(r, approveAmount, initialBalance, "spender balance should be 50") // Create a bank contract caller. bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) require.NoError(r, err, "Failed to create bank contract caller") - // Get the initial balance of the owner in ERC20ZRC20 tokens. Should be 200. - ownerERC20InitialBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, owner) - require.NoError(r, err, "Error retrieving initial owner balance") - require.EqualValues(r, uint64(0), ownerERC20InitialBalance.Uint64(), "Initial ERC20ZRC20 has to be 200") - fmt.Println("owner balance ERC20: ", ownerERC20InitialBalance) - - // Get the balance of the owner in coins "zevm/0x12345". This calls bank.balanceOf(). + // Get the balance of the spender in coins "zevm/WZetaAddr". This calls bank.balanceOf(). // BalanceOf will convert the ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". - retBalanceOf, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, owner) + retBalanceOf, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) + fmt.Printf("DEBUG: initial bank.balanceOf() %s\n", retBalanceOf.String()) require.NoError(r, err, "Error calling bank.balanceOf()") require.EqualValues(r, uint64(0), retBalanceOf.Uint64(), "Initial cosmos coins balance has to be 0") - fmt.Println("owner balance zevm/coin: ", retBalanceOf) - // Allow the bank contract to spend 100 ERC20ZRC20 tokens. - tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, spender, big.NewInt(100)) + // Allow the bank contract to spend 25 WZeta tokens. + tx, err := r.WZeta.Approve(r.ZEVMAuth, bankAddr, big.NewInt(25)) require.NoError(r, err, "Error approving allowance for bank contract") - fmt.Printf("approve allowance tx: %+v\n", tx) - - receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequireTxSuccessful(r, receipt, "approve allowance tx") - fmt.Printf("approve allowance tx receipt: %+v\n", receipt) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.EqualValues(r, uint64(1), receipt.Status, "approve allowance tx failed") - // Check the allowance of the bank in ERC20ZRC20 tokens. Should be 100. - balance, err := r.ERC20ZRC20.Allowance(&bind.CallOpts{Context: r.Ctx}, owner, spender) + // Check the allowance of the bank in WZeta tokens. Should be 25. + balance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, bankAddr) + fmt.Printf("DEBUG: bank allowance %s\n", balance.String()) require.NoError(r, err, "Error retrieving bank allowance") - require.EqualValues(r, uint64(100), balance.Uint64(), "Error allowance for bank contract") - fmt.Printf("bank allowance: %v\n", balance) - - // Call deposit with 100 coins. - tx, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(100)) - require.NoError(r, err, "Error calling bank.deposit") - utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - fmt.Printf("Deposit tx: %+v\n", tx) - - // Check the balance of the owner in coins "zevm/0x12345". - retBalanceOf, err = bankContract.BalanceOf(nil, r.ERC20ZRC20Addr, owner) - require.NoError(r, err, "Error calling balanceOf") - require.EqualValues(r, uint64(100), retBalanceOf.Uint64(), "balanceOf result has to be 100") - fmt.Printf("owner balance zevm/coin (should increase): %+v\n", retBalanceOf) - - // Check the balance of the owner in r.ERC20ZRC20Addr, should be 100 less. - ownerERC20FinalBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, owner) + require.EqualValues(r, uint64(25), balance.Uint64(), "Error allowance for bank contract") + + // Call deposit with 25 coins. + tx, err = bankContract.Deposit(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) + fmt.Printf("DEBUG: bank.deposit() tx hash %s\n", tx.Hash().String()) + require.NoError(r, err, "Error calling bank.deposit()") + + r.Logger.Info("Waiting for 5 blocks") + r.WaitForBlocks(5) + fmt.Printf("DEBUG: bank.deposit() tx %+v\n", tx) + + // Check the balance of the spender in coins "zevm/WZetaAddr". + retBalanceOf, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) + fmt.Printf("DEBUG: final bank.balanceOf() tx %s\n", retBalanceOf.String()) + require.NoError(r, err, "Error calling bank.balanceOf()") + require.EqualValues(r, uint64(25), retBalanceOf.Uint64(), "balanceOf result has to be 25") + + // Check the balance of the spender in r.WZeta, should be 100 less. + finalBalance, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) + fmt.Printf("DEBUG: final WZeta balance %s\n", finalBalance.String()) require.NoError(r, err, "Error retrieving final owner balance") - fmt.Printf("owner final ERC20 balance (should decrease): %+v\n", retBalanceOf) require.EqualValues( r, - ownerERC20InitialBalance.Uint64()-100, // expected - ownerERC20FinalBalance.Uint64(), // actual - "Final balance should be initial - 100", + initialBalance.Uint64()-25, // expected + finalBalance.Uint64(), // actual + "Final balance should be initial - 25", ) } diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index fae3d8fbec..7acebd1ab2 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -1,6 +1,8 @@ package bank import ( + "fmt" + "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -103,6 +105,7 @@ func (c *Contract) RequiredGas(input []byte) uint64 { // Run is the entrypoint of the precompiled contract, it switches over the input method, // and execute them accordingly. func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byte, error) { + fmt.Println("DEBUG: bank.Run()") method, err := ABI.MethodById(contract.Input[:4]) if err != nil { return nil, err @@ -117,31 +120,40 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt switch method.Name { case DepositMethodName: + fmt.Println("DEBUG: bank.Run(): DepositMethodName") if readOnly { - return nil, nil + return nil, ptypes.ErrUnexpected{ + Got: "method not allowed in read-only mode " + method.Name, + } } var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + fmt.Println("DEBUG: bank.Run(): DepositMethodName: ExecuteNativeAction c.deposit()") res, err = c.deposit(ctx, method, contract.CallerAddress, args) return err }) if execErr != nil { + fmt.Printf("DEBUG: bank.Run(): execErr %s", execErr.Error()) return nil, err } return res, nil case WithdrawMethodName: if readOnly { - return nil, nil + return nil, ptypes.ErrUnexpected{ + Got: "method not allowed in read-only mode " + method.Name, + } } return nil, nil // TODO case BalanceOfMethodName: + fmt.Println("DEBUG: bank.Run(): BalanceOfMethodName") var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + fmt.Println("DEBUG: bank.Run(): DepositMethodName: ExecuteNativeAction c.balanceOf()") res, err = c.balanceOf(ctx, method, args) return err }) diff --git a/precompiles/bank/call_contract.go b/precompiles/bank/call_contract.go deleted file mode 100644 index 8277f96b8a..0000000000 --- a/precompiles/bank/call_contract.go +++ /dev/null @@ -1,57 +0,0 @@ -package bank - -import ( - "math/big" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - - ptypes "github.com/zeta-chain/node/precompiles/types" -) - -// CallContract calls a given contract on behalf of the precompiled contract. -// Note that the precompile contract address is hardcoded. -func (c *Contract) CallContract( - ctx sdk.Context, - abi *abi.ABI, - dst common.Address, - method string, - args []interface{}, -) ([]interface{}, error) { - res, err := c.fungibleKeeper.CallEVM( - ctx, // ctx - *abi, // abi - ContractAddress, // from - dst, // to - big.NewInt(0), // value - nil, // gasLimit - true, // commit - false, // noEthereumTxEvent - method, // method - args..., // args - ) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "CallEVM " + method, - Got: err.Error(), - } - } - - if res.VmError != "" { - return nil, &ptypes.ErrUnexpected{ - When: "VmError " + method, - Got: res.VmError, - } - } - - ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) - if err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "Unpack " + method, - Got: err.Error(), - } - } - - return ret, nil -} diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index a1214d0a06..4a8cf39d9d 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -1,6 +1,7 @@ package bank import ( + "fmt" "math/big" "cosmossdk.io/math" @@ -19,8 +20,8 @@ func (c *Contract) deposit( caller common.Address, args []interface{}, ) (result []byte, err error) { - // This function is developed using the - // Check - Effects - Interactions pattern: + fmt.Printf("DEBUG: deposit()\n") + // This function is developed using the Check - Effects - Interactions pattern: // 1. Check everything is correct. if len(args) != 2 { return nil, &(ptypes.ErrInvalidNumberOfArgs{ @@ -29,48 +30,74 @@ func (c *Contract) deposit( }) } + // Unpack parameters for function deposit. // function deposit(address zrc20, uint256 amount) external returns (bool success); - zrc20Addr, amount := args[0].(common.Address), args[1].(*big.Int) - if amount.Sign() < 0 || amount == nil || amount == new(big.Int) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return nil, &ptypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + amount, ok := args[1].(*big.Int) + if !ok || amount.Sign() < 0 || amount == nil || amount == new(big.Int) { return nil, &ptypes.ErrInvalidAmount{ Got: amount.String(), } } + fmt.Printf("DEBUG: deposit(): zrc20Addr (ERC20ZRC20) %s\n", zrc20Addr.String()) + fmt.Printf("DEBUG: deposit(): caller %s\n", caller.String()) // Initialize the ZRC20 ABI, as we need to call the balanceOf and allowance methods. zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() if err != nil { + fmt.Printf("DEBUG: deposit(): zrc20.ZRC20MetaData.GetAbi() error %s\n", err.Error()) return nil, &ptypes.ErrUnexpected{ - When: "ZRC20MetaData.GetAbi", + When: "ZRC20MetaData.GetAbi()", Got: err.Error(), } } + fmt.Printf("DEBUG: deposit(): zrc20ABI %v\n", zrc20ABI) // Check for enough balance. // function balanceOf(address account) public view virtual override returns (uint256) - argsBalanceOf := []interface{}{caller} - - resBalanceOf, err := c.CallContract(ctx, zrc20ABI, zrc20Addr, "balanceOf", argsBalanceOf) + resBalanceOf, err := c.CallContract( + ctx, + &c.fungibleKeeper, + zrc20ABI, + ContractAddress, + zrc20Addr, + "balanceOf", + []interface{}{caller}, + ) if err != nil { + fmt.Printf("DEBUG: deposit(): balanceOf c.CallContract error %s\n", err.Error()) return nil, &ptypes.ErrUnexpected{ When: "balanceOf", Got: err.Error(), } } + fmt.Printf("DEBUG: deposit(): resBalanceOf %v\n", resBalanceOf) - balance := resBalanceOf[0].(*big.Int) - if balance.Cmp(amount) < 0 { - return nil, &ptypes.ErrUnexpected{ - When: "balance0f", - Got: "not enough balance", + balance, ok := resBalanceOf[0].(*big.Int) + if !ok || balance.Cmp(amount) < 0 { + return nil, &ptypes.ErrInvalidAmount{ + Got: "not enough balance", } } + fmt.Printf("DEBUG: deposit(): balanceOf caller %v\n", balance.Uint64()) // Check for enough allowance. // function allowance(address owner, address spender) public view virtual override returns (uint256) - argsAllowance := []interface{}{caller, ContractAddress} - - resAllowance, err := c.CallContract(ctx, zrc20ABI, zrc20Addr, "allowance", argsAllowance) + resAllowance, err := c.CallContract( + ctx, + &c.fungibleKeeper, + zrc20ABI, + ContractAddress, + zrc20Addr, + "allowance", + []interface{}{caller, ContractAddress}, + ) if err != nil { return nil, &ptypes.ErrUnexpected{ When: "allowance", @@ -78,13 +105,13 @@ func (c *Contract) deposit( } } - allowance := resAllowance[0].(*big.Int) - if allowance.Cmp(amount) < 0 { - return nil, &ptypes.ErrUnexpected{ - When: "allowance", - Got: "not enough allowance", + allowance, ok := resAllowance[0].(*big.Int) + if !ok || allowance.Cmp(amount) < 0 { + return nil, &ptypes.ErrInvalidAmount{ + Got: "not enough allowance", } } + fmt.Printf("DEBUG: deposit(): allowance caller %v\n", allowance.Uint64()) // Handle the toAddr: // check it's valid and not blocked. @@ -95,11 +122,12 @@ func (c *Contract) deposit( Reason: "empty address", } } + fmt.Printf("DEBUG: deposit(): caller toAddr %s\n", toAddr.String()) if c.bankKeeper.BlockedAddr(toAddr) { return nil, &ptypes.ErrInvalidAddr{ Got: toAddr.String(), - Reason: "blocked by bank keeper", + Reason: "destination address blocked by bank keeper", } } @@ -109,6 +137,7 @@ func (c *Contract) deposit( // - Mint coins. // - Send coins to the caller. tokenDenom := ZRC20ToCosmosDenom(zrc20Addr) + coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) if !coin.IsValid() { return nil, &ptypes.ErrInvalidCoin{ @@ -132,9 +161,15 @@ func (c *Contract) deposit( // 2. Effect: subtract balance. // function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) - argsTransferFrom := []interface{}{caller, ContractAddress, amount} - - resTransferFrom, err := c.CallContract(ctx, zrc20ABI, zrc20Addr, "transferFrom", argsTransferFrom) + resTransferFrom, err := c.CallContract( + ctx, + &c.fungibleKeeper, + zrc20ABI, + ContractAddress, + zrc20Addr, + "transferFrom", + []interface{}{caller, ContractAddress, amount}, + ) if err != nil { return nil, &ptypes.ErrUnexpected{ When: "transferFrom", @@ -142,28 +177,35 @@ func (c *Contract) deposit( } } - transferred := resTransferFrom[0].(bool) - if !transferred { + transferred, ok := resTransferFrom[0].(bool) + if !ok || !transferred { return nil, &ptypes.ErrUnexpected{ When: "transferFrom", Got: "transaction not successful", } } + fmt.Printf("DEBUG: deposit(): transferred %v\n", transferred) // 3. Interactions: create cosmos coin and send. - if err := c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet); err != nil { + err = c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet) + if err != nil { + fmt.Printf("DEBUG: deposit(): MintCoins error %s\n", err.Error()) return nil, &ptypes.ErrUnexpected{ When: "MintCoins", Got: err.Error(), } } + fmt.Printf("DEBUG: deposit(): MintCoins finished\n") - if err := c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet); err != nil { + err = c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet) + if err != nil { + fmt.Printf("DEBUG: deposit(): SendCoinsFromModuleToAccount error %s\n", err.Error()) return nil, &ptypes.ErrUnexpected{ When: "SendCoinsFromModuleToAccount", Got: err.Error(), } } + fmt.Printf("DEBUG: deposit(): SendCoinsFromModuleToAccount finished\n") return method.Outputs.Pack(true) } diff --git a/precompiles/types/types.go b/precompiles/types/types.go index 7bd4a57bfa..9d33b7f822 100644 --- a/precompiles/types/types.go +++ b/precompiles/types/types.go @@ -4,9 +4,12 @@ import ( "math/big" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "github.com/zeta-chain/ethermint/x/evm/statedb" + + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) // Interface compliance. @@ -30,8 +33,19 @@ type Registrable interface { RegistryKey() common.Address } +type ContractCaller interface { + CallContract(ctx sdk.Context, + fungibleKeeper *fungiblekeeper.Keeper, + abi *abi.ABI, + from common.Address, + dst common.Address, + method string, + args []interface{}) ([]interface{}, error) +} + type BaseContract interface { Registrable + ContractCaller } // A baseContract implements Registrable and BaseContract interfaces. @@ -52,3 +66,49 @@ func (c *baseContract) RegistryKey() common.Address { func BytesToBigInt(data []byte) *big.Int { return big.NewInt(0).SetBytes(data[:]) } + +func (c *baseContract) CallContract( + ctx sdk.Context, + fungibleKeeper *fungiblekeeper.Keeper, + abi *abi.ABI, + from common.Address, + dst common.Address, + method string, + args []interface{}, +) ([]interface{}, error) { + res, err := fungibleKeeper.CallEVM( + ctx, // ctx + *abi, // abi + from, // from + dst, // to + big.NewInt(0), // value + nil, // gasLimit + true, // commit + false, // noEthereumTxEvent + method, // method + args..., // args + ) + if err != nil { + return nil, &ErrUnexpected{ + When: "CallEVM " + method, + Got: err.Error(), + } + } + + if res.VmError != "" { + return nil, &ErrUnexpected{ + When: "VmError " + method, + Got: res.VmError, + } + } + + ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) + if err != nil { + return nil, &ErrUnexpected{ + When: "Unpack " + method, + Got: err.Error(), + } + } + + return ret, nil +} diff --git a/x/fungible/keeper/evm.go b/x/fungible/keeper/evm.go index ab0332dfe3..0ea4d4df87 100644 --- a/x/fungible/keeper/evm.go +++ b/x/fungible/keeper/evm.go @@ -668,6 +668,7 @@ func (k Keeper) CallEVM( } k.Logger(ctx).Debug("calling EVM", "from", from, "contract", contract, "value", value, "method", method) + fmt.Printf("DEBUG: CallEVM: calling EVM from %s contract %s value %s method %s\n", from, contract, value, method) resp, err := k.CallEVMWithData(ctx, from, &contract, data, commit, noEthereumTxEvent, value, gasLimit) if err != nil { errMes := fmt.Sprintf( @@ -707,18 +708,30 @@ func (k Keeper) CallEVMWithData( value *big.Int, gasLimit *big.Int, ) (*evmtypes.MsgEthereumTxResponse, error) { + fmt.Printf( + "DEBUG: CallEVMWithData with from %s, contract %s, commit %v, gasLimit %v, noEthereumTxEvent %v, value %v\n", + from, + contract, + commit, + gasLimit, + noEthereumTxEvent, + value, + ) nonce, err := k.authKeeper.GetSequence(ctx, from.Bytes()) if err != nil { + fmt.Printf("DEBUG: CallEVMWithData error GetSequence: %s\n", err.Error()) return nil, err } gasCap := config.DefaultGasCap if commit && gasLimit == nil { + fmt.Printf("DEBUG: CallEVMWithData entered if commit and gasLimit == nil\n") args, err := json.Marshal(evmtypes.TransactionArgs{ From: &from, To: contract, Data: (*hexutil.Bytes)(&data), }) if err != nil { + fmt.Printf("DEBUG: CallEVMWithData entered if commit and gasLimit == nil, ERROR: %s\n", err.Error()) return nil, cosmoserrors.Wrapf(sdkerrors.ErrJSONMarshal, "failed to marshal tx args: %s", err.Error()) } @@ -727,6 +740,7 @@ func (k Keeper) CallEVMWithData( GasCap: config.DefaultGasCap, }) if err != nil { + fmt.Printf("DEBUG: CallEVMWithData error EstimateGas: %s\n", err.Error()) return nil, err } gasCap = gasRes.Gas @@ -750,13 +764,21 @@ func (k Keeper) CallEVMWithData( ) k.evmKeeper.WithChainID(ctx) //FIXME: set chainID for signer; should not need to do this; but seems necessary. Why? k.Logger(ctx).Debug("call evm", "gasCap", gasCap, "chainid", k.evmKeeper.ChainID(), "ctx.chainid", ctx.ChainID()) + fmt.Printf( + "DEBUG: CallEVMWithData: call evm gasCap %d chainid %d ctx.chainid %d\n", + gasCap, + k.evmKeeper.ChainID(), + ctx.ChainID(), + ) res, err := k.evmKeeper.ApplyMessage(ctx, msg, evmtypes.NewNoOpTracer(), commit) if err != nil { + fmt.Printf("DEBUG: ApplyMessage error: %s\n", err.Error()) return nil, err } // Emit events and log for the transaction if it is committed if commit { + fmt.Printf("DEBUG: Enter commit\n") msgBytes, err := json.Marshal(msg) if err != nil { return nil, cosmoserrors.Wrap(err, "failed to encode msg") @@ -830,8 +852,10 @@ func (k Keeper) CallEVMWithData( k.evmKeeper.SetLogSizeTransient(ctx, (k.evmKeeper.GetLogSizeTransient(ctx))+uint64(len(logs))) } } + fmt.Printf("DEBUG: Finish commit\n") if res.Failed() { + fmt.Printf("DEBUG: Enter res.Failed()\n") return res, cosmoserrors.Wrapf(evmtypes.ErrVMExecution, "%s: ret 0x%x", res.VmError, res.Ret) } From c1836a3f7f08170d3b040c2c6b775ebcbb524482 Mon Sep 17 00:00:00 2001 From: skosito Date: Mon, 16 Sep 2024 19:23:33 +0200 Subject: [PATCH 17/33] fix eth events in deposit precompile method --- e2e/e2etests/test_precompiles_bank.go | 3 +-- precompiles/bank/method_deposit.go | 3 +++ precompiles/types/types.go | 22 ++++++++++++---------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 9fda3492ca..d081882d95 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -61,9 +61,8 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { tx, err = bankContract.Deposit(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) fmt.Printf("DEBUG: bank.deposit() tx hash %s\n", tx.Hash().String()) require.NoError(r, err, "Error calling bank.deposit()") + utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - r.Logger.Info("Waiting for 5 blocks") - r.WaitForBlocks(5) fmt.Printf("DEBUG: bank.deposit() tx %+v\n", tx) // Check the balance of the spender in coins "zevm/WZetaAddr". diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 4a8cf39d9d..78387391f7 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -68,6 +68,7 @@ func (c *Contract) deposit( ContractAddress, zrc20Addr, "balanceOf", + true, []interface{}{caller}, ) if err != nil { @@ -96,6 +97,7 @@ func (c *Contract) deposit( ContractAddress, zrc20Addr, "allowance", + true, []interface{}{caller, ContractAddress}, ) if err != nil { @@ -168,6 +170,7 @@ func (c *Contract) deposit( ContractAddress, zrc20Addr, "transferFrom", + true, []interface{}{caller, ContractAddress, amount}, ) if err != nil { diff --git a/precompiles/types/types.go b/precompiles/types/types.go index 9d33b7f822..a071b3fa90 100644 --- a/precompiles/types/types.go +++ b/precompiles/types/types.go @@ -40,6 +40,7 @@ type ContractCaller interface { from common.Address, dst common.Address, method string, + noEthereumTxEvent bool, args []interface{}) ([]interface{}, error) } @@ -74,19 +75,20 @@ func (c *baseContract) CallContract( from common.Address, dst common.Address, method string, + noEthereumTxEvent bool, args []interface{}, ) ([]interface{}, error) { res, err := fungibleKeeper.CallEVM( - ctx, // ctx - *abi, // abi - from, // from - dst, // to - big.NewInt(0), // value - nil, // gasLimit - true, // commit - false, // noEthereumTxEvent - method, // method - args..., // args + ctx, // ctx + *abi, // abi + from, // from + dst, // to + big.NewInt(0), // value + nil, // gasLimit + true, // commit + noEthereumTxEvent, // noEthereumTxEvent + method, // method + args..., // args ) if err != nil { return nil, &ErrUnexpected{ From 8503dc818639e63363c9408b95c388ccdbfe5b6a Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Mon, 16 Sep 2024 20:43:19 +0200 Subject: [PATCH 18/33] emit Deposit event --- e2e/e2etests/test_precompiles_bank.go | 13 ++- precompiles/bank/IBank.abi | 25 +++++ precompiles/bank/IBank.go | 156 +++++++++++++++++++++++++- precompiles/bank/IBank.json | 25 +++++ precompiles/bank/IBank.sol | 10 ++ precompiles/bank/bank.go | 2 +- precompiles/bank/logs.go | 46 ++++++++ precompiles/bank/method_deposit.go | 21 +++- precompiles/types/types.go | 4 +- 9 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 precompiles/bank/logs.go diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index d081882d95..f833917005 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -5,6 +5,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" @@ -61,9 +62,15 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { tx, err = bankContract.Deposit(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) fmt.Printf("DEBUG: bank.deposit() tx hash %s\n", tx.Hash().String()) require.NoError(r, err, "Error calling bank.deposit()") - utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - - fmt.Printf("DEBUG: bank.deposit() tx %+v\n", tx) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + + // Deposit event should be emitted. + depositEvent, err := bankContract.ParseDeposit(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, big.NewInt(25).Uint64(), depositEvent.Amount.Uint64()) + require.Equal(r, common.BytesToAddress(spender.Bytes()), depositEvent.Depositor) + require.Equal(r, r.WZetaAddr, depositEvent.Token) + fmt.Println("Deposit event emitted ", depositEvent) // Check the balance of the spender in coins "zevm/WZetaAddr". retBalanceOf, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) diff --git a/precompiles/bank/IBank.abi b/precompiles/bank/IBank.abi index 03eee6d210..b370e21ae7 100644 --- a/precompiles/bank/IBank.abi +++ b/precompiles/bank/IBank.abi @@ -1,4 +1,29 @@ [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, { "inputs": [ { diff --git a/precompiles/bank/IBank.go b/precompiles/bank/IBank.go index 8f95c51c08..acf63a7e45 100644 --- a/precompiles/bank/IBank.go +++ b/precompiles/bank/IBank.go @@ -31,7 +31,7 @@ var ( // IBankMetaData contains all meta data concerning the IBank contract. var IBankMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", } // IBankABI is the input ABI used to generate the binding from. @@ -252,3 +252,157 @@ func (_IBank *IBankSession) Withdraw(zrc20 common.Address, amount *big.Int) (*ty func (_IBank *IBankTransactorSession) Withdraw(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { return _IBank.Contract.Withdraw(&_IBank.TransactOpts, zrc20, amount) } + +// IBankDepositIterator is returned from FilterDeposit and is used to iterate over the raw logs and unpacked data for Deposit events raised by the IBank contract. +type IBankDepositIterator struct { + Event *IBankDeposit // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IBankDepositIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IBankDeposit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IBankDeposit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IBankDepositIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IBankDepositIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IBankDeposit represents a Deposit event raised by the IBank contract. +type IBankDeposit struct { + Depositor common.Address + Token common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDeposit is a free log retrieval operation binding the contract event 0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62. +// +// Solidity: event Deposit(address indexed depositor, address indexed token, uint256 amount) +func (_IBank *IBankFilterer) FilterDeposit(opts *bind.FilterOpts, depositor []common.Address, token []common.Address) (*IBankDepositIterator, error) { + + var depositorRule []interface{} + for _, depositorItem := range depositor { + depositorRule = append(depositorRule, depositorItem) + } + var tokenRule []interface{} + for _, tokenItem := range token { + tokenRule = append(tokenRule, tokenItem) + } + + logs, sub, err := _IBank.contract.FilterLogs(opts, "Deposit", depositorRule, tokenRule) + if err != nil { + return nil, err + } + return &IBankDepositIterator{contract: _IBank.contract, event: "Deposit", logs: logs, sub: sub}, nil +} + +// WatchDeposit is a free log subscription operation binding the contract event 0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62. +// +// Solidity: event Deposit(address indexed depositor, address indexed token, uint256 amount) +func (_IBank *IBankFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *IBankDeposit, depositor []common.Address, token []common.Address) (event.Subscription, error) { + + var depositorRule []interface{} + for _, depositorItem := range depositor { + depositorRule = append(depositorRule, depositorItem) + } + var tokenRule []interface{} + for _, tokenItem := range token { + tokenRule = append(tokenRule, tokenItem) + } + + logs, sub, err := _IBank.contract.WatchLogs(opts, "Deposit", depositorRule, tokenRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IBankDeposit) + if err := _IBank.contract.UnpackLog(event, "Deposit", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDeposit is a log parse operation binding the contract event 0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62. +// +// Solidity: event Deposit(address indexed depositor, address indexed token, uint256 amount) +func (_IBank *IBankFilterer) ParseDeposit(log types.Log) (*IBankDeposit, error) { + event := new(IBankDeposit) + if err := _IBank.contract.UnpackLog(event, "Deposit", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/precompiles/bank/IBank.json b/precompiles/bank/IBank.json index 808e0039c0..e54f871005 100644 --- a/precompiles/bank/IBank.json +++ b/precompiles/bank/IBank.json @@ -1,5 +1,30 @@ { "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, { "inputs": [ { diff --git a/precompiles/bank/IBank.sol b/precompiles/bank/IBank.sol index 7926d6e695..43c427409b 100644 --- a/precompiles/bank/IBank.sol +++ b/precompiles/bank/IBank.sol @@ -14,6 +14,16 @@ IBank constant IBANK_CONTRACT = IBank(IBANK_PRECOMPILE_ADDRESS); /// @dev Interface for the IBank contract. interface IBank { + /// @notice Deposit event is emitted when deposit function is called. + /// @param depositor Depositor address. + /// @param token ZRC20 address deposited. + /// @param amount Amount deposited. + event Deposit( + address indexed depositor, + address indexed token, + uint256 amount + ); + /// @notice Deposit a ZRC20 token and mint the corresponding Cosmos token to the user's account. /// @param zrc20 The ZRC20 token address to be deposited. /// @param amount The amount of ZRC20 tokens to deposit. diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 7acebd1ab2..2d18d362b6 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -130,7 +130,7 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { fmt.Println("DEBUG: bank.Run(): DepositMethodName: ExecuteNativeAction c.deposit()") - res, err = c.deposit(ctx, method, contract.CallerAddress, args) + res, err = c.deposit(ctx, evm, contract, method, args) return err }) if execErr != nil { diff --git a/precompiles/bank/logs.go b/precompiles/bank/logs.go new file mode 100644 index 0000000000..64e5fca021 --- /dev/null +++ b/precompiles/bank/logs.go @@ -0,0 +1,46 @@ +package bank + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/zeta-chain/node/precompiles/logs" +) + +const ( + DepositEventName = "Deposit" + WithdrawEventName = "Withdraw" +) + +func (c *Contract) AddDepositLog( + ctx sdk.Context, + stateDB vm.StateDB, + depositor common.Address, + token common.Address, + amount *big.Int, +) error { + event := c.Abi().Events[DepositEventName] + + // depositor and ZRC20 address are indexed. + topics, err := logs.MakeTopics( + event, + []interface{}{common.BytesToAddress(depositor.Bytes())}, + []interface{}{common.BytesToAddress(token.Bytes())}, + ) + if err != nil { + return err + } + + // amount is part of event data. + data, err := logs.PackBigInt(amount) + if err != nil { + return err + } + + logs.AddLog(ctx, c.Address(), stateDB, topics, data) + + return nil +} diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 78387391f7..aca1868d1b 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" ptypes "github.com/zeta-chain/node/precompiles/types" @@ -16,8 +17,9 @@ import ( func (c *Contract) deposit( ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, method *abi.Method, - caller common.Address, args []interface{}, ) (result []byte, err error) { fmt.Printf("DEBUG: deposit()\n") @@ -30,6 +32,13 @@ func (c *Contract) deposit( }) } + // caller is usually the CallerAddress, except when the call was made through a contract. + // For those cases set the caller to the evm.Origin. + caller := contract.CallerAddress + if contract.CallerAddress != evm.Origin { + caller = evm.Origin + } + // Unpack parameters for function deposit. // function deposit(address zrc20, uint256 amount) external returns (bool success); zrc20Addr, ok := args[0].(common.Address) @@ -65,7 +74,6 @@ func (c *Contract) deposit( ctx, &c.fungibleKeeper, zrc20ABI, - ContractAddress, zrc20Addr, "balanceOf", true, @@ -94,7 +102,6 @@ func (c *Contract) deposit( ctx, &c.fungibleKeeper, zrc20ABI, - ContractAddress, zrc20Addr, "allowance", true, @@ -167,7 +174,6 @@ func (c *Contract) deposit( ctx, &c.fungibleKeeper, zrc20ABI, - ContractAddress, zrc20Addr, "transferFrom", true, @@ -210,5 +216,12 @@ func (c *Contract) deposit( } fmt.Printf("DEBUG: deposit(): SendCoinsFromModuleToAccount finished\n") + if err := c.AddDepositLog(ctx, evm.StateDB, caller, zrc20Addr, amount); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "AddDepositLog", + Got: err.Error(), + } + } + return method.Outputs.Pack(true) } diff --git a/precompiles/types/types.go b/precompiles/types/types.go index a071b3fa90..70415e675f 100644 --- a/precompiles/types/types.go +++ b/precompiles/types/types.go @@ -37,7 +37,6 @@ type ContractCaller interface { CallContract(ctx sdk.Context, fungibleKeeper *fungiblekeeper.Keeper, abi *abi.ABI, - from common.Address, dst common.Address, method string, noEthereumTxEvent bool, @@ -72,7 +71,6 @@ func (c *baseContract) CallContract( ctx sdk.Context, fungibleKeeper *fungiblekeeper.Keeper, abi *abi.ABI, - from common.Address, dst common.Address, method string, noEthereumTxEvent bool, @@ -81,7 +79,7 @@ func (c *baseContract) CallContract( res, err := fungibleKeeper.CallEVM( ctx, // ctx *abi, // abi - from, // from + c.RegistryKey(), // from dst, // to big.NewInt(0), // value nil, // gasLimit From 6a19fb08f6841d822ea9bdd0aaaf9b8fc7a367e9 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 17 Sep 2024 11:59:06 +0200 Subject: [PATCH 19/33] add withdraw function --- e2e/e2etests/test_precompiles_bank.go | 4 +- precompiles/bank/IBank.abi | 41 ++++- precompiles/bank/IBank.go | 226 +++++++++++++++++++++++--- precompiles/bank/IBank.json | 41 ++++- precompiles/bank/IBank.sol | 22 ++- precompiles/bank/bank.go | 68 ++++++-- precompiles/bank/coin.go | 39 ++++- precompiles/bank/logs.go | 44 ++++- precompiles/bank/method_balance_of.go | 38 +++-- precompiles/bank/method_deposit.go | 124 +++++--------- precompiles/types/errors.go | 9 + precompiles/types/types.go | 27 +-- x/fungible/keeper/evm.go | 24 --- 13 files changed, 515 insertions(+), 192 deletions(-) diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index f833917005..2bcd780c5f 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -68,8 +68,8 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { depositEvent, err := bankContract.ParseDeposit(*receipt.Logs[0]) require.NoError(r, err) require.Equal(r, big.NewInt(25).Uint64(), depositEvent.Amount.Uint64()) - require.Equal(r, common.BytesToAddress(spender.Bytes()), depositEvent.Depositor) - require.Equal(r, r.WZetaAddr, depositEvent.Token) + require.Equal(r, common.BytesToAddress(spender.Bytes()), depositEvent.Zrc20Depositor) + require.Equal(r, r.WZetaAddr, depositEvent.Zrc20Token) fmt.Println("Deposit event emitted ", depositEvent) // Check the balance of the spender in coins "zevm/WZetaAddr". diff --git a/precompiles/bank/IBank.abi b/precompiles/bank/IBank.abi index b370e21ae7..3d8a84508d 100644 --- a/precompiles/bank/IBank.abi +++ b/precompiles/bank/IBank.abi @@ -5,15 +5,21 @@ { "indexed": true, "internalType": "address", - "name": "depositor", + "name": "zrc20_depositor", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "token", + "name": "zrc20_token", "type": "address" }, + { + "indexed": true, + "internalType": "string", + "name": "cosmos_token", + "type": "string" + }, { "indexed": false, "internalType": "uint256", @@ -24,6 +30,37 @@ "name": "Deposit", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_withdrawer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": true, + "internalType": "string", + "name": "cosmos_token", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, { "inputs": [ { diff --git a/precompiles/bank/IBank.go b/precompiles/bank/IBank.go index acf63a7e45..a2c55cf145 100644 --- a/precompiles/bank/IBank.go +++ b/precompiles/bank/IBank.go @@ -31,7 +31,7 @@ var ( // IBankMetaData contains all meta data concerning the IBank contract. var IBankMetaData = &bind.MetaData{ - ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_depositor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"string\",\"name\":\"cosmos_token\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_withdrawer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"string\",\"name\":\"cosmos_token\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdraw\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", } // IBankABI is the input ABI used to generate the binding from. @@ -322,48 +322,57 @@ func (it *IBankDepositIterator) Close() error { // IBankDeposit represents a Deposit event raised by the IBank contract. type IBankDeposit struct { - Depositor common.Address - Token common.Address - Amount *big.Int - Raw types.Log // Blockchain specific contextual infos + Zrc20Depositor common.Address + Zrc20Token common.Address + CosmosToken common.Hash + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos } -// FilterDeposit is a free log retrieval operation binding the contract event 0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62. +// FilterDeposit is a free log retrieval operation binding the contract event 0x2dc24880b34b2026fb1cd0231e65ef42ca1c78e4efdd9711b7629740206bfa46. // -// Solidity: event Deposit(address indexed depositor, address indexed token, uint256 amount) -func (_IBank *IBankFilterer) FilterDeposit(opts *bind.FilterOpts, depositor []common.Address, token []common.Address) (*IBankDepositIterator, error) { +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +func (_IBank *IBankFilterer) FilterDeposit(opts *bind.FilterOpts, zrc20_depositor []common.Address, zrc20_token []common.Address, cosmos_token []string) (*IBankDepositIterator, error) { - var depositorRule []interface{} - for _, depositorItem := range depositor { - depositorRule = append(depositorRule, depositorItem) + var zrc20_depositorRule []interface{} + for _, zrc20_depositorItem := range zrc20_depositor { + zrc20_depositorRule = append(zrc20_depositorRule, zrc20_depositorItem) } - var tokenRule []interface{} - for _, tokenItem := range token { - tokenRule = append(tokenRule, tokenItem) + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var cosmos_tokenRule []interface{} + for _, cosmos_tokenItem := range cosmos_token { + cosmos_tokenRule = append(cosmos_tokenRule, cosmos_tokenItem) } - logs, sub, err := _IBank.contract.FilterLogs(opts, "Deposit", depositorRule, tokenRule) + logs, sub, err := _IBank.contract.FilterLogs(opts, "Deposit", zrc20_depositorRule, zrc20_tokenRule, cosmos_tokenRule) if err != nil { return nil, err } return &IBankDepositIterator{contract: _IBank.contract, event: "Deposit", logs: logs, sub: sub}, nil } -// WatchDeposit is a free log subscription operation binding the contract event 0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62. +// WatchDeposit is a free log subscription operation binding the contract event 0x2dc24880b34b2026fb1cd0231e65ef42ca1c78e4efdd9711b7629740206bfa46. // -// Solidity: event Deposit(address indexed depositor, address indexed token, uint256 amount) -func (_IBank *IBankFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *IBankDeposit, depositor []common.Address, token []common.Address) (event.Subscription, error) { +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +func (_IBank *IBankFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *IBankDeposit, zrc20_depositor []common.Address, zrc20_token []common.Address, cosmos_token []string) (event.Subscription, error) { - var depositorRule []interface{} - for _, depositorItem := range depositor { - depositorRule = append(depositorRule, depositorItem) + var zrc20_depositorRule []interface{} + for _, zrc20_depositorItem := range zrc20_depositor { + zrc20_depositorRule = append(zrc20_depositorRule, zrc20_depositorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) } - var tokenRule []interface{} - for _, tokenItem := range token { - tokenRule = append(tokenRule, tokenItem) + var cosmos_tokenRule []interface{} + for _, cosmos_tokenItem := range cosmos_token { + cosmos_tokenRule = append(cosmos_tokenRule, cosmos_tokenItem) } - logs, sub, err := _IBank.contract.WatchLogs(opts, "Deposit", depositorRule, tokenRule) + logs, sub, err := _IBank.contract.WatchLogs(opts, "Deposit", zrc20_depositorRule, zrc20_tokenRule, cosmos_tokenRule) if err != nil { return nil, err } @@ -395,9 +404,9 @@ func (_IBank *IBankFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *IBa }), nil } -// ParseDeposit is a log parse operation binding the contract event 0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62. +// ParseDeposit is a log parse operation binding the contract event 0x2dc24880b34b2026fb1cd0231e65ef42ca1c78e4efdd9711b7629740206bfa46. // -// Solidity: event Deposit(address indexed depositor, address indexed token, uint256 amount) +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) func (_IBank *IBankFilterer) ParseDeposit(log types.Log) (*IBankDeposit, error) { event := new(IBankDeposit) if err := _IBank.contract.UnpackLog(event, "Deposit", log); err != nil { @@ -406,3 +415,166 @@ func (_IBank *IBankFilterer) ParseDeposit(log types.Log) (*IBankDeposit, error) event.Raw = log return event, nil } + +// IBankWithdrawIterator is returned from FilterWithdraw and is used to iterate over the raw logs and unpacked data for Withdraw events raised by the IBank contract. +type IBankWithdrawIterator struct { + Event *IBankWithdraw // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IBankWithdrawIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IBankWithdraw) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IBankWithdraw) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IBankWithdrawIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IBankWithdrawIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IBankWithdraw represents a Withdraw event raised by the IBank contract. +type IBankWithdraw struct { + Zrc20Withdrawer common.Address + Zrc20Token common.Address + CosmosToken common.Hash + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWithdraw is a free log retrieval operation binding the contract event 0x4b230e98ab931f2b86715011c55a66078553f7ae5933f387d1e23954b2b9d19a. +// +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +func (_IBank *IBankFilterer) FilterWithdraw(opts *bind.FilterOpts, zrc20_withdrawer []common.Address, zrc20_token []common.Address, cosmos_token []string) (*IBankWithdrawIterator, error) { + + var zrc20_withdrawerRule []interface{} + for _, zrc20_withdrawerItem := range zrc20_withdrawer { + zrc20_withdrawerRule = append(zrc20_withdrawerRule, zrc20_withdrawerItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var cosmos_tokenRule []interface{} + for _, cosmos_tokenItem := range cosmos_token { + cosmos_tokenRule = append(cosmos_tokenRule, cosmos_tokenItem) + } + + logs, sub, err := _IBank.contract.FilterLogs(opts, "Withdraw", zrc20_withdrawerRule, zrc20_tokenRule, cosmos_tokenRule) + if err != nil { + return nil, err + } + return &IBankWithdrawIterator{contract: _IBank.contract, event: "Withdraw", logs: logs, sub: sub}, nil +} + +// WatchWithdraw is a free log subscription operation binding the contract event 0x4b230e98ab931f2b86715011c55a66078553f7ae5933f387d1e23954b2b9d19a. +// +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +func (_IBank *IBankFilterer) WatchWithdraw(opts *bind.WatchOpts, sink chan<- *IBankWithdraw, zrc20_withdrawer []common.Address, zrc20_token []common.Address, cosmos_token []string) (event.Subscription, error) { + + var zrc20_withdrawerRule []interface{} + for _, zrc20_withdrawerItem := range zrc20_withdrawer { + zrc20_withdrawerRule = append(zrc20_withdrawerRule, zrc20_withdrawerItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var cosmos_tokenRule []interface{} + for _, cosmos_tokenItem := range cosmos_token { + cosmos_tokenRule = append(cosmos_tokenRule, cosmos_tokenItem) + } + + logs, sub, err := _IBank.contract.WatchLogs(opts, "Withdraw", zrc20_withdrawerRule, zrc20_tokenRule, cosmos_tokenRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IBankWithdraw) + if err := _IBank.contract.UnpackLog(event, "Withdraw", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseWithdraw is a log parse operation binding the contract event 0x4b230e98ab931f2b86715011c55a66078553f7ae5933f387d1e23954b2b9d19a. +// +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +func (_IBank *IBankFilterer) ParseWithdraw(log types.Log) (*IBankWithdraw, error) { + event := new(IBankWithdraw) + if err := _IBank.contract.UnpackLog(event, "Withdraw", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/precompiles/bank/IBank.json b/precompiles/bank/IBank.json index e54f871005..6193826a71 100644 --- a/precompiles/bank/IBank.json +++ b/precompiles/bank/IBank.json @@ -6,15 +6,21 @@ { "indexed": true, "internalType": "address", - "name": "depositor", + "name": "zrc20_depositor", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "token", + "name": "zrc20_token", "type": "address" }, + { + "indexed": true, + "internalType": "string", + "name": "cosmos_token", + "type": "string" + }, { "indexed": false, "internalType": "uint256", @@ -25,6 +31,37 @@ "name": "Deposit", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_withdrawer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": true, + "internalType": "string", + "name": "cosmos_token", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, { "inputs": [ { diff --git a/precompiles/bank/IBank.sol b/precompiles/bank/IBank.sol index 43c427409b..5d4a25c271 100644 --- a/precompiles/bank/IBank.sol +++ b/precompiles/bank/IBank.sol @@ -15,12 +15,26 @@ IBank constant IBANK_CONTRACT = IBank(IBANK_PRECOMPILE_ADDRESS); /// @dev Interface for the IBank contract. interface IBank { /// @notice Deposit event is emitted when deposit function is called. - /// @param depositor Depositor address. - /// @param token ZRC20 address deposited. + /// @param zrc20_depositor Depositor EVM address. + /// @param zrc20_token ZRC20 address deposited. + /// @param cosmos_token Cosmos token denomination the tokens were converted into. /// @param amount Amount deposited. event Deposit( - address indexed depositor, - address indexed token, + address indexed zrc20_depositor, + address indexed zrc20_token, + string indexed cosmos_token, + uint256 amount + ); + + /// @notice Withdraw event is emitted when withdraw function is called. + /// @param zrc20_withdrawer Withdrawer EVM address. + /// @param zrc20_token ZRC20 address withdrawn. + /// @param cosmos_token Cosmos token denomination the tokens were converted from. + /// @param amount Amount withdrawn. + event Withdraw( + address indexed zrc20_withdrawer, + address indexed zrc20_token, + string indexed cosmos_token, uint256 amount ); diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 2d18d362b6..db4c5e4d64 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" ptypes "github.com/zeta-chain/node/precompiles/types" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" @@ -53,6 +54,7 @@ type Contract struct { bankKeeper bank.Keeper fungibleKeeper fungiblekeeper.Keeper + zrc20ABI *abi.ABI cdc codec.Codec kvGasConfig storetypes.GasConfig } @@ -63,10 +65,18 @@ func NewIBankContract( cdc codec.Codec, kvGasConfig storetypes.GasConfig, ) *Contract { + // Instantiate the ZRC20 ABI only one time. + // This avoids instantiating it every time deposit or withdraw are called. + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + if err != nil { + return nil + } + return &Contract{ BaseContract: ptypes.NewBaseContract(ContractAddress), bankKeeper: bankKeeper, fungibleKeeper: fungibleKeeper, + zrc20ABI: zrc20ABI, cdc: cdc, kvGasConfig: kvGasConfig, } @@ -105,7 +115,6 @@ func (c *Contract) RequiredGas(input []byte) uint64 { // Run is the entrypoint of the precompiled contract, it switches over the input method, // and execute them accordingly. func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byte, error) { - fmt.Println("DEBUG: bank.Run()") method, err := ABI.MethodById(contract.Input[:4]) if err != nil { return nil, err @@ -119,8 +128,8 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt stateDB := evm.StateDB.(ptypes.ExtStateDB) switch method.Name { - case DepositMethodName: - fmt.Println("DEBUG: bank.Run(): DepositMethodName") + // Deposit and Withdraw methods are both not allowed in read-only mode. + case DepositMethodName, WithdrawMethodName: if readOnly { return nil, ptypes.ErrUnexpected{ Got: "method not allowed in read-only mode " + method.Name, @@ -129,8 +138,13 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { - fmt.Println("DEBUG: bank.Run(): DepositMethodName: ExecuteNativeAction c.deposit()") - res, err = c.deposit(ctx, evm, contract, method, args) + if method.Name == DepositMethodName { + fmt.Println("DEBUG: bank.Run(): DepositMethodName: ExecuteNativeAction c.deposit()") + res, err = c.deposit(ctx, evm, contract, method, args) + } else { + fmt.Println("DEBUG: bank.Run(): WithdrawMethodName: ExecuteNativeAction c.withdraw()") + res, err = c.withdraw(ctx, evm, contract, method, args) + } return err }) if execErr != nil { @@ -139,16 +153,6 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt } return res, nil - case WithdrawMethodName: - if readOnly { - return nil, ptypes.ErrUnexpected{ - Got: "method not allowed in read-only mode " + method.Name, - } - } - - return nil, nil - // TODO - case BalanceOfMethodName: fmt.Println("DEBUG: bank.Run(): BalanceOfMethodName") var res []byte @@ -168,3 +172,37 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt } } } + +// getEVMCallerAddress returns the caller address. +// Usually the caller is the contract.CallerAddress, which is the address of the contract that called the precompiled contract. +// If contract.CallerAddress != evm.Origin is true, it means the call was made through a contract, +// on which case there is a need to set the caller to the evm.Origin. +func getEVMCallerAddress(evm *vm.EVM, contract *vm.Contract) (common.Address, error) { + caller := contract.CallerAddress + if contract.CallerAddress != evm.Origin { + caller = evm.Origin + } + + return caller, nil +} + +// getCosmosAddress returns the counterpart cosmos address of the given ethereum address. +// It checks if the address is empty or blocked by the bank keeper. +func getCosmosAddress(bankKeeper bank.Keeper, addr common.Address) (sdk.AccAddress, error) { + toAddr := sdk.AccAddress(addr.Bytes()) + if toAddr.Empty() { + return nil, &ptypes.ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "empty address", + } + } + + if bankKeeper.BlockedAddr(toAddr) { + return nil, &ptypes.ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "destination address blocked by bank keeper", + } + } + + return toAddr, nil +} diff --git a/precompiles/bank/coin.go b/precompiles/bank/coin.go index c6ff0a262e..eddf6d4a37 100644 --- a/precompiles/bank/coin.go +++ b/precompiles/bank/coin.go @@ -1,7 +1,44 @@ package bank -import "github.com/ethereum/go-ethereum/common" +import ( + "math/big" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + + ptypes "github.com/zeta-chain/node/precompiles/types" +) + +// ZRC20ToCosmosDenom returns the cosmos coin address for a given ZRC20 address. +// This is converted to "zevm/{ZRC20Address}". func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { return ZEVMDenom + ZRC20Address.String() } + +// createCoinSet creates a sdk.Coins from a tokenDenom and an amount. +// It's mostly a helper function to avoid code duplication. +func createCoinSet(tokenDenom string, amount *big.Int) (sdk.Coins, error) { + coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) + if !coin.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coin.GetDenom(), + Negative: coin.IsNegative(), + Nil: coin.IsNil(), + } + } + + // A sdk.Coins (type []sdk.Coin) has to be created because it's the type expected by MintCoins + // and SendCoinsFromModuleToAccount. + // But sdk.Coins will only contain one coin, always. + coinSet := sdk.NewCoins(coin) + if !coinSet.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coinSet.Sort().GetDenomByIndex(0), + Negative: coinSet.IsAnyNegative(), + Nil: coinSet.IsAnyNil(), + } + } + + return coinSet, nil +} diff --git a/precompiles/bank/logs.go b/precompiles/bank/logs.go index 64e5fca021..6e7d8398cf 100644 --- a/precompiles/bank/logs.go +++ b/precompiles/bank/logs.go @@ -18,17 +18,51 @@ const ( func (c *Contract) AddDepositLog( ctx sdk.Context, stateDB vm.StateDB, - depositor common.Address, - token common.Address, + zrc20Depositor common.Address, + zrc20Token common.Address, + cosmosCoin string, amount *big.Int, ) error { event := c.Abi().Events[DepositEventName] - // depositor and ZRC20 address are indexed. + // ZRC20, cosmos coin and depositor. topics, err := logs.MakeTopics( event, - []interface{}{common.BytesToAddress(depositor.Bytes())}, - []interface{}{common.BytesToAddress(token.Bytes())}, + []interface{}{common.BytesToAddress(zrc20Depositor.Bytes())}, + []interface{}{common.BytesToAddress(zrc20Token.Bytes())}, + []interface{}{cosmosCoin}, + ) + if err != nil { + return err + } + + // amount is part of event data. + data, err := logs.PackBigInt(amount) + if err != nil { + return err + } + + logs.AddLog(ctx, c.Address(), stateDB, topics, data) + + return nil +} + +func (c *Contract) AddWithdrawLog( + ctx sdk.Context, + stateDB vm.StateDB, + zrc20Withdrawer common.Address, + zrc20Token common.Address, + cosmosCoin string, + amount *big.Int, +) error { + event := c.Abi().Events[WithdrawEventName] + + // ZRC20, cosmos coin and witgdrawer are indexed. + topics, err := logs.MakeTopics( + event, + []interface{}{common.BytesToAddress(zrc20Withdrawer.Bytes())}, + []interface{}{common.BytesToAddress(zrc20Token.Bytes())}, + []interface{}{cosmosCoin}, ) if err != nil { return err diff --git a/precompiles/bank/method_balance_of.go b/precompiles/bank/method_balance_of.go index 637dd76cf4..1045a393df 100644 --- a/precompiles/bank/method_balance_of.go +++ b/precompiles/bank/method_balance_of.go @@ -21,22 +21,20 @@ func (c *Contract) balanceOf( } // function balanceOf(address zrc20, address user) external view returns (uint256 balance); - tokenAddr, addr := args[0].(common.Address), args[1].(common.Address) - - // common.Address has to be converted to AccAddress. - accAddr := sdk.AccAddress(addr.Bytes()) - if accAddr.Empty() { - return nil, &ptypes.ErrInvalidAddr{ - Got: accAddr.String(), - } + zrc20Addr, addr, err := unpackBalanceOfArgs(args) + if err != nil { + return nil, err } - // Convert ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". - tokenDenom := ZRC20ToCosmosDenom(tokenAddr) + // Get the counterpart cosmos address. + toAddr, err := getCosmosAddress(c.bankKeeper, addr) + if err != nil { + return nil, err + } // Bank Keeper GetBalance returns the specified Cosmos coin balance for a given address. // Check explicitly the balance is a non-negative non-nil value. - coin := c.bankKeeper.GetBalance(ctx, accAddr, tokenDenom) + coin := c.bankKeeper.GetBalance(ctx, toAddr, ZRC20ToCosmosDenom(zrc20Addr)) if !coin.IsValid() { return nil, &ptypes.ErrInvalidCoin{ Got: coin.GetDenom(), @@ -47,3 +45,21 @@ func (c *Contract) balanceOf( return method.Outputs.Pack(coin.Amount.BigInt()) } + +func unpackBalanceOfArgs(args []interface{}) (zrc20Addr common.Address, addr common.Address, err error) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return common.Address{}, common.Address{}, &ptypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + addr, ok = args[1].(common.Address) + if !ok { + return common.Address{}, common.Address{}, &ptypes.ErrInvalidAddr{ + Got: addr.String(), + } + } + + return zrc20Addr, addr, nil +} diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index aca1868d1b..b33519ab99 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -1,20 +1,18 @@ package bank import ( - "fmt" "math/big" - "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" ptypes "github.com/zeta-chain/node/precompiles/types" "github.com/zeta-chain/node/x/fungible/types" ) +// From IBank.sol: function deposit(address zrc20, uint256 amount) external returns (bool success); func (c *Contract) deposit( ctx sdk.Context, evm *vm.EVM, @@ -22,7 +20,6 @@ func (c *Contract) deposit( method *abi.Method, args []interface{}, ) (result []byte, err error) { - fmt.Printf("DEBUG: deposit()\n") // This function is developed using the Check - Effects - Interactions pattern: // 1. Check everything is correct. if len(args) != 2 { @@ -32,61 +29,41 @@ func (c *Contract) deposit( }) } - // caller is usually the CallerAddress, except when the call was made through a contract. - // For those cases set the caller to the evm.Origin. - caller := contract.CallerAddress - if contract.CallerAddress != evm.Origin { - caller = evm.Origin - } - // Unpack parameters for function deposit. // function deposit(address zrc20, uint256 amount) external returns (bool success); - zrc20Addr, ok := args[0].(common.Address) - if !ok { - return nil, &ptypes.ErrInvalidAddr{ - Got: zrc20Addr.String(), - } + zrc20Addr, amount, err := unpackDepositArgs(args) + if err != nil { + return nil, err } - amount, ok := args[1].(*big.Int) - if !ok || amount.Sign() < 0 || amount == nil || amount == new(big.Int) { - return nil, &ptypes.ErrInvalidAmount{ - Got: amount.String(), - } + // Get the correct caller address. + caller, err := getEVMCallerAddress(evm, contract) + if err != nil { + return nil, err } - fmt.Printf("DEBUG: deposit(): zrc20Addr (ERC20ZRC20) %s\n", zrc20Addr.String()) - fmt.Printf("DEBUG: deposit(): caller %s\n", caller.String()) - // Initialize the ZRC20 ABI, as we need to call the balanceOf and allowance methods. - zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + // Get the cosmos address of the caller. + toAddr, err := getCosmosAddress(c.bankKeeper, caller) if err != nil { - fmt.Printf("DEBUG: deposit(): zrc20.ZRC20MetaData.GetAbi() error %s\n", err.Error()) - return nil, &ptypes.ErrUnexpected{ - When: "ZRC20MetaData.GetAbi()", - Got: err.Error(), - } + return nil, err } - fmt.Printf("DEBUG: deposit(): zrc20ABI %v\n", zrc20ABI) // Check for enough balance. // function balanceOf(address account) public view virtual override returns (uint256) resBalanceOf, err := c.CallContract( ctx, &c.fungibleKeeper, - zrc20ABI, + c.zrc20ABI, zrc20Addr, "balanceOf", - true, []interface{}{caller}, ) if err != nil { - fmt.Printf("DEBUG: deposit(): balanceOf c.CallContract error %s\n", err.Error()) return nil, &ptypes.ErrUnexpected{ When: "balanceOf", Got: err.Error(), } } - fmt.Printf("DEBUG: deposit(): resBalanceOf %v\n", resBalanceOf) balance, ok := resBalanceOf[0].(*big.Int) if !ok || balance.Cmp(amount) < 0 { @@ -94,17 +71,15 @@ func (c *Contract) deposit( Got: "not enough balance", } } - fmt.Printf("DEBUG: deposit(): balanceOf caller %v\n", balance.Uint64()) - // Check for enough allowance. + // Check for enough bank's allowance. // function allowance(address owner, address spender) public view virtual override returns (uint256) resAllowance, err := c.CallContract( ctx, &c.fungibleKeeper, - zrc20ABI, + c.zrc20ABI, zrc20Addr, "allowance", - true, []interface{}{caller, ContractAddress}, ) if err != nil { @@ -120,52 +95,15 @@ func (c *Contract) deposit( Got: "not enough allowance", } } - fmt.Printf("DEBUG: deposit(): allowance caller %v\n", allowance.Uint64()) - - // Handle the toAddr: - // check it's valid and not blocked. - toAddr := sdk.AccAddress(caller.Bytes()) - if toAddr.Empty() { - return nil, &ptypes.ErrInvalidAddr{ - Got: toAddr.String(), - Reason: "empty address", - } - } - fmt.Printf("DEBUG: deposit(): caller toAddr %s\n", toAddr.String()) - - if c.bankKeeper.BlockedAddr(toAddr) { - return nil, &ptypes.ErrInvalidAddr{ - Got: toAddr.String(), - Reason: "destination address blocked by bank keeper", - } - } // The process of creating a new cosmos coin is: // - Generate the new coin denom using ZRC20 address, // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". // - Mint coins. // - Send coins to the caller. - tokenDenom := ZRC20ToCosmosDenom(zrc20Addr) - - coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) - if !coin.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ - Got: coin.GetDenom(), - Negative: coin.IsNegative(), - Nil: coin.IsNil(), - } - } - - // A sdk.Coins (type []sdk.Coin) has to be created because it's the type expected by MintCoins - // and SendCoinsFromModuleToAccount. - // But sdk.Coins will only contain one coin, always. - coinSet := sdk.NewCoins(coin) - if !coinSet.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ - Got: coinSet.Sort().GetDenomByIndex(0), - Negative: coinSet.IsAnyNegative(), - Nil: coinSet.IsAnyNil(), - } + coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) + if err != nil { + return nil, err } // 2. Effect: subtract balance. @@ -173,10 +111,9 @@ func (c *Contract) deposit( resTransferFrom, err := c.CallContract( ctx, &c.fungibleKeeper, - zrc20ABI, + c.zrc20ABI, zrc20Addr, "transferFrom", - true, []interface{}{caller, ContractAddress, amount}, ) if err != nil { @@ -193,30 +130,25 @@ func (c *Contract) deposit( Got: "transaction not successful", } } - fmt.Printf("DEBUG: deposit(): transferred %v\n", transferred) // 3. Interactions: create cosmos coin and send. err = c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet) if err != nil { - fmt.Printf("DEBUG: deposit(): MintCoins error %s\n", err.Error()) return nil, &ptypes.ErrUnexpected{ When: "MintCoins", Got: err.Error(), } } - fmt.Printf("DEBUG: deposit(): MintCoins finished\n") err = c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet) if err != nil { - fmt.Printf("DEBUG: deposit(): SendCoinsFromModuleToAccount error %s\n", err.Error()) return nil, &ptypes.ErrUnexpected{ When: "SendCoinsFromModuleToAccount", Got: err.Error(), } } - fmt.Printf("DEBUG: deposit(): SendCoinsFromModuleToAccount finished\n") - if err := c.AddDepositLog(ctx, evm.StateDB, caller, zrc20Addr, amount); err != nil { + if err := c.AddDepositLog(ctx, evm.StateDB, caller, zrc20Addr, ZRC20ToCosmosDenom(zrc20Addr), amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "AddDepositLog", Got: err.Error(), @@ -225,3 +157,21 @@ func (c *Contract) deposit( return method.Outputs.Pack(true) } + +func unpackDepositArgs(args []interface{}) (zrc20Addr common.Address, amount *big.Int, err error) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return common.Address{}, nil, &ptypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + amount, ok = args[1].(*big.Int) + if !ok || amount.Sign() < 0 || amount == nil || amount == new(big.Int) { + return common.Address{}, nil, &ptypes.ErrInvalidAmount{ + Got: amount.String(), + } + } + + return zrc20Addr, amount, nil +} diff --git a/precompiles/types/errors.go b/precompiles/types/errors.go index 73b84bd2ee..9a4b172dd4 100644 --- a/precompiles/types/errors.go +++ b/precompiles/types/errors.go @@ -57,6 +57,15 @@ func (e ErrInvalidAmount) Error() string { return fmt.Sprintf("invalid token amount: %s", e.Got) } +type ErrInsufficientBalance struct { + Requested string + Got string +} + +func (e ErrInsufficientBalance) Error() string { + return fmt.Sprintf("insufficient balance: requested %s, current %s", e.Requested, e.Got) +} + /* Method related errors */ diff --git a/precompiles/types/types.go b/precompiles/types/types.go index 70415e675f..dad01f348f 100644 --- a/precompiles/types/types.go +++ b/precompiles/types/types.go @@ -39,7 +39,6 @@ type ContractCaller interface { abi *abi.ABI, dst common.Address, method string, - noEthereumTxEvent bool, args []interface{}) ([]interface{}, error) } @@ -67,26 +66,30 @@ func BytesToBigInt(data []byte) *big.Int { return big.NewInt(0).SetBytes(data[:]) } +// CallContract calls a contract method on behalf of a precompiled contract. +// - noEtherumTxEvent is set to true because we don't want to emit EthereumTxEvent, +// as any MsgEthereumTx with more than one ethereum_tx will fail and the receipt +// won't be able to be retrieved. +// - from is set always to the precompiled contract address. func (c *baseContract) CallContract( ctx sdk.Context, fungibleKeeper *fungiblekeeper.Keeper, abi *abi.ABI, dst common.Address, method string, - noEthereumTxEvent bool, args []interface{}, ) ([]interface{}, error) { res, err := fungibleKeeper.CallEVM( - ctx, // ctx - *abi, // abi - c.RegistryKey(), // from - dst, // to - big.NewInt(0), // value - nil, // gasLimit - true, // commit - noEthereumTxEvent, // noEthereumTxEvent - method, // method - args..., // args + ctx, // ctx + *abi, // abi + c.RegistryKey(), // from + dst, // to + big.NewInt(0), // value + nil, // gasLimit + true, // commit + true, // noEthereumTxEvent + method, // method + args..., // args ) if err != nil { return nil, &ErrUnexpected{ diff --git a/x/fungible/keeper/evm.go b/x/fungible/keeper/evm.go index 0ea4d4df87..ab0332dfe3 100644 --- a/x/fungible/keeper/evm.go +++ b/x/fungible/keeper/evm.go @@ -668,7 +668,6 @@ func (k Keeper) CallEVM( } k.Logger(ctx).Debug("calling EVM", "from", from, "contract", contract, "value", value, "method", method) - fmt.Printf("DEBUG: CallEVM: calling EVM from %s contract %s value %s method %s\n", from, contract, value, method) resp, err := k.CallEVMWithData(ctx, from, &contract, data, commit, noEthereumTxEvent, value, gasLimit) if err != nil { errMes := fmt.Sprintf( @@ -708,30 +707,18 @@ func (k Keeper) CallEVMWithData( value *big.Int, gasLimit *big.Int, ) (*evmtypes.MsgEthereumTxResponse, error) { - fmt.Printf( - "DEBUG: CallEVMWithData with from %s, contract %s, commit %v, gasLimit %v, noEthereumTxEvent %v, value %v\n", - from, - contract, - commit, - gasLimit, - noEthereumTxEvent, - value, - ) nonce, err := k.authKeeper.GetSequence(ctx, from.Bytes()) if err != nil { - fmt.Printf("DEBUG: CallEVMWithData error GetSequence: %s\n", err.Error()) return nil, err } gasCap := config.DefaultGasCap if commit && gasLimit == nil { - fmt.Printf("DEBUG: CallEVMWithData entered if commit and gasLimit == nil\n") args, err := json.Marshal(evmtypes.TransactionArgs{ From: &from, To: contract, Data: (*hexutil.Bytes)(&data), }) if err != nil { - fmt.Printf("DEBUG: CallEVMWithData entered if commit and gasLimit == nil, ERROR: %s\n", err.Error()) return nil, cosmoserrors.Wrapf(sdkerrors.ErrJSONMarshal, "failed to marshal tx args: %s", err.Error()) } @@ -740,7 +727,6 @@ func (k Keeper) CallEVMWithData( GasCap: config.DefaultGasCap, }) if err != nil { - fmt.Printf("DEBUG: CallEVMWithData error EstimateGas: %s\n", err.Error()) return nil, err } gasCap = gasRes.Gas @@ -764,21 +750,13 @@ func (k Keeper) CallEVMWithData( ) k.evmKeeper.WithChainID(ctx) //FIXME: set chainID for signer; should not need to do this; but seems necessary. Why? k.Logger(ctx).Debug("call evm", "gasCap", gasCap, "chainid", k.evmKeeper.ChainID(), "ctx.chainid", ctx.ChainID()) - fmt.Printf( - "DEBUG: CallEVMWithData: call evm gasCap %d chainid %d ctx.chainid %d\n", - gasCap, - k.evmKeeper.ChainID(), - ctx.ChainID(), - ) res, err := k.evmKeeper.ApplyMessage(ctx, msg, evmtypes.NewNoOpTracer(), commit) if err != nil { - fmt.Printf("DEBUG: ApplyMessage error: %s\n", err.Error()) return nil, err } // Emit events and log for the transaction if it is committed if commit { - fmt.Printf("DEBUG: Enter commit\n") msgBytes, err := json.Marshal(msg) if err != nil { return nil, cosmoserrors.Wrap(err, "failed to encode msg") @@ -852,10 +830,8 @@ func (k Keeper) CallEVMWithData( k.evmKeeper.SetLogSizeTransient(ctx, (k.evmKeeper.GetLogSizeTransient(ctx))+uint64(len(logs))) } } - fmt.Printf("DEBUG: Finish commit\n") if res.Failed() { - fmt.Printf("DEBUG: Enter res.Failed()\n") return res, cosmoserrors.Wrapf(evmtypes.ErrVMExecution, "%s: ret 0x%x", res.VmError, res.Ret) } From e7ddcb0797f254c463158d0c04598778520dc5d0 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 17 Sep 2024 12:51:59 +0200 Subject: [PATCH 20/33] finalize withdraw --- e2e/e2etests/test_precompiles_bank.go | 64 ++++++++--- precompiles/bank/IBank.abi | 12 ++ precompiles/bank/IBank.go | 28 ++--- precompiles/bank/IBank.json | 12 ++ precompiles/bank/IBank.sol | 4 + precompiles/bank/logs.go | 49 +++++++- precompiles/bank/method_deposit.go | 2 +- precompiles/bank/method_withdraw.go | 156 ++++++++++++++++++++++++++ 8 files changed, 291 insertions(+), 36 deletions(-) create mode 100644 precompiles/bank/method_withdraw.go diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 2bcd780c5f..d078397849 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -1,7 +1,6 @@ package e2etests import ( - "fmt" "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -25,24 +24,21 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { spender, bankAddr := r.EVMAddress(), bank.ContractAddress + // Create a bank contract caller. + bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create bank contract caller") + // Deposit and approve 50 WZETA for the test. approveAmount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(50)) r.DepositAndApproveWZeta(approveAmount) - fmt.Printf("DEBUG: approveAmount %s\n", approveAmount.String()) + // Initial WZETA spender balance should be 50. initialBalance, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) - fmt.Printf("DEBUG: initialBalance %s\n", initialBalance.String()) require.NoError(r, err, "Error approving allowance for bank contract") require.EqualValues(r, approveAmount, initialBalance, "spender balance should be 50") - // Create a bank contract caller. - bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) - require.NoError(r, err, "Failed to create bank contract caller") - - // Get the balance of the spender in coins "zevm/WZetaAddr". This calls bank.balanceOf(). - // BalanceOf will convert the ZRC20 address to a Cosmos denom formatted as "zevm/0x12345". + // Initial cosmos coin spender balance should be 0. retBalanceOf, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) - fmt.Printf("DEBUG: initial bank.balanceOf() %s\n", retBalanceOf.String()) require.NoError(r, err, "Error calling bank.balanceOf()") require.EqualValues(r, uint64(0), retBalanceOf.Uint64(), "Initial cosmos coins balance has to be 0") @@ -54,13 +50,11 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { // Check the allowance of the bank in WZeta tokens. Should be 25. balance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, bankAddr) - fmt.Printf("DEBUG: bank allowance %s\n", balance.String()) require.NoError(r, err, "Error retrieving bank allowance") require.EqualValues(r, uint64(25), balance.Uint64(), "Error allowance for bank contract") - // Call deposit with 25 coins. + // Call Deposit with 25 coins. tx, err = bankContract.Deposit(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) - fmt.Printf("DEBUG: bank.deposit() tx hash %s\n", tx.Hash().String()) require.NoError(r, err, "Error calling bank.deposit()") receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) @@ -70,17 +64,14 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.Equal(r, big.NewInt(25).Uint64(), depositEvent.Amount.Uint64()) require.Equal(r, common.BytesToAddress(spender.Bytes()), depositEvent.Zrc20Depositor) require.Equal(r, r.WZetaAddr, depositEvent.Zrc20Token) - fmt.Println("Deposit event emitted ", depositEvent) - // Check the balance of the spender in coins "zevm/WZetaAddr". + // After deposit, cosmos coin spender balance should be 25. retBalanceOf, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) - fmt.Printf("DEBUG: final bank.balanceOf() tx %s\n", retBalanceOf.String()) require.NoError(r, err, "Error calling bank.balanceOf()") require.EqualValues(r, uint64(25), retBalanceOf.Uint64(), "balanceOf result has to be 25") - // Check the balance of the spender in r.WZeta, should be 100 less. + // After deposit, WZeta spender balance should be 25 less than initial. finalBalance, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) - fmt.Printf("DEBUG: final WZeta balance %s\n", finalBalance.String()) require.NoError(r, err, "Error retrieving final owner balance") require.EqualValues( r, @@ -88,4 +79,41 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { finalBalance.Uint64(), // actual "Final balance should be initial - 25", ) + + // After deposit, WZeta bank balance should be 25. + balance, err = r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddr) + require.NoError(r, err, "Error retrieving bank's balance") + require.EqualValues(r, uint64(25), balance.Uint64(), "Wrong locked WZeta amount in bank contract") + + // Withdraw 15 coins to spender. + tx, err = bankContract.Withdraw(r.ZEVMAuth, r.WZetaAddr, big.NewInt(15)) + require.NoError(r, err, "Error calling bank.withdraw()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + + // Withdraw event should be emitted. + withdrawEvent, err := bankContract.ParseWithdraw(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, big.NewInt(15).Uint64(), withdrawEvent.Amount.Uint64()) + require.Equal(r, common.BytesToAddress(spender.Bytes()), withdrawEvent.Zrc20Withdrawer) + require.Equal(r, r.WZetaAddr, withdrawEvent.Zrc20Token) + + // After withdraw, WZeta spender balance should be only 10 less than initial. (25 - 15 = 10e2e/e2etests/test_precompiles_bank.go ) + afterWithdraw, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) + require.NoError(r, err, "Error retrieving final owner balance") + require.EqualValues( + r, + initialBalance.Uint64()-10, // expected + afterWithdraw.Uint64(), // actual + "Balance after withdraw should be initial - 10", + ) + + // After withdraw, cosmos coin spender balance should be 10. + retBalanceOf, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) + require.NoError(r, err, "Error calling bank.balanceOf()") + require.EqualValues(r, uint64(10), retBalanceOf.Uint64(), "balanceOf result has to be 10") + + // Final WZETA bank balance should be 10. + balance, err = r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddr) + require.NoError(r, err, "Error retrieving bank's allowance") + require.EqualValues(r, uint64(10), balance.Uint64(), "Wrong locked WZeta amount in bank contract") } diff --git a/precompiles/bank/IBank.abi b/precompiles/bank/IBank.abi index 3d8a84508d..9806953a39 100644 --- a/precompiles/bank/IBank.abi +++ b/precompiles/bank/IBank.abi @@ -20,6 +20,12 @@ "name": "cosmos_token", "type": "string" }, + { + "indexed": false, + "internalType": "string", + "name": "cosmos_address", + "type": "string" + }, { "indexed": false, "internalType": "uint256", @@ -51,6 +57,12 @@ "name": "cosmos_token", "type": "string" }, + { + "indexed": false, + "internalType": "string", + "name": "cosmos_address", + "type": "string" + }, { "indexed": false, "internalType": "uint256", diff --git a/precompiles/bank/IBank.go b/precompiles/bank/IBank.go index a2c55cf145..5064cc8661 100644 --- a/precompiles/bank/IBank.go +++ b/precompiles/bank/IBank.go @@ -31,7 +31,7 @@ var ( // IBankMetaData contains all meta data concerning the IBank contract. var IBankMetaData = &bind.MetaData{ - ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_depositor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"string\",\"name\":\"cosmos_token\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_withdrawer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"string\",\"name\":\"cosmos_token\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdraw\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_depositor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"string\",\"name\":\"cosmos_token\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"cosmos_address\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_withdrawer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"string\",\"name\":\"cosmos_token\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"cosmos_address\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdraw\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", } // IBankABI is the input ABI used to generate the binding from. @@ -325,13 +325,14 @@ type IBankDeposit struct { Zrc20Depositor common.Address Zrc20Token common.Address CosmosToken common.Hash + CosmosAddress string Amount *big.Int Raw types.Log // Blockchain specific contextual infos } -// FilterDeposit is a free log retrieval operation binding the contract event 0x2dc24880b34b2026fb1cd0231e65ef42ca1c78e4efdd9711b7629740206bfa46. +// FilterDeposit is a free log retrieval operation binding the contract event 0xbd7d4de0b30a306221956a420cad57737ae9c1ee63072c96a4f1ab81e6eea264. // -// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) func (_IBank *IBankFilterer) FilterDeposit(opts *bind.FilterOpts, zrc20_depositor []common.Address, zrc20_token []common.Address, cosmos_token []string) (*IBankDepositIterator, error) { var zrc20_depositorRule []interface{} @@ -354,9 +355,9 @@ func (_IBank *IBankFilterer) FilterDeposit(opts *bind.FilterOpts, zrc20_deposito return &IBankDepositIterator{contract: _IBank.contract, event: "Deposit", logs: logs, sub: sub}, nil } -// WatchDeposit is a free log subscription operation binding the contract event 0x2dc24880b34b2026fb1cd0231e65ef42ca1c78e4efdd9711b7629740206bfa46. +// WatchDeposit is a free log subscription operation binding the contract event 0xbd7d4de0b30a306221956a420cad57737ae9c1ee63072c96a4f1ab81e6eea264. // -// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) func (_IBank *IBankFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *IBankDeposit, zrc20_depositor []common.Address, zrc20_token []common.Address, cosmos_token []string) (event.Subscription, error) { var zrc20_depositorRule []interface{} @@ -404,9 +405,9 @@ func (_IBank *IBankFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *IBa }), nil } -// ParseDeposit is a log parse operation binding the contract event 0x2dc24880b34b2026fb1cd0231e65ef42ca1c78e4efdd9711b7629740206bfa46. +// ParseDeposit is a log parse operation binding the contract event 0xbd7d4de0b30a306221956a420cad57737ae9c1ee63072c96a4f1ab81e6eea264. // -// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) func (_IBank *IBankFilterer) ParseDeposit(log types.Log) (*IBankDeposit, error) { event := new(IBankDeposit) if err := _IBank.contract.UnpackLog(event, "Deposit", log); err != nil { @@ -488,13 +489,14 @@ type IBankWithdraw struct { Zrc20Withdrawer common.Address Zrc20Token common.Address CosmosToken common.Hash + CosmosAddress string Amount *big.Int Raw types.Log // Blockchain specific contextual infos } -// FilterWithdraw is a free log retrieval operation binding the contract event 0x4b230e98ab931f2b86715011c55a66078553f7ae5933f387d1e23954b2b9d19a. +// FilterWithdraw is a free log retrieval operation binding the contract event 0x1ad70707c91d850319aeab00514a0166569359f0b8dc5285bdd6e6b9c464b18e. // -// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) func (_IBank *IBankFilterer) FilterWithdraw(opts *bind.FilterOpts, zrc20_withdrawer []common.Address, zrc20_token []common.Address, cosmos_token []string) (*IBankWithdrawIterator, error) { var zrc20_withdrawerRule []interface{} @@ -517,9 +519,9 @@ func (_IBank *IBankFilterer) FilterWithdraw(opts *bind.FilterOpts, zrc20_withdra return &IBankWithdrawIterator{contract: _IBank.contract, event: "Withdraw", logs: logs, sub: sub}, nil } -// WatchWithdraw is a free log subscription operation binding the contract event 0x4b230e98ab931f2b86715011c55a66078553f7ae5933f387d1e23954b2b9d19a. +// WatchWithdraw is a free log subscription operation binding the contract event 0x1ad70707c91d850319aeab00514a0166569359f0b8dc5285bdd6e6b9c464b18e. // -// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) func (_IBank *IBankFilterer) WatchWithdraw(opts *bind.WatchOpts, sink chan<- *IBankWithdraw, zrc20_withdrawer []common.Address, zrc20_token []common.Address, cosmos_token []string) (event.Subscription, error) { var zrc20_withdrawerRule []interface{} @@ -567,9 +569,9 @@ func (_IBank *IBankFilterer) WatchWithdraw(opts *bind.WatchOpts, sink chan<- *IB }), nil } -// ParseWithdraw is a log parse operation binding the contract event 0x4b230e98ab931f2b86715011c55a66078553f7ae5933f387d1e23954b2b9d19a. +// ParseWithdraw is a log parse operation binding the contract event 0x1ad70707c91d850319aeab00514a0166569359f0b8dc5285bdd6e6b9c464b18e. // -// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, uint256 amount) +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) func (_IBank *IBankFilterer) ParseWithdraw(log types.Log) (*IBankWithdraw, error) { event := new(IBankWithdraw) if err := _IBank.contract.UnpackLog(event, "Withdraw", log); err != nil { diff --git a/precompiles/bank/IBank.json b/precompiles/bank/IBank.json index 6193826a71..1ef1654602 100644 --- a/precompiles/bank/IBank.json +++ b/precompiles/bank/IBank.json @@ -21,6 +21,12 @@ "name": "cosmos_token", "type": "string" }, + { + "indexed": false, + "internalType": "string", + "name": "cosmos_address", + "type": "string" + }, { "indexed": false, "internalType": "uint256", @@ -52,6 +58,12 @@ "name": "cosmos_token", "type": "string" }, + { + "indexed": false, + "internalType": "string", + "name": "cosmos_address", + "type": "string" + }, { "indexed": false, "internalType": "uint256", diff --git a/precompiles/bank/IBank.sol b/precompiles/bank/IBank.sol index 5d4a25c271..4b5d7f2eba 100644 --- a/precompiles/bank/IBank.sol +++ b/precompiles/bank/IBank.sol @@ -18,11 +18,13 @@ interface IBank { /// @param zrc20_depositor Depositor EVM address. /// @param zrc20_token ZRC20 address deposited. /// @param cosmos_token Cosmos token denomination the tokens were converted into. + /// @param cosmos_address Cosmos address the tokens were deposited to. /// @param amount Amount deposited. event Deposit( address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, + string cosmos_address, uint256 amount ); @@ -30,11 +32,13 @@ interface IBank { /// @param zrc20_withdrawer Withdrawer EVM address. /// @param zrc20_token ZRC20 address withdrawn. /// @param cosmos_token Cosmos token denomination the tokens were converted from. + /// @param cosmos_address Cosmos address the tokens were withdrawn from. /// @param amount Amount withdrawn. event Withdraw( address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, + string cosmos_address, uint256 amount ); diff --git a/precompiles/bank/logs.go b/precompiles/bank/logs.go index 6e7d8398cf..9e1e22473c 100644 --- a/precompiles/bank/logs.go +++ b/precompiles/bank/logs.go @@ -4,6 +4,7 @@ import ( "math/big" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" @@ -20,6 +21,7 @@ func (c *Contract) AddDepositLog( stateDB vm.StateDB, zrc20Depositor common.Address, zrc20Token common.Address, + cosmosAddr string, cosmosCoin string, amount *big.Int, ) error { @@ -36,8 +38,27 @@ func (c *Contract) AddDepositLog( return err } - // amount is part of event data. - data, err := logs.PackBigInt(amount) + // Amount and cosmos address are part of event data. + uint256Type, err := abi.NewType("uint256", "", nil) + if err != nil { + return err + } + + stringType, err := abi.NewType("string", "", nil) + if err != nil { + return err + } + + arguments := abi.Arguments{ + { + Type: stringType, + }, + { + Type: uint256Type, + }, + } + + data, err := arguments.Pack(cosmosAddr, amount) if err != nil { return err } @@ -52,6 +73,7 @@ func (c *Contract) AddWithdrawLog( stateDB vm.StateDB, zrc20Withdrawer common.Address, zrc20Token common.Address, + cosmosAddr string, cosmosCoin string, amount *big.Int, ) error { @@ -68,8 +90,27 @@ func (c *Contract) AddWithdrawLog( return err } - // amount is part of event data. - data, err := logs.PackBigInt(amount) + // Amount and cosmos address are part of event data. + uint256Type, err := abi.NewType("uint256", "", nil) + if err != nil { + return err + } + + stringType, err := abi.NewType("string", "", nil) + if err != nil { + return err + } + + arguments := abi.Arguments{ + { + Type: stringType, + }, + { + Type: uint256Type, + }, + } + + data, err := arguments.Pack(cosmosAddr, amount) if err != nil { return err } diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index b33519ab99..d6e5d118ba 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -148,7 +148,7 @@ func (c *Contract) deposit( } } - if err := c.AddDepositLog(ctx, evm.StateDB, caller, zrc20Addr, ZRC20ToCosmosDenom(zrc20Addr), amount); err != nil { + if err := c.AddDepositLog(ctx, evm.StateDB, caller, zrc20Addr, toAddr.String(), coinSet.Denoms()[0], amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "AddDepositLog", Got: err.Error(), diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go new file mode 100644 index 0000000000..daf19fc1f0 --- /dev/null +++ b/precompiles/bank/method_withdraw.go @@ -0,0 +1,156 @@ +package bank + +import ( + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/x/fungible/types" +) + +// From IBank.sol: function withdraw(address zrc20, uint256 amount) external returns (bool success); +func (c *Contract) withdraw( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) (result []byte, err error) { + // 1. Check everything is correct. + if len(args) != 2 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + + // Unpack parameters for function withdraw. + // function withdraw(address zrc20, uint256 amount) external returns (bool success); + zrc20Addr, amount, err := unpackWithdrawArgs(args) + if err != nil { + return nil, err + } + + // Get the correct caller address. + caller, err := getEVMCallerAddress(evm, contract) + if err != nil { + return nil, err + } + + // Get the cosmos address of the caller. + // This address should have enough cosmos coin balance as the requested amount. + fromAddr, err := getCosmosAddress(c.bankKeeper, caller) + if err != nil { + return nil, err + } + + // Caller has to have enough cosmos coin balance to withdraw the requested amount. + coin := c.bankKeeper.GetBalance(ctx, fromAddr, ZRC20ToCosmosDenom(zrc20Addr)) + if coin.Amount.LT(math.NewIntFromBigInt(amount)) { + return nil, &ptypes.ErrInsufficientBalance{ + Requested: amount.String(), + Got: coin.Amount.String(), + } + } + + coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) + if err != nil { + return nil, err + } + + // Check for bank's ZRC20 balance. + // function balanceOf(address account) public view virtual override returns (uint256) + resBalanceOf, err := c.CallContract( + ctx, + &c.fungibleKeeper, + c.zrc20ABI, + zrc20Addr, + "balanceOf", + []interface{}{caller}, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "balanceOf", + Got: err.Error(), + } + } + + balance, ok := resBalanceOf[0].(*big.Int) + if !ok || balance.Cmp(amount) < 0 { + return nil, &ptypes.ErrInvalidAmount{ + Got: "not enough bank balance", + } + } + + // 2. Effect: transfer balance. + // function transferFrom(address sender, address recipient, uint256 amount) + resTransferFrom, err := c.CallContract( + ctx, + &c.fungibleKeeper, + c.zrc20ABI, + zrc20Addr, + "transferFrom", + []interface{}{ContractAddress /* sender */, caller /* receiver */, amount}, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "transferFrom", + Got: err.Error(), + } + } + + transferred, ok := resTransferFrom[0].(bool) + if !ok || !transferred { + return nil, &ptypes.ErrUnexpected{ + When: "transferFrom", + Got: "transaction not successful", + } + } + + // 3. Interactions: send to module and burn. + if err := c.bankKeeper.SendCoinsFromAccountToModule(ctx, fromAddr, types.ModuleName, coinSet); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "SendCoinsFromAccountToModule", + Got: err.Error(), + } + } + + if err := c.bankKeeper.BurnCoins(ctx, types.ModuleName, coinSet); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "BurnCoins", + Got: err.Error(), + } + } + + if err := c.AddWithdrawLog(ctx, evm.StateDB, caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "AddWithdrawLog", + Got: err.Error(), + } + } + + return method.Outputs.Pack(true) +} + +func unpackWithdrawArgs(args []interface{}) (zrc20Addr common.Address, amount *big.Int, err error) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return common.Address{}, nil, &ptypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + amount, ok = args[1].(*big.Int) + if !ok || amount.Sign() < 0 || amount == nil || amount == new(big.Int) { + return common.Address{}, nil, &ptypes.ErrInvalidAmount{ + Got: amount.String(), + } + } + + return zrc20Addr, amount, nil +} From f1e5b3aa06f1bd74dbb28149ab1cdcd935ff36fb Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 17 Sep 2024 13:20:59 +0200 Subject: [PATCH 21/33] pack event arguments generically --- changelog.md | 1 + precompiles/bank/logs.go | 53 ++++++-------------------------- precompiles/logs/logs.go | 38 +++++++++++++++-------- precompiles/staking/logs.go | 12 ++++++-- precompiles/types/errors_test.go | 14 ++++++++- 5 files changed, 59 insertions(+), 59 deletions(-) diff --git a/changelog.md b/changelog.md index accd802d9f..b58231fb03 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ * [2784](https://github.com/zeta-chain/node/pull/2784) - staking precompiled contract * [2795](https://github.com/zeta-chain/node/pull/2795) - support restricted address in Solana * [2861](https://github.com/zeta-chain/node/pull/2861) - emit events from staking precompile +* [2860](https://github.com/zeta-chain/node/pull/2860) - bank precompiled contract ### Refactor diff --git a/precompiles/bank/logs.go b/precompiles/bank/logs.go index 9e1e22473c..f2118ae07f 100644 --- a/precompiles/bank/logs.go +++ b/precompiles/bank/logs.go @@ -4,7 +4,6 @@ import ( "math/big" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" @@ -38,27 +37,11 @@ func (c *Contract) AddDepositLog( return err } - // Amount and cosmos address are part of event data. - uint256Type, err := abi.NewType("uint256", "", nil) - if err != nil { - return err - } - - stringType, err := abi.NewType("string", "", nil) - if err != nil { - return err - } - - arguments := abi.Arguments{ - { - Type: stringType, - }, - { - Type: uint256Type, - }, - } - - data, err := arguments.Pack(cosmosAddr, amount) + // Pack cosmos address and amount as data. + data, err := logs.PackArguments([]logs.Argument{ + {Type: "string", Value: cosmosAddr}, + {Type: "uint256", Value: amount}, + }) if err != nil { return err } @@ -90,27 +73,11 @@ func (c *Contract) AddWithdrawLog( return err } - // Amount and cosmos address are part of event data. - uint256Type, err := abi.NewType("uint256", "", nil) - if err != nil { - return err - } - - stringType, err := abi.NewType("string", "", nil) - if err != nil { - return err - } - - arguments := abi.Arguments{ - { - Type: stringType, - }, - { - Type: uint256Type, - }, - } - - data, err := arguments.Pack(cosmosAddr, amount) + // Pack cosmos address and amount as data. + data, err := logs.PackArguments([]logs.Argument{ + {Type: "string", Value: cosmosAddr}, + {Type: "uint256", Value: amount}, + }) if err != nil { return err } diff --git a/precompiles/logs/logs.go b/precompiles/logs/logs.go index f168e33da4..07960a442e 100644 --- a/precompiles/logs/logs.go +++ b/precompiles/logs/logs.go @@ -1,8 +1,6 @@ package logs import ( - "math/big" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -10,6 +8,11 @@ import ( "github.com/ethereum/go-ethereum/core/vm" ) +type Argument struct { + Type string + Value interface{} +} + // AddLog adds log to stateDB func AddLog(ctx sdk.Context, precompileAddr common.Address, stateDB vm.StateDB, topics []common.Hash, data []byte) { stateDB.AddLog(&types.Log{ @@ -38,18 +41,29 @@ func MakeTopics(event abi.Event, query ...[]interface{}) ([]common.Hash, error) return topics, nil } -// PackBigInt is a helper function to pack a uint256 amount -func PackBigInt(amount *big.Int) ([]byte, error) { - uint256Type, err := abi.NewType("uint256", "", nil) - if err != nil { - return nil, err +// PackArguments packs an arbitrary number of logs.Arguments as non-indexed data. +// When packing data, make sure the Argument are passed in the same order as the event definition. +func PackArguments(args []Argument) ([]byte, error) { + types := abi.Arguments{} + toPack := []interface{}{} + + for _, arg := range args { + abiType, err := abi.NewType(arg.Type, "", nil) + if err != nil { + return nil, err + } + + types = append(types, abi.Argument{ + Type: abiType, + }) + + toPack = append(toPack, arg.Value) } - arguments := abi.Arguments{ - { - Type: uint256Type, - }, + data, err := types.Pack(toPack...) + if err != nil { + return nil, err } - return arguments.Pack(amount) + return data, nil } diff --git a/precompiles/staking/logs.go b/precompiles/staking/logs.go index 165dd91aa3..ea8e51274c 100644 --- a/precompiles/staking/logs.go +++ b/precompiles/staking/logs.go @@ -37,7 +37,9 @@ func (c *Contract) AddStakeLog( } // amount is part of event data - data, err := logs.PackBigInt(amount) + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) if err != nil { return err } @@ -67,7 +69,9 @@ func (c *Contract) AddUnstakeLog( } // amount is part of event data - data, err := logs.PackBigInt(amount) + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) if err != nil { return err } @@ -108,7 +112,9 @@ func (c *Contract) AddMoveStakeLog( } // amount is part of event data - data, err := logs.PackBigInt(amount) + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) if err != nil { return err } diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go index 4fa3bc72e2..288501a38d 100644 --- a/precompiles/types/errors_test.go +++ b/precompiles/types/errors_test.go @@ -78,8 +78,20 @@ func Test_ErrUnexpected(t *testing.T) { Got: "bar", } got := e.Error() - expect := "unexpected foo, got: bar" + expect := "unexpected error in foo: bar" if got != expect { t.Errorf("Expected %v, got %v", expect, got) } } + +func Test_ErrInsufficientBalance(t *testing.T) { + e := ErrInsufficientBalance{ + Requested: "foo", + Got: "bar", + } + got := e.Error() + expect := "insufficient balance: requested foo, current bar" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} \ No newline at end of file From 05d0bfbd204cffc3048e6a9e3a1a12c0fd3da2a5 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 17 Sep 2024 13:38:10 +0200 Subject: [PATCH 22/33] add high level event function --- precompiles/bank/const.go | 2 ++ precompiles/bank/logs.go | 52 +++-------------------------- precompiles/bank/method_deposit.go | 2 +- precompiles/bank/method_withdraw.go | 2 +- precompiles/types/errors_test.go | 2 +- 5 files changed, 10 insertions(+), 50 deletions(-) diff --git a/precompiles/bank/const.go b/precompiles/bank/const.go index e3c5084652..144c374aa4 100644 --- a/precompiles/bank/const.go +++ b/precompiles/bank/const.go @@ -7,9 +7,11 @@ const ( // Write methods. DepositMethodName = "deposit" DepositMethodGas = 200_000 + DepositEventName = "Deposit" WithdrawMethodName = "withdraw" WithdrawMethodGas = 200_000 + WithdrawEventName = "Withdraw" // Read methods. BalanceOfMethodName = "balanceOf" diff --git a/precompiles/bank/logs.go b/precompiles/bank/logs.go index f2118ae07f..d20dacd24f 100644 --- a/precompiles/bank/logs.go +++ b/precompiles/bank/logs.go @@ -10,62 +10,21 @@ import ( "github.com/zeta-chain/node/precompiles/logs" ) -const ( - DepositEventName = "Deposit" - WithdrawEventName = "Withdraw" -) - -func (c *Contract) AddDepositLog( - ctx sdk.Context, - stateDB vm.StateDB, - zrc20Depositor common.Address, - zrc20Token common.Address, - cosmosAddr string, - cosmosCoin string, - amount *big.Int, -) error { - event := c.Abi().Events[DepositEventName] - - // ZRC20, cosmos coin and depositor. - topics, err := logs.MakeTopics( - event, - []interface{}{common.BytesToAddress(zrc20Depositor.Bytes())}, - []interface{}{common.BytesToAddress(zrc20Token.Bytes())}, - []interface{}{cosmosCoin}, - ) - if err != nil { - return err - } - - // Pack cosmos address and amount as data. - data, err := logs.PackArguments([]logs.Argument{ - {Type: "string", Value: cosmosAddr}, - {Type: "uint256", Value: amount}, - }) - if err != nil { - return err - } - - logs.AddLog(ctx, c.Address(), stateDB, topics, data) - - return nil -} - -func (c *Contract) AddWithdrawLog( +func (c *Contract) addEventLog( ctx sdk.Context, stateDB vm.StateDB, - zrc20Withdrawer common.Address, + eventName string, + zrc20Addr common.Address, zrc20Token common.Address, cosmosAddr string, cosmosCoin string, amount *big.Int, ) error { - event := c.Abi().Events[WithdrawEventName] + event := c.Abi().Events[eventName] - // ZRC20, cosmos coin and witgdrawer are indexed. topics, err := logs.MakeTopics( event, - []interface{}{common.BytesToAddress(zrc20Withdrawer.Bytes())}, + []interface{}{common.BytesToAddress(zrc20Addr.Bytes())}, []interface{}{common.BytesToAddress(zrc20Token.Bytes())}, []interface{}{cosmosCoin}, ) @@ -73,7 +32,6 @@ func (c *Contract) AddWithdrawLog( return err } - // Pack cosmos address and amount as data. data, err := logs.PackArguments([]logs.Argument{ {Type: "string", Value: cosmosAddr}, {Type: "uint256", Value: amount}, diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index d6e5d118ba..88778efdd1 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -148,7 +148,7 @@ func (c *Contract) deposit( } } - if err := c.AddDepositLog(ctx, evm.StateDB, caller, zrc20Addr, toAddr.String(), coinSet.Denoms()[0], amount); err != nil { + if err := c.addEventLog(ctx, evm.StateDB, DepositEventName, caller, zrc20Addr, toAddr.String(), coinSet.Denoms()[0], amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "AddDepositLog", Got: err.Error(), diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index daf19fc1f0..3e3a79063f 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -127,7 +127,7 @@ func (c *Contract) withdraw( } } - if err := c.AddWithdrawLog(ctx, evm.StateDB, caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount); err != nil { + if err := c.addEventLog(ctx, evm.StateDB, WithdrawEventName, caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "AddWithdrawLog", Got: err.Error(), diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go index 288501a38d..daa7081f8f 100644 --- a/precompiles/types/errors_test.go +++ b/precompiles/types/errors_test.go @@ -94,4 +94,4 @@ func Test_ErrInsufficientBalance(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } -} \ No newline at end of file +} From 0fa8d5043d4d36006f8850a2a6409a19ac6341aa Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 17 Sep 2024 18:25:27 +0200 Subject: [PATCH 23/33] first round of review fixes --- app/app.go | 6 ------ contrib/localnet/scripts/start-zetacored.sh | 2 +- e2e/e2etests/test_precompiles_bank.go | 6 ++++-- precompiles/bank/coin.go | 2 -- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/app.go b/app/app.go index 4eeaa16fa7..cc271fc35f 100644 --- a/app/app.go +++ b/app/app.go @@ -93,7 +93,6 @@ import ( "github.com/zeta-chain/node/docs/openapi" zetamempool "github.com/zeta-chain/node/pkg/mempool" "github.com/zeta-chain/node/precompiles" - bankprecompile "github.com/zeta-chain/node/precompiles/bank" srvflags "github.com/zeta-chain/node/server/flags" authoritymodule "github.com/zeta-chain/node/x/authority" authoritykeeper "github.com/zeta-chain/node/x/authority/keeper" @@ -1066,11 +1065,6 @@ func (app *App) BlockedAddrs() map[string]bool { // Each enabled precompiled stateful contract should be added as a BlockedAddrs. // That way it's marked as non payable by the bank keeper. for addr, enabled := range precompiles.EnabledStatefulContracts { - // bank precompile has to be able to receive funds. - if addr == bankprecompile.ContractAddress { - continue - } - if enabled { blockList[addr.String()] = enabled } diff --git a/contrib/localnet/scripts/start-zetacored.sh b/contrib/localnet/scripts/start-zetacored.sh index fdad77e632..e3f4e707af 100755 --- a/contrib/localnet/scripts/start-zetacored.sh +++ b/contrib/localnet/scripts/start-zetacored.sh @@ -271,7 +271,7 @@ then zetacored add-genesis-account "$address" 100000000000000000000000000azeta # bank precompile user address=$(yq -r '.additional_accounts.user_bank_precompile.bech32_address' /root/config.yml) - zetacored add-genesis-account "$address" 100000000000000000000000000azeta + zetacored add-genesis-account "$address" 000000000000000000000000000azeta # 3. Copy the genesis.json to all the nodes .And use it to create a gentx for every node zetacored gentx operator 1000000000000000000000azeta --chain-id=$CHAINID --keyring-backend=$KEYRING --gas-prices 20000000000azeta diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index d078397849..881fdb903e 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -34,7 +34,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { // Initial WZETA spender balance should be 50. initialBalance, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) - require.NoError(r, err, "Error approving allowance for bank contract") + require.NoError(r, err, "Error getting initial balance") require.EqualValues(r, approveAmount, initialBalance, "spender balance should be 50") // Initial cosmos coin spender balance should be 0. @@ -57,6 +57,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { tx, err = bankContract.Deposit(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) require.NoError(r, err, "Error calling bank.deposit()") receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "withdraw tx failed") // Deposit event should be emitted. depositEvent, err := bankContract.ParseDeposit(*receipt.Logs[0]) @@ -89,6 +90,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { tx, err = bankContract.Withdraw(r.ZEVMAuth, r.WZetaAddr, big.NewInt(15)) require.NoError(r, err, "Error calling bank.withdraw()") receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "withdraw tx failed") // Withdraw event should be emitted. withdrawEvent, err := bankContract.ParseWithdraw(*receipt.Logs[0]) @@ -97,7 +99,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.Equal(r, common.BytesToAddress(spender.Bytes()), withdrawEvent.Zrc20Withdrawer) require.Equal(r, r.WZetaAddr, withdrawEvent.Zrc20Token) - // After withdraw, WZeta spender balance should be only 10 less than initial. (25 - 15 = 10e2e/e2etests/test_precompiles_bank.go ) + // After withdraw, WZeta spender balance should be only 10 less than initial. (25 - 15 = 10) afterWithdraw, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) require.NoError(r, err, "Error retrieving final owner balance") require.EqualValues( diff --git a/precompiles/bank/coin.go b/precompiles/bank/coin.go index eddf6d4a37..868562b1ad 100644 --- a/precompiles/bank/coin.go +++ b/precompiles/bank/coin.go @@ -16,8 +16,6 @@ func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { return ZEVMDenom + ZRC20Address.String() } -// createCoinSet creates a sdk.Coins from a tokenDenom and an amount. -// It's mostly a helper function to avoid code duplication. func createCoinSet(tokenDenom string, amount *big.Int) (sdk.Coins, error) { coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) if !coin.IsValid() { From b6b78a9a25ec008a6ee397a453e97e2de14c7777 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 18 Sep 2024 12:24:16 +0200 Subject: [PATCH 24/33] second round of reviews --- precompiles/bank/bank.go | 9 +-------- precompiles/bank/const.go | 2 +- precompiles/bank/method_withdraw.go | 7 +++++++ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index db4c5e4d64..9e9f116d5f 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -1,8 +1,6 @@ package bank import ( - "fmt" - "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -139,25 +137,20 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { if method.Name == DepositMethodName { - fmt.Println("DEBUG: bank.Run(): DepositMethodName: ExecuteNativeAction c.deposit()") res, err = c.deposit(ctx, evm, contract, method, args) - } else { - fmt.Println("DEBUG: bank.Run(): WithdrawMethodName: ExecuteNativeAction c.withdraw()") + } else if method.Name == WithdrawMethodName { res, err = c.withdraw(ctx, evm, contract, method, args) } return err }) if execErr != nil { - fmt.Printf("DEBUG: bank.Run(): execErr %s", execErr.Error()) return nil, err } return res, nil case BalanceOfMethodName: - fmt.Println("DEBUG: bank.Run(): BalanceOfMethodName") var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { - fmt.Println("DEBUG: bank.Run(): DepositMethodName: ExecuteNativeAction c.balanceOf()") res, err = c.balanceOf(ctx, method, args) return err }) diff --git a/precompiles/bank/const.go b/precompiles/bank/const.go index 144c374aa4..3c9755d2e3 100644 --- a/precompiles/bank/const.go +++ b/precompiles/bank/const.go @@ -2,7 +2,7 @@ package bank const ( // ZEVM cosmos coins prefix. - ZEVMDenom = "zevm/" + ZEVMDenom = "zrc20/" // Write methods. DepositMethodName = "deposit" diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index 3e3a79063f..2833ea1cdc 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -51,6 +51,13 @@ func (c *Contract) withdraw( // Caller has to have enough cosmos coin balance to withdraw the requested amount. coin := c.bankKeeper.GetBalance(ctx, fromAddr, ZRC20ToCosmosDenom(zrc20Addr)) + if coin.Amount.IsNil() { + return nil, &ptypes.ErrInsufficientBalance{ + Requested: amount.String(), + Got: "nil", + } + } + if coin.Amount.LT(math.NewIntFromBigInt(amount)) { return nil, &ptypes.ErrInsufficientBalance{ Requested: amount.String(), From 6271e7abb3f9043857227d1ea553b8ee54bdc09b Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 18 Sep 2024 13:37:21 +0200 Subject: [PATCH 25/33] create bank account when instantiating bank --- cmd/zetae2e/config/localnet.yml | 4 ---- contrib/localnet/scripts/start-zetacored.sh | 3 --- precompiles/bank/bank.go | 7 +++++++ precompiles/bank/method_balance_of.go | 10 ++++++++++ precompiles/bank/method_deposit.go | 9 +++++++++ precompiles/bank/method_withdraw.go | 9 +++++++++ precompiles/precompiles.go | 4 ++-- precompiles/types/errors.go | 9 +++++++++ precompiles/types/errors_test.go | 12 ++++++++++++ 9 files changed, 58 insertions(+), 9 deletions(-) diff --git a/cmd/zetae2e/config/localnet.yml b/cmd/zetae2e/config/localnet.yml index 799f0ad2c0..5bd207a020 100644 --- a/cmd/zetae2e/config/localnet.yml +++ b/cmd/zetae2e/config/localnet.yml @@ -61,10 +61,6 @@ additional_accounts: bech32_address: "zeta1nry9yeg6njhjrp2ctppa8558vqxal9fxk69zxg" evm_address: "0x98c852651A9CAF2185585843d3D287600Ddf9526" private_key: "bf9456c679bb5a952a9a137fcfc920e0413efdb97c36de1e57455763084230cb" - user_bank_precompile: - bech32_address: "zeta1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqr84sgapz" - evm_address: "0x0000000000000000000000000000000000000067" - private_key: "" policy_accounts: emergency_policy_account: bech32_address: "zeta16m2cnrdwtgweq4njc6t470vl325gw4kp6s7tap" diff --git a/contrib/localnet/scripts/start-zetacored.sh b/contrib/localnet/scripts/start-zetacored.sh index e3f4e707af..1f5e2fca12 100755 --- a/contrib/localnet/scripts/start-zetacored.sh +++ b/contrib/localnet/scripts/start-zetacored.sh @@ -269,9 +269,6 @@ then # v2 erc20 revert tester address=$(yq -r '.additional_accounts.user_v2_erc20_revert.bech32_address' /root/config.yml) zetacored add-genesis-account "$address" 100000000000000000000000000azeta -# bank precompile user - address=$(yq -r '.additional_accounts.user_bank_precompile.bech32_address' /root/config.yml) - zetacored add-genesis-account "$address" 000000000000000000000000000azeta # 3. Copy the genesis.json to all the nodes .And use it to create a gentx for every node zetacored gentx operator 1000000000000000000000azeta --chain-id=$CHAINID --keyring-backend=$KEYRING --gas-prices 20000000000azeta diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 9e9f116d5f..18898969a2 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -4,6 +4,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -58,11 +59,17 @@ type Contract struct { } func NewIBankContract( + ctx sdk.Context, bankKeeper bank.Keeper, fungibleKeeper fungiblekeeper.Keeper, cdc codec.Codec, kvGasConfig storetypes.GasConfig, ) *Contract { + accAddress := sdk.AccAddress(ContractAddress.Bytes()) + if fungibleKeeper.GetAuthKeeper().GetAccount(ctx, accAddress) == nil { + fungibleKeeper.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + } + // Instantiate the ZRC20 ABI only one time. // This avoids instantiating it every time deposit or withdraw are called. zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() diff --git a/precompiles/bank/method_balance_of.go b/precompiles/bank/method_balance_of.go index 1045a393df..04e71a60d4 100644 --- a/precompiles/bank/method_balance_of.go +++ b/precompiles/bank/method_balance_of.go @@ -32,6 +32,16 @@ func (c *Contract) balanceOf( return nil, err } + // Safety check: token has to be a valid whitelisted ZRC20 and not be paused. + // Do not check for t.Paused, as the balance is read only the EOA won't be able to operate. + _, found := c.fungibleKeeper.GetForeignCoins(ctx, zrc20Addr.String()) + if !found { + return nil, &ptypes.ErrInvalidToken{ + Got: zrc20Addr.String(), + Reason: "token is not a whitelisted ZRC20", + } + } + // Bank Keeper GetBalance returns the specified Cosmos coin balance for a given address. // Check explicitly the balance is a non-negative non-nil value. coin := c.bankKeeper.GetBalance(ctx, toAddr, ZRC20ToCosmosDenom(zrc20Addr)) diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 88778efdd1..88d8873da0 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -48,6 +48,15 @@ func (c *Contract) deposit( return nil, err } + // Safety check: token has to be a valid whitelisted ZRC20 and not be paused. + t, found := c.fungibleKeeper.GetForeignCoins(ctx, zrc20Addr.String()) + if !found || t.Paused { + return nil, &ptypes.ErrInvalidToken{ + Got: zrc20Addr.String(), + Reason: "token is not a whitelisted ZRC20 or it's paused", + } + } + // Check for enough balance. // function balanceOf(address account) public view virtual override returns (uint256) resBalanceOf, err := c.CallContract( diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index 2833ea1cdc..59d98d2da1 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -49,6 +49,15 @@ func (c *Contract) withdraw( return nil, err } + // Safety check: token has to be a valid whitelisted ZRC20 and not be paused. + t, found := c.fungibleKeeper.GetForeignCoins(ctx, zrc20Addr.String()) + if !found || t.Paused { + return nil, &ptypes.ErrInvalidToken{ + Got: zrc20Addr.String(), + Reason: "token is not a whitelisted ZRC20 or it's paused", + } + } + // Caller has to have enough cosmos coin balance to withdraw the requested amount. coin := c.bankKeeper.GetBalance(ctx, fromAddr, ZRC20ToCosmosDenom(zrc20Addr)) if coin.Amount.IsNil() { diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index b67df7e76c..cdd5e2ff74 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -58,8 +58,8 @@ func StatefulContracts( } if EnabledStatefulContracts[bank.ContractAddress] { - bankContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { - return bank.NewIBankContract(bankKeeper, *fungibleKeeper, cdc, gasConfig) + bankContract := func(ctx sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { + return bank.NewIBankContract(ctx, bankKeeper, *fungibleKeeper, cdc, gasConfig) } // Append the staking contract to the precompiledContracts slice. diff --git a/precompiles/types/errors.go b/precompiles/types/errors.go index 9a4b172dd4..2d87768574 100644 --- a/precompiles/types/errors.go +++ b/precompiles/types/errors.go @@ -39,6 +39,15 @@ func (e ErrInvalidArgument) Error() string { Token related errors */ +type ErrInvalidToken struct { + Got string + Reason string +} + +func (e ErrInvalidToken) Error() string { + return fmt.Sprintf("invalid token %s: %s", e.Got, e.Reason) +} + type ErrInvalidCoin struct { Got string Negative bool diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go index daa7081f8f..cd3c93e3a2 100644 --- a/precompiles/types/errors_test.go +++ b/precompiles/types/errors_test.go @@ -95,3 +95,15 @@ func Test_ErrInsufficientBalance(t *testing.T) { t.Errorf("Expected %v, got %v", expect, got) } } + +func Test_ErrInvalidToken(t *testing.T) { + e := ErrInvalidToken{ + Got: "foo", + Reason: "bar", + } + got := e.Error() + expect := "invalid token foo: bar" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } +} From 1a4f553513c1697816af10549ec0a40ae55f8147 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 19 Sep 2024 11:17:44 +0200 Subject: [PATCH 26/33] e2e: add good and bad scenarios --- cmd/zetae2e/local/local.go | 1 + cmd/zetae2e/local/precompiles.go | 4 + e2e/e2etests/e2etests.go | 9 +- e2e/e2etests/test_precompiles_bank.go | 206 +++++++++++++++++--------- e2e/utils/require.go | 4 + precompiles/bank/method_withdraw.go | 11 +- 6 files changed, 158 insertions(+), 77 deletions(-) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 7db944ac96..365e2c4858 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -322,6 +322,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestPrecompilesStakingName, e2etests.TestPrecompilesStakingThroughContractName, e2etests.TestPrecompilesBankName, + e2etests.TestPrecompilesBankFailName, } } diff --git a/cmd/zetae2e/local/precompiles.go b/cmd/zetae2e/local/precompiles.go index 400db0d973..46668c9f44 100644 --- a/cmd/zetae2e/local/precompiles.go +++ b/cmd/zetae2e/local/precompiles.go @@ -35,6 +35,10 @@ func statefulPrecompilesTestRoutine( precompileRunner.Logger.Print("🏃 starting stateful precompiled contracts tests") startTime := time.Now() + // Send ERC20 that will be depositted into ERC20ZRC20 tokens. + txERC20Send := deployerRunner.SendERC20OnEvm(account.EVMAddress(), 10000) + precompileRunner.WaitForTxReceiptOnEvm(txERC20Send) + testsToRun, err := precompileRunner.GetE2ETestsToRunByName( e2etests.AllE2ETests, testNames..., diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 01872dc37b..529ffc2f62 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -160,6 +160,7 @@ const ( TestPrecompilesStakingName = "precompile_contracts_staking" TestPrecompilesStakingThroughContractName = "precompile_contracts_staking_through_contract" TestPrecompilesBankName = "precompile_contracts_bank" + TestPrecompilesBankFailName = "precompile_contracts_bank_fail" TestPrecompilesBankThroughContractName = "precompile_contracts_bank_through_contract" ) @@ -881,8 +882,14 @@ var AllE2ETests = []runner.E2ETest{ ), runner.NewE2ETest( TestPrecompilesBankName, - "test stateful precompiled contracts bank", + "test stateful precompiled contracts bank with ZRC20 tokens", []runner.ArgDefinition{}, TestPrecompilesBank, ), + runner.NewE2ETest( + TestPrecompilesBankFailName, + "test stateful precompiled contracts bank with non ZRC20 tokens", + []runner.ArgDefinition{}, + TestPrecompilesBankNonZRC20, + ), } diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 881fdb903e..8356d24518 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -4,7 +4,6 @@ import ( "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" @@ -22,6 +21,125 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { r.ZEVMAuth.GasLimit = previousGasLimit }() + totalAmount := big.NewInt(1e3) + depositAmount := big.NewInt(500) + higherBalanceAmount := big.NewInt(1001) + higherAllowanceAmount := big.NewInt(501) + spender := r.EVMAddress() + + // Get ERC20ZRC20. + txHash := r.DepositERC20WithAmountAndMessage(r.EVMAddress(), totalAmount, []byte{}) + utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + // Create a bank contract caller. + bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create bank contract caller") + + // Cosmos coin balance should be 0 at this point. + cosmosBalance, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + require.Equal(r, uint64(0), cosmosBalance.Uint64(), "spender cosmos coin balance should be 0") + + // Approve allowance of 500 ERC20ZRC20 tokens for the bank contract. Should pass. + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, depositAmount) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Approve ETHZRC20 bank allowance tx failed") + + // Deposit 501 ERC20ZRC20 tokens to the bank contract. + // It's higher than allowance but lower than balance, should fail. + tx, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, higherAllowanceAmount) + require.NoError(r, err, "Call bank.Deposit() with amout higher than allowance") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than allowed should fail") + + // Add 500 to allowance for a total of 1000 ERC20ZRC20 tokens. + tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, depositAmount) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Approve ETHZRC20 bank allowance tx failed") + + // Deposit 1001 ERC20ZRC20 tokens to the bank contract. + // It's higher than spender balance but within approved allowance, should fail. + tx, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, higherBalanceAmount) + require.NoError(r, err, "Call bank.Deposit() with amout higher than balance") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than balance should fail") + + // Deposit 500 ERC20ZRC20 tokens to the bank contract. Should pass. + tx, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, depositAmount) + require.NoError(r, err, "Call bank.Deposit() with correct amount") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Depositting a correct amount should pass") + + // Check the deposit event. + eventDeposit, err := bankContract.ParseDeposit(*receipt.Logs[0]) + require.NoError(r, err, "Parse Deposit event") + require.Equal(r, r.EVMAddress(), eventDeposit.Zrc20Depositor, "Deposit event token should be r.EVMAddress()") + require.Equal(r, r.ERC20ZRC20Addr, eventDeposit.Zrc20Token, "Deposit event token should be ERC20ZRC20Addr") + require.Equal(r, depositAmount, eventDeposit.Amount, "Deposit event amount should be 500") + + // Spender: cosmos coin balance should be 500 at this point. + cosmosBalance, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + require.Equal(r, uint64(500), cosmosBalance.Uint64(), "spender cosmos coin balance should be 500") + + // Bank: ERC20ZRC20 balance should be 500 tokens locked. + bankZRC20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bank.ContractAddress) + require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") + require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") + + // Try to withdraw 501 ERC20ZRC20 tokens. Should fail. + tx, err = bankContract.Withdraw(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(501)) + require.NoError(r, err, "Error calling bank.withdraw()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "Withdrawing more than cosmos coin balance amount should fail") + + // Bank: ERC20ZRC20 balance should be 500 tokens locked after a failed withdraw. + // No tokens should be unlocked with a failed withdraw. + bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bank.ContractAddress) + require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") + require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") + + // Try to withdraw 500 ERC20ZRC20 tokens. Should pass. + tx, err = bankContract.Withdraw(r.ZEVMAuth, r.ERC20ZRC20Addr, depositAmount) + require.NoError(r, err, "Error calling bank.withdraw()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Withdraw correct amount should pass") + + // Check the withdraw event. + eventWithdraw, err := bankContract.ParseWithdraw(*receipt.Logs[0]) + require.NoError(r, err, "Parse Deposit event") + require.Equal(r, r.EVMAddress(), eventWithdraw.Zrc20Withdrawer, "Deposit event token should be r.EVMAddress()") + require.Equal(r, r.ERC20ZRC20Addr, eventWithdraw.Zrc20Token, "Deposit event token should be ERC20ZRC20Addr") + require.Equal(r, depositAmount, eventWithdraw.Amount, "Deposit event amount should be 500") + + // Spender: cosmos coin balance should be 0 at this point. + cosmosBalance, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + require.Equal(r, uint64(0), cosmosBalance.Uint64(), "spender cosmos coin balance should be 0") + + // Spender: ERC20ZRC20 balance should be 1000 at this point. + zrc20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + require.Equal(r, uint64(1000), zrc20Balance.Uint64(), "spender ERC20ZRC20 balance should be 1000") + + // Bank: ERC20ZRC20 balance should be 0 tokens locked. + bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bank.ContractAddress) + require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") + require.Equal(r, uint64(0), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 0") +} + +func TestPrecompilesBankNonZRC20(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + // Increase the gasLimit. It's required because of the gas consumed by precompiled functions. + previousGasLimit := r.ZEVMAuth.GasLimit + r.ZEVMAuth.GasLimit = 10_000_000 + defer func() { + r.ZEVMAuth.GasLimit = previousGasLimit + }() + spender, bankAddr := r.EVMAddress(), bank.ContractAddress // Create a bank contract caller. @@ -32,15 +150,15 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { approveAmount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(50)) r.DepositAndApproveWZeta(approveAmount) - // Initial WZETA spender balance should be 50. - initialBalance, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) - require.NoError(r, err, "Error getting initial balance") - require.EqualValues(r, approveAmount, initialBalance, "spender balance should be 50") - - // Initial cosmos coin spender balance should be 0. - retBalanceOf, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) - require.NoError(r, err, "Error calling bank.balanceOf()") - require.EqualValues(r, uint64(0), retBalanceOf.Uint64(), "Initial cosmos coins balance has to be 0") + // Non ZRC20 balanceOf check should fail. + _, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) + require.Error(r, err, "bank.balanceOf() should error out when checking for non ZRC20 balance") + require.Contains( + r, + err.Error(), + "invalid token 0x5F0b1a82749cb4E2278EC87F8BF6B618dC71a8bf: token is not a whitelisted ZRC20", + "Error should be 'token is not a whitelisted ZRC20'", + ) // Allow the bank contract to spend 25 WZeta tokens. tx, err := r.WZeta.Approve(r.ZEVMAuth, bankAddr, big.NewInt(25)) @@ -49,73 +167,19 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.EqualValues(r, uint64(1), receipt.Status, "approve allowance tx failed") // Check the allowance of the bank in WZeta tokens. Should be 25. - balance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, bankAddr) + allowance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, bankAddr) require.NoError(r, err, "Error retrieving bank allowance") - require.EqualValues(r, uint64(25), balance.Uint64(), "Error allowance for bank contract") + require.EqualValues(r, uint64(25), allowance.Uint64(), "Error allowance for bank contract") - // Call Deposit with 25 coins. + // Call Deposit with 25 Non ZRC20 tokens. Should fail. tx, err = bankContract.Deposit(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) require.NoError(r, err, "Error calling bank.deposit()") receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequireTxSuccessful(r, receipt, "withdraw tx failed") - - // Deposit event should be emitted. - depositEvent, err := bankContract.ParseDeposit(*receipt.Logs[0]) - require.NoError(r, err) - require.Equal(r, big.NewInt(25).Uint64(), depositEvent.Amount.Uint64()) - require.Equal(r, common.BytesToAddress(spender.Bytes()), depositEvent.Zrc20Depositor) - require.Equal(r, r.WZetaAddr, depositEvent.Zrc20Token) - - // After deposit, cosmos coin spender balance should be 25. - retBalanceOf, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) - require.NoError(r, err, "Error calling bank.balanceOf()") - require.EqualValues(r, uint64(25), retBalanceOf.Uint64(), "balanceOf result has to be 25") - - // After deposit, WZeta spender balance should be 25 less than initial. - finalBalance, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) - require.NoError(r, err, "Error retrieving final owner balance") - require.EqualValues( - r, - initialBalance.Uint64()-25, // expected - finalBalance.Uint64(), // actual - "Final balance should be initial - 25", - ) - - // After deposit, WZeta bank balance should be 25. - balance, err = r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddr) - require.NoError(r, err, "Error retrieving bank's balance") - require.EqualValues(r, uint64(25), balance.Uint64(), "Wrong locked WZeta amount in bank contract") + require.Equal(r, uint64(0), receipt.Status, "Non ZRC20 deposit should fail") - // Withdraw 15 coins to spender. - tx, err = bankContract.Withdraw(r.ZEVMAuth, r.WZetaAddr, big.NewInt(15)) + // Call Withdraw with 25 on ZRC20 tokens. Should fail. + tx, err = bankContract.Withdraw(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) require.NoError(r, err, "Error calling bank.withdraw()") receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) - utils.RequireTxSuccessful(r, receipt, "withdraw tx failed") - - // Withdraw event should be emitted. - withdrawEvent, err := bankContract.ParseWithdraw(*receipt.Logs[0]) - require.NoError(r, err) - require.Equal(r, big.NewInt(15).Uint64(), withdrawEvent.Amount.Uint64()) - require.Equal(r, common.BytesToAddress(spender.Bytes()), withdrawEvent.Zrc20Withdrawer) - require.Equal(r, r.WZetaAddr, withdrawEvent.Zrc20Token) - - // After withdraw, WZeta spender balance should be only 10 less than initial. (25 - 15 = 10) - afterWithdraw, err := r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) - require.NoError(r, err, "Error retrieving final owner balance") - require.EqualValues( - r, - initialBalance.Uint64()-10, // expected - afterWithdraw.Uint64(), // actual - "Balance after withdraw should be initial - 10", - ) - - // After withdraw, cosmos coin spender balance should be 10. - retBalanceOf, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) - require.NoError(r, err, "Error calling bank.balanceOf()") - require.EqualValues(r, uint64(10), retBalanceOf.Uint64(), "balanceOf result has to be 10") - - // Final WZETA bank balance should be 10. - balance, err = r.WZeta.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddr) - require.NoError(r, err, "Error retrieving bank's allowance") - require.EqualValues(r, uint64(10), balance.Uint64(), "Wrong locked WZeta amount in bank contract") + require.Equal(r, uint64(0), receipt.Status, "Non ZRC20 withdraw should fail") } diff --git a/e2e/utils/require.go b/e2e/utils/require.go index b139a2cad5..e610219e15 100644 --- a/e2e/utils/require.go +++ b/e2e/utils/require.go @@ -43,5 +43,9 @@ func errSuffix(msgAndArgs ...any) string { template := "; " + msgAndArgs[0].(string) + if len(msgAndArgs) == 1 { + return template + } + return fmt.Sprintf(template, msgAndArgs[1:]) } diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index 59d98d2da1..72c380792b 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -87,7 +87,7 @@ func (c *Contract) withdraw( c.zrc20ABI, zrc20Addr, "balanceOf", - []interface{}{caller}, + []interface{}{ContractAddress}, ) if err != nil { return nil, &ptypes.ErrUnexpected{ @@ -97,21 +97,22 @@ func (c *Contract) withdraw( } balance, ok := resBalanceOf[0].(*big.Int) - if !ok || balance.Cmp(amount) < 0 { + if !ok || balance.Cmp(amount) == -1 { return nil, &ptypes.ErrInvalidAmount{ Got: "not enough bank balance", } } // 2. Effect: transfer balance. - // function transferFrom(address sender, address recipient, uint256 amount) + + // function transfer(address recipient, uint256 amount) public virtual override returns (bool) resTransferFrom, err := c.CallContract( ctx, &c.fungibleKeeper, c.zrc20ABI, zrc20Addr, - "transferFrom", - []interface{}{ContractAddress /* sender */, caller /* receiver */, amount}, + "transfer", + []interface{}{caller /* sender */, amount}, ) if err != nil { return nil, &ptypes.ErrUnexpected{ From 9ed6bfb8722b2eea065ba34f6318fa522502dca5 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 19 Sep 2024 11:31:47 +0200 Subject: [PATCH 27/33] modify fmt --- precompiles/types/types.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/precompiles/types/types.go b/precompiles/types/types.go index dad01f348f..fd153d43b2 100644 --- a/precompiles/types/types.go +++ b/precompiles/types/types.go @@ -34,7 +34,8 @@ type Registrable interface { } type ContractCaller interface { - CallContract(ctx sdk.Context, + CallContract( + ctx sdk.Context, fungibleKeeper *fungiblekeeper.Keeper, abi *abi.ABI, dst common.Address, From b8094e230096bc6811d8302b958e8088f7224286 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 19 Sep 2024 14:50:42 +0200 Subject: [PATCH 28/33] chore: group input into eventData struct --- precompiles/bank/logs.go | 24 ++++++++++++++---------- precompiles/bank/method_deposit.go | 2 +- precompiles/bank/method_withdraw.go | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/precompiles/bank/logs.go b/precompiles/bank/logs.go index d20dacd24f..36b877dfa5 100644 --- a/precompiles/bank/logs.go +++ b/precompiles/bank/logs.go @@ -10,31 +10,35 @@ import ( "github.com/zeta-chain/node/precompiles/logs" ) +type eventData struct { + zrc20Addr common.Address + zrc20Token common.Address + cosmosAddr string + cosmosCoin string + amount *big.Int +} + func (c *Contract) addEventLog( ctx sdk.Context, stateDB vm.StateDB, eventName string, - zrc20Addr common.Address, - zrc20Token common.Address, - cosmosAddr string, - cosmosCoin string, - amount *big.Int, + eventData eventData, ) error { event := c.Abi().Events[eventName] topics, err := logs.MakeTopics( event, - []interface{}{common.BytesToAddress(zrc20Addr.Bytes())}, - []interface{}{common.BytesToAddress(zrc20Token.Bytes())}, - []interface{}{cosmosCoin}, + []interface{}{common.BytesToAddress(eventData.zrc20Addr.Bytes())}, + []interface{}{common.BytesToAddress(eventData.zrc20Token.Bytes())}, + []interface{}{eventData.cosmosCoin}, ) if err != nil { return err } data, err := logs.PackArguments([]logs.Argument{ - {Type: "string", Value: cosmosAddr}, - {Type: "uint256", Value: amount}, + {Type: "string", Value: eventData.cosmosAddr}, + {Type: "uint256", Value: eventData.amount}, }) if err != nil { return err diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 88d8873da0..fb9c8c8848 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -157,7 +157,7 @@ func (c *Contract) deposit( } } - if err := c.addEventLog(ctx, evm.StateDB, DepositEventName, caller, zrc20Addr, toAddr.String(), coinSet.Denoms()[0], amount); err != nil { + if err := c.addEventLog(ctx, evm.StateDB, DepositEventName, eventData{caller, zrc20Addr, toAddr.String(), coinSet.Denoms()[0], amount}); err != nil { return nil, &ptypes.ErrUnexpected{ When: "AddDepositLog", Got: err.Error(), diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index 72c380792b..a3970f464a 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -144,7 +144,7 @@ func (c *Contract) withdraw( } } - if err := c.addEventLog(ctx, evm.StateDB, WithdrawEventName, caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount); err != nil { + if err := c.addEventLog(ctx, evm.StateDB, WithdrawEventName, eventData{caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount}); err != nil { return nil, &ptypes.ErrUnexpected{ When: "AddWithdrawLog", Got: err.Error(), From a7533eb18e61c15c5f3b15ced6e17a1e54d88ce4 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 19 Sep 2024 18:18:38 +0200 Subject: [PATCH 29/33] docs: document bank's methods --- precompiles/bank/method_balance_of.go | 5 +++++ precompiles/bank/method_deposit.go | 13 ++++++++++++- precompiles/bank/method_withdraw.go | 6 ++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/precompiles/bank/method_balance_of.go b/precompiles/bank/method_balance_of.go index 04e71a60d4..e4bc644a2b 100644 --- a/precompiles/bank/method_balance_of.go +++ b/precompiles/bank/method_balance_of.go @@ -8,6 +8,11 @@ import ( ptypes "github.com/zeta-chain/node/precompiles/types" ) +// balanceOf returns the balance of cosmos coins minted by the bank's deposit function, +// for a given cosmos account calculated with toAddr := sdk.AccAddress(addr.Bytes()). +// The denomination of the cosmos coin will be "zrc20/0x12345" where 0x12345 is the ZRC20 address. +// Call this function using solidity with the following signature: +// From IBank.sol: function balanceOf(address zrc20, address user) external view returns (uint256 balance); func (c *Contract) balanceOf( ctx sdk.Context, method *abi.Method, diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index fb9c8c8848..fd4bfe124e 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -12,7 +12,18 @@ import ( "github.com/zeta-chain/node/x/fungible/types" ) -// From IBank.sol: function deposit(address zrc20, uint256 amount) external returns (bool success); +// deposit is used to deposit ZRC20 into the bank contract, and receive the same amount of cosmos coins in exchange. +// The denomination of the cosmos coin will be "zrc20/ZRC20Address", as an example depossiting an arbitrary ZRC20 token with +// address 0x12345 will mint cosmos coins with the denomination "zrc20/0x12345". +// The caller cosmos address will be calculated from the EVM caller address. by executing toAddr := sdk.AccAddress(addr.Bytes()). +// This function can be think of a permissionless way of minting cosmos coins. +// This is how deposit works: +// - The caller has to allow the bank contract to spend a certain amount ZRC20 token coins on its behalf. This is mandatory. +// - Then, the caller calls deposit(ZRC20 address, amount), to deposit the amount and receive cosmos coins. +// - The bank will check there's enough balance, the caller is not a blocked address, and the token is a not paused ZRC20. +// - Then the cosmos coins "zrc20/0x12345" will be minted and sent to the caller's cosmos address. +// Call this function using solidity with the following signature: +// - From IBank.sol: function deposit(address zrc20, uint256 amount) external returns (bool success); func (c *Contract) deposit( ctx sdk.Context, evm *vm.EVM, diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index a3970f464a..81b7bcef4b 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -13,7 +13,13 @@ import ( "github.com/zeta-chain/node/x/fungible/types" ) +// withdraw is used to withdraw cosmos coins minted using the bank's deposit function. +// The caller has to have enough cosmos coin on its cosmos account balance to withdraw the requested amount. +// After all check pass the bank will burn the cosmos coins and transfer the ZRC20 amount to the withdrawer. +// The cosmos coins have the denomination of "zrc20/0x12345" where 0x12345 is the ZRC20 address. +// Call this function using solidity with the following signature: // From IBank.sol: function withdraw(address zrc20, uint256 amount) external returns (bool success); +// The address to be passed to the function is the ZRC20 address, like in 0x12345. func (c *Contract) withdraw( ctx sdk.Context, evm *vm.EVM, From 67ad4faac4bce888d9cb89af9b5308f53cccc76a Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Fri, 20 Sep 2024 09:57:24 +0200 Subject: [PATCH 30/33] chore: generate files with suffix .gen.go --- codecov.yml | 4 +--- precompiles/bank/{IBank.go => IBank.gen.go} | 0 precompiles/bank/bindings.go | 2 +- precompiles/prototype/{IPrototype.go => IPrototype.gen.go} | 0 precompiles/prototype/bindings.go | 2 +- precompiles/staking/{IStaking.go => IStaking.gen.go} | 0 precompiles/staking/bindings.go | 2 +- 7 files changed, 4 insertions(+), 6 deletions(-) rename precompiles/bank/{IBank.go => IBank.gen.go} (100%) rename precompiles/prototype/{IPrototype.go => IPrototype.gen.go} (100%) rename precompiles/staking/{IStaking.go => IStaking.gen.go} (100%) diff --git a/codecov.yml b/codecov.yml index bface7f7ba..fee85c9c04 100644 --- a/codecov.yml +++ b/codecov.yml @@ -80,6 +80,4 @@ ignore: - "precompiles/**/*.abi" - "precompiles/**/*.json" - "precompiles/**/*.sol" - - "precompiles/prototype/IPrototype.go" - - "precompiles/staking/IStaking.go" - - "precompiles/bank/IBank.go" + - "precompiles/**/*.gen.go" diff --git a/precompiles/bank/IBank.go b/precompiles/bank/IBank.gen.go similarity index 100% rename from precompiles/bank/IBank.go rename to precompiles/bank/IBank.gen.go diff --git a/precompiles/bank/bindings.go b/precompiles/bank/bindings.go index d127657566..98f35ceeee 100644 --- a/precompiles/bank/bindings.go +++ b/precompiles/bank/bindings.go @@ -1,6 +1,6 @@ //go:generate sh -c "solc IBank.sol --combined-json abi | jq '.contracts.\"IBank.sol:IBank\"' > IBank.json" //go:generate sh -c "cat IBank.json | jq .abi > IBank.abi" -//go:generate sh -c "abigen --abi IBank.abi --pkg bank --type IBank --out IBank.go" +//go:generate sh -c "abigen --abi IBank.abi --pkg bank --type IBank --out IBank.gen.go" package bank diff --git a/precompiles/prototype/IPrototype.go b/precompiles/prototype/IPrototype.gen.go similarity index 100% rename from precompiles/prototype/IPrototype.go rename to precompiles/prototype/IPrototype.gen.go diff --git a/precompiles/prototype/bindings.go b/precompiles/prototype/bindings.go index e4a31a5e56..eeb59d988e 100644 --- a/precompiles/prototype/bindings.go +++ b/precompiles/prototype/bindings.go @@ -1,6 +1,6 @@ //go:generate sh -c "solc IPrototype.sol --combined-json abi | jq '.contracts.\"IPrototype.sol:IPrototype\"' > IPrototype.json" //go:generate sh -c "cat IPrototype.json | jq .abi > IPrototype.abi" -//go:generate sh -c "abigen --abi IPrototype.abi --pkg prototype --type IPrototype --out IPrototype.go" +//go:generate sh -c "abigen --abi IPrototype.abi --pkg prototype --type IPrototype --out IPrototype.gen.go" package prototype diff --git a/precompiles/staking/IStaking.go b/precompiles/staking/IStaking.gen.go similarity index 100% rename from precompiles/staking/IStaking.go rename to precompiles/staking/IStaking.gen.go diff --git a/precompiles/staking/bindings.go b/precompiles/staking/bindings.go index 5e182735be..324289b9f0 100644 --- a/precompiles/staking/bindings.go +++ b/precompiles/staking/bindings.go @@ -1,6 +1,6 @@ //go:generate sh -c "solc IStaking.sol --combined-json abi | jq '.contracts.\"IStaking.sol:IStaking\"' > IStaking.json" //go:generate sh -c "cat IStaking.json | jq .abi > IStaking.abi" -//go:generate sh -c "abigen --abi IStaking.abi --pkg staking --type IStaking --out IStaking.go" +//go:generate sh -c "abigen --abi IStaking.abi --pkg staking --type IStaking --out IStaking.gen.go" package staking From 82f5204a40820325cafba5755c3ee634344aa5ca Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Fri, 20 Sep 2024 10:13:19 +0200 Subject: [PATCH 31/33] chore: assert errors with errorIs --- precompiles/types/errors_test.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go index cd3c93e3a2..f430e1178d 100644 --- a/precompiles/types/errors_test.go +++ b/precompiles/types/errors_test.go @@ -1,6 +1,10 @@ package types -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/require" +) func Test_ErrInvalidAddr(t *testing.T) { e := ErrInvalidAddr{ @@ -12,6 +16,7 @@ func Test_ErrInvalidAddr(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidAddr{"foo", "bar"}, e) } func Test_ErrInvalidNumberOfArgs(t *testing.T) { @@ -24,6 +29,7 @@ func Test_ErrInvalidNumberOfArgs(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidNumberOfArgs{1, 2}, e) } func Test_ErrInvalidArgument(t *testing.T) { @@ -35,6 +41,7 @@ func Test_ErrInvalidArgument(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidArgument{"foo"}, e) } func Test_ErrInvalidMethod(t *testing.T) { @@ -46,6 +53,7 @@ func Test_ErrInvalidMethod(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidMethod{"foo"}, e) } func Test_ErrInvalidCoin(t *testing.T) { @@ -59,6 +67,7 @@ func Test_ErrInvalidCoin(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidCoin{"foo", true, false}, e) } func Test_ErrInvalidAmount(t *testing.T) { @@ -70,6 +79,7 @@ func Test_ErrInvalidAmount(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidAmount{"foo"}, e) } func Test_ErrUnexpected(t *testing.T) { @@ -82,6 +92,7 @@ func Test_ErrUnexpected(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrUnexpected{"foo", "bar"}, e) } func Test_ErrInsufficientBalance(t *testing.T) { @@ -94,6 +105,7 @@ func Test_ErrInsufficientBalance(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInsufficientBalance{"foo", "bar"}, e) } func Test_ErrInvalidToken(t *testing.T) { @@ -106,4 +118,5 @@ func Test_ErrInvalidToken(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidToken{"foo", "bar"}, e) } From 4d0a57bb9424dd94ca93a55ff5ff86bb49cab3cd Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Fri, 20 Sep 2024 11:40:28 +0200 Subject: [PATCH 32/33] chore: reset e2e test by resetting allowance --- e2e/e2etests/test_precompiles_bank.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 8356d24518..1e78212355 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -19,6 +19,13 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { r.ZEVMAuth.GasLimit = 10_000_000 defer func() { r.ZEVMAuth.GasLimit = previousGasLimit + + // Reset the allowance to 0; this is needed when running upgrade tests where + // this test runs twice. + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, big.NewInt(0)) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Resetting allowance failed") }() totalAmount := big.NewInt(1e3) @@ -53,8 +60,8 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than allowed should fail") - // Add 500 to allowance for a total of 1000 ERC20ZRC20 tokens. - tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, depositAmount) + // Approve allowance of 1000 ERC20ZRC20 tokens. + tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, big.NewInt(1e3)) require.NoError(r, err) receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) utils.RequireTxSuccessful(r, receipt, "Approve ETHZRC20 bank allowance tx failed") From c9bf427e66302e5705b8e7022824e4b6a7e8efcf Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Sat, 21 Sep 2024 20:25:11 +0200 Subject: [PATCH 33/33] test: add first batch of unit test --- precompiles/bank/bank_test.go | 156 ++++++++++++++++ precompiles/bank/coin.go | 5 +- precompiles/bank/coin_test.go | 29 +++ precompiles/bank/method_deposit.go | 8 +- precompiles/bank/method_test.go | 284 +++++++++++++++++++++++++++++ precompiles/types/errors.go | 9 +- precompiles/types/errors_test.go | 21 ++- 7 files changed, 495 insertions(+), 17 deletions(-) create mode 100644 precompiles/bank/bank_test.go create mode 100644 precompiles/bank/coin_test.go create mode 100644 precompiles/bank/method_test.go diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go new file mode 100644 index 0000000000..518c330a7f --- /dev/null +++ b/precompiles/bank/bank_test.go @@ -0,0 +1,156 @@ +package bank + +import ( + "encoding/json" + "math/big" + "testing" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/require" + ethermint "github.com/zeta-chain/ethermint/types" + "github.com/zeta-chain/node/testutil/keeper" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_IBankContract(t *testing.T) { + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + fungibleKeeper, ctx, keepers, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + t.Run("should create contract and check address and ABI", func(t *testing.T) { + contract := NewIBankContract(ctx, keepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + require.NotNil(t, contract, "NewIBankContract() should not return a nil contract") + + address := contract.Address() + require.Equal(t, ContractAddress, address, "contract address should match the precompiled address") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + }) + + t.Run("should check methods are present in ABI", func(t *testing.T) { + contract := NewIBankContract(ctx, keepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + abi := contract.Abi() + + require.NotNil(t, abi.Methods[DepositMethodName], "deposit method should be present in the ABI") + require.NotNil(t, abi.Methods[WithdrawMethodName], "withdraw method should be present in the ABI") + require.NotNil(t, abi.Methods[BalanceOfMethodName], "balanceOf method should be present in the ABI") + }) + + t.Run("should check gas requirements for methods", func(t *testing.T) { + contract := NewIBankContract(ctx, keepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + abi := contract.Abi() + var method [4]byte + + t.Run("deposit", func(t *testing.T) { + gasDeposit := contract.RequiredGas(abi.Methods[DepositMethodName].ID) + copy(method[:], abi.Methods[DepositMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasDeposit, + "deposit method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasDeposit, + ) + }) + + t.Run("withdraw", func(t *testing.T) { + gasWithdraw := contract.RequiredGas(abi.Methods[WithdrawMethodName].ID) + copy(method[:], abi.Methods[WithdrawMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasWithdraw, + "withdraw method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasWithdraw, + ) + }) + + t.Run("balanceOf", func(t *testing.T) { + gasBalanceOf := contract.RequiredGas(abi.Methods[BalanceOfMethodName].ID) + copy(method[:], abi.Methods[BalanceOfMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasBalanceOf, + "balanceOf method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasBalanceOf, + ) + }) + + t.Run("invalid method", func(t *testing.T) { + invalidMethodBytes := []byte("invalidMethod") + gasInvalidMethod := contract.RequiredGas(invalidMethodBytes) + require.Equal( + t, + uint64(0), + gasInvalidMethod, + "invalid method should require %d gas, got %d", + uint64(0), + gasInvalidMethod, + ) + }) + }) +} + +func Test_InvalidMethod(t *testing.T) { + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + fungibleKeeper, ctx, keepers, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + contract := NewIBankContract(ctx, keepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + require.NotNil(t, contract, "NewIBankContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + _, doNotExist := abi.Methods["invalidMethod"] + require.False(t, doNotExist, "invalidMethod should not be present in the ABI") +} + +func Test_InvalidABI(t *testing.T) { + IBankMetaData.ABI = "invalid json" + defer func() { + if r := recover(); r != nil { + require.IsType(t, &json.SyntaxError{}, r, "expected error type: json.SyntaxError, got: %T", r) + } + }() + + initABI() +} + +func Test_getEVMCallerAddress(t *testing.T) { + mockEVM := vm.EVM{ + TxContext: vm.TxContext{ + Origin: common.Address{}, + }, + } + + mockVMContract := vm.NewContract( + contractRef{address: common.Address{}}, + contractRef{address: ContractAddress}, + big.NewInt(0), + 0, + ) + + // When contract.CallerAddress == evm.Origin, caller is set to contract.CallerAddress. + caller, err := getEVMCallerAddress(&mockEVM, mockVMContract) + require.NoError(t, err) + require.Equal(t, common.Address{}, caller, "address shouldn be the same") + + // When contract.CallerAddress != evm.Origin, caller should be set to evm.Origin. + mockEVM.Origin = sample.EthAddress() + caller, err = getEVMCallerAddress(&mockEVM, mockVMContract) + require.NoError(t, err) + require.Equal(t, mockEVM.Origin, caller, "address should be evm.Origin") +} diff --git a/precompiles/bank/coin.go b/precompiles/bank/coin.go index 868562b1ad..2fab8aedcc 100644 --- a/precompiles/bank/coin.go +++ b/precompiles/bank/coin.go @@ -30,11 +30,12 @@ func createCoinSet(tokenDenom string, amount *big.Int) (sdk.Coins, error) { // and SendCoinsFromModuleToAccount. // But sdk.Coins will only contain one coin, always. coinSet := sdk.NewCoins(coin) - if !coinSet.IsValid() { + if !coinSet.IsValid() || coinSet.Empty() || coinSet.IsAnyNil() || coinSet == nil { return nil, &ptypes.ErrInvalidCoin{ - Got: coinSet.Sort().GetDenomByIndex(0), + Got: coinSet.String(), Negative: coinSet.IsAnyNegative(), Nil: coinSet.IsAnyNil(), + Empty: coinSet.Empty(), } } diff --git a/precompiles/bank/coin_test.go b/precompiles/bank/coin_test.go new file mode 100644 index 0000000000..0a9669e0a6 --- /dev/null +++ b/precompiles/bank/coin_test.go @@ -0,0 +1,29 @@ +package bank + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func Test_ZRC20ToCosmosDenom(t *testing.T) { + address := big.NewInt(12345) // 0x3039 + expected := "zrc20/0x0000000000000000000000000000000000003039" + denom := ZRC20ToCosmosDenom(common.BigToAddress(address)) + require.Equal(t, expected, denom, "denom should be %s, got %s", expected, denom) +} + +func Test_createCoinSet(t *testing.T) { + tokenDenom := "zrc20/0x0000000000000000000000000000000000003039" + amount := big.NewInt(100) + + coinSet, err := createCoinSet(tokenDenom, amount) + require.NoError(t, err, "createCoinSet should not return an error") + require.NotNil(t, coinSet, "coinSet should not be nil") + + coin := coinSet[0] + require.Equal(t, tokenDenom, coin.Denom, "coin denom should be %s, got %s", tokenDenom, coin.Denom) + require.Equal(t, amount, coin.Amount.BigInt(), "coin amount should be %s, got %s", amount, coin.Amount.BigInt()) +} diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index fd4bfe124e..3d139124e3 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -86,9 +86,9 @@ func (c *Contract) deposit( } balance, ok := resBalanceOf[0].(*big.Int) - if !ok || balance.Cmp(amount) < 0 { + if !ok || balance.Cmp(amount) < 0 || balance.Cmp(big.NewInt(0)) <= 0 { return nil, &ptypes.ErrInvalidAmount{ - Got: "not enough balance", + Got: balance.String(), } } @@ -110,9 +110,9 @@ func (c *Contract) deposit( } allowance, ok := resAllowance[0].(*big.Int) - if !ok || allowance.Cmp(amount) < 0 { + if !ok || allowance.Cmp(amount) < 0 || allowance.Cmp(big.NewInt(0)) <= 0 { return nil, &ptypes.ErrInvalidAmount{ - Got: "not enough allowance", + Got: allowance.String(), } } diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go new file mode 100644 index 0000000000..bf94918430 --- /dev/null +++ b/precompiles/bank/method_test.go @@ -0,0 +1,284 @@ +package bank + +import ( + "math/big" + "testing" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" + ethermint "github.com/zeta-chain/ethermint/types" + evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" + "github.com/zeta-chain/ethermint/x/evm/statedb" + "github.com/zeta-chain/node/pkg/chains" + erc1967proxy "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" + ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/keeper" + "github.com/zeta-chain/node/testutil/sample" + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" + gatewayzevm "github.com/zeta-chain/protocol-contracts/v2/pkg/gatewayzevm.sol" +) + +func Test_Deposit(t *testing.T) { + t.Run("should fail when caller has 0 token balance", func(t *testing.T) { + ts := setupChain(t) + + methodID := ts.abi.Methods[DepositMethodName] + + // Set CallerAddress and evm.Origin to the caller address. + // Caller does not have any balance, and bank does not have any allowance. + ts.mockVMContract.CallerAddress = fungibletypes.ModuleAddressEVM + ts.mockEVM.Origin = fungibletypes.ModuleAddressEVM + + // Set the input arguments for the deposit method. + ts.mockVMContract.Input = packInputArgs( + t, + methodID, + []interface{}{ts.zrc20Address, big.NewInt(0)}..., + ) + + _, err := ts.contract.Run(ts.mockEVM, ts.mockVMContract, false) + require.ErrorAs( + t, + ptypes.ErrInvalidAmount{ + Got: "0", + }, + err, + ) + }) + + t.Run("should fail when bank has 0 token allowance", func(t *testing.T) { + ts := setupChain(t) + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressEVM, big.NewInt(1000)) + + methodID := ts.abi.Methods[DepositMethodName] + + // Set CallerAddress and evm.Origin to the caller address. + // Caller does not have any balance, and bank does not have any allowance. + ts.mockVMContract.CallerAddress = fungibletypes.ModuleAddressEVM + ts.mockEVM.Origin = fungibletypes.ModuleAddressEVM + + // Set the input arguments for the deposit method. + ts.mockVMContract.Input = packInputArgs( + t, + methodID, + []interface{}{ts.zrc20Address, big.NewInt(0)}..., + ) + + _, err := ts.contract.Run(ts.mockEVM, ts.mockVMContract, false) + require.ErrorAs( + t, + ptypes.ErrInvalidAmount{ + Got: "0", + }, + err, + ) + }) +} + +/* + Functions to set up the test environment. +*/ + +type testSuite struct { + ctx sdk.Context + fungibleKeeper *fungiblekeeper.Keeper + sdkKeepers keeper.SDKKeepers + contract *Contract + abi abi.ABI + mockEVM *vm.EVM + mockVMContract *vm.Contract + zrc20Address common.Address +} + +func setupChain(t *testing.T) testSuite { + // Initialize basic parameters to mock the chain. + fungibleKeeper, ctx, sdkKeepers, _ := keeper.FungibleKeeper(t) + chainID := getValidChainID(t) + + // Make sure the account store is initialized. + // This is completely needed for accounts to be created in the state. + fungibleKeeper.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) + + // Deploy system contracts in order to deploy a ZRC20 token. + deploySystemContracts(t, ctx, fungibleKeeper, *sdkKeepers.EvmKeeper) + zrc20Address := setupGasCoin(t, ctx, fungibleKeeper, sdkKeepers.EvmKeeper, chainID, "ZRC20", "ZRC20") + + // Keepers and chain configuration. + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + gasConfig := storetypes.TransientGasConfig() + + // Create the bank contract. + contract := NewIBankContract(ctx, sdkKeepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + require.NotNil(t, contract, "NewIBankContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + address := contract.Address() + require.NotNil(t, address, "contract address should not be nil") + + mockEVM := vm.NewEVM( + vm.BlockContext{}, + vm.TxContext{}, + statedb.New(ctx, sdkKeepers.EvmKeeper, statedb.TxConfig{}), + ¶ms.ChainConfig{}, + vm.Config{}, + ) + + mockVMContract := vm.NewContract( + contractRef{address: common.Address{}}, + contractRef{address: ContractAddress}, + big.NewInt(0), + 0, + ) + + return testSuite{ + ctx, + fungibleKeeper, + sdkKeepers, + contract, + abi, + mockEVM, + mockVMContract, + zrc20Address, + } +} + +// setupGasCoin is a helper function to setup the gas coin for testing +func setupGasCoin( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk *evmkeeper.Keeper, + chainID int64, + assetName string, + symbol string, +) (zrc20 common.Address) { + addr, err := k.SetupChainGasCoinAndPool( + ctx, + chainID, + assetName, + symbol, + 8, + nil, + ) + require.NoError(t, err) + assertContractDeployment(t, *evmk, ctx, addr) + return addr +} + +// get a valid chain id independently of the build flag +func getValidChainID(t *testing.T) int64 { + list := chains.DefaultChainsList() + require.NotEmpty(t, list) + require.NotNil(t, list[0]) + return list[0].ChainId +} + +// require that a contract has been deployed by checking stored code is non-empty. +func assertContractDeployment(t *testing.T, k evmkeeper.Keeper, ctx sdk.Context, contractAddress common.Address) { + acc := k.GetAccount(ctx, contractAddress) + require.NotNil(t, acc) + code := k.GetCode(ctx, common.BytesToHash(acc.CodeHash)) + require.NotEmpty(t, code) +} + +// deploySystemContracts deploys the system contracts and returns their addresses. +func deploySystemContracts( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk evmkeeper.Keeper, +) (wzeta, uniswapV2Factory, uniswapV2Router, connector, systemContract common.Address) { + var err error + + wzeta, err = k.DeployWZETA(ctx) + require.NoError(t, err) + require.NotEmpty(t, wzeta) + assertContractDeployment(t, evmk, ctx, wzeta) + + uniswapV2Factory, err = k.DeployUniswapV2Factory(ctx) + require.NoError(t, err) + require.NotEmpty(t, uniswapV2Factory) + assertContractDeployment(t, evmk, ctx, uniswapV2Factory) + + uniswapV2Router, err = k.DeployUniswapV2Router02(ctx, uniswapV2Factory, wzeta) + require.NoError(t, err) + require.NotEmpty(t, uniswapV2Router) + assertContractDeployment(t, evmk, ctx, uniswapV2Router) + + connector, err = k.DeployConnectorZEVM(ctx, wzeta) + require.NoError(t, err) + require.NotEmpty(t, connector) + assertContractDeployment(t, evmk, ctx, connector) + + systemContract, err = k.DeploySystemContract(ctx, wzeta, uniswapV2Factory, uniswapV2Router) + require.NoError(t, err) + require.NotEmpty(t, systemContract) + assertContractDeployment(t, evmk, ctx, systemContract) + + // deploy the gateway contract + contract := deployGatewayContract(t, ctx, k, &evmk, wzeta, sample.EthAddress()) + require.NotEmpty(t, contract) + + return +} + +// deploy upgradable gateway contract and return its address +func deployGatewayContract( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk *evmkeeper.Keeper, + wzeta, admin common.Address, +) common.Address { + // Deploy the gateway contract + implAddr, err := k.DeployContract(ctx, gatewayzevm.GatewayZEVMMetaData) + require.NoError(t, err) + require.NotEmpty(t, implAddr) + assertContractDeployment(t, *evmk, ctx, implAddr) + + // Deploy the proxy contract + gatewayABI, err := gatewayzevm.GatewayZEVMMetaData.GetAbi() + require.NoError(t, err) + + // Encode the initializer data + initializerData, err := gatewayABI.Pack("initialize", wzeta, admin) + require.NoError(t, err) + + gatewayContract, err := k.DeployContract(ctx, erc1967proxy.ERC1967ProxyMetaData, implAddr, initializerData) + require.NoError(t, err) + require.NotEmpty(t, gatewayContract) + assertContractDeployment(t, *evmk, ctx, gatewayContract) + + // store the gateway in the system contract object + sys, found := k.GetSystemContract(ctx) + if !found { + sys = fungibletypes.SystemContract{} + } + sys.Gateway = gatewayContract.Hex() + k.SetSystemContract(ctx, sys) + + return gatewayContract +} + +func packInputArgs(t *testing.T, methodID abi.Method, args ...interface{}) []byte { + input, err := methodID.Inputs.Pack(args...) + require.NoError(t, err) + return append(methodID.ID, input...) +} + +type contractRef struct { + address common.Address +} + +func (c contractRef) Address() common.Address { + return c.address +} diff --git a/precompiles/types/errors.go b/precompiles/types/errors.go index 2d87768574..03e397fa14 100644 --- a/precompiles/types/errors.go +++ b/precompiles/types/errors.go @@ -52,10 +52,17 @@ type ErrInvalidCoin struct { Got string Negative bool Nil bool + Empty bool } func (e ErrInvalidCoin) Error() string { - return fmt.Sprintf("invalid coin: denom: %s, is negative: %v, is nil: %v", e.Got, e.Negative, e.Nil) + return fmt.Sprintf( + "invalid coin: denom: %s, is negative: %v, is nil: %v, is empty: %v", + e.Got, + e.Negative, + e.Nil, + e.Empty, + ) } type ErrInvalidAmount struct { diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go index f430e1178d..c80647a08d 100644 --- a/precompiles/types/errors_test.go +++ b/precompiles/types/errors_test.go @@ -16,7 +16,7 @@ func Test_ErrInvalidAddr(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrInvalidAddr{"foo", "bar"}, e) + require.ErrorIs(t, ErrInvalidAddr{"foo", "bar"}, e) } func Test_ErrInvalidNumberOfArgs(t *testing.T) { @@ -29,7 +29,7 @@ func Test_ErrInvalidNumberOfArgs(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrInvalidNumberOfArgs{1, 2}, e) + require.ErrorIs(t, ErrInvalidNumberOfArgs{1, 2}, e) } func Test_ErrInvalidArgument(t *testing.T) { @@ -41,7 +41,7 @@ func Test_ErrInvalidArgument(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrInvalidArgument{"foo"}, e) + require.ErrorIs(t, ErrInvalidArgument{"foo"}, e) } func Test_ErrInvalidMethod(t *testing.T) { @@ -53,7 +53,7 @@ func Test_ErrInvalidMethod(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrInvalidMethod{"foo"}, e) + require.ErrorIs(t, ErrInvalidMethod{"foo"}, e) } func Test_ErrInvalidCoin(t *testing.T) { @@ -61,13 +61,14 @@ func Test_ErrInvalidCoin(t *testing.T) { Got: "foo", Negative: true, Nil: false, + Empty: false, } got := e.Error() - expect := "invalid coin: denom: foo, is negative: true, is nil: false" + expect := "invalid coin: denom: foo, is negative: true, is nil: false, is empty: false" if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrInvalidCoin{"foo", true, false}, e) + require.ErrorIs(t, ErrInvalidCoin{"foo", true, false, false}, e) } func Test_ErrInvalidAmount(t *testing.T) { @@ -79,7 +80,7 @@ func Test_ErrInvalidAmount(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrInvalidAmount{"foo"}, e) + require.ErrorIs(t, ErrInvalidAmount{"foo"}, e) } func Test_ErrUnexpected(t *testing.T) { @@ -92,7 +93,7 @@ func Test_ErrUnexpected(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrUnexpected{"foo", "bar"}, e) + require.ErrorIs(t, ErrUnexpected{"foo", "bar"}, e) } func Test_ErrInsufficientBalance(t *testing.T) { @@ -105,7 +106,7 @@ func Test_ErrInsufficientBalance(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrInsufficientBalance{"foo", "bar"}, e) + require.ErrorIs(t, ErrInsufficientBalance{"foo", "bar"}, e) } func Test_ErrInvalidToken(t *testing.T) { @@ -118,5 +119,5 @@ func Test_ErrInvalidToken(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } - require.ErrorIs(t, ErrInvalidToken{"foo", "bar"}, e) + require.ErrorIs(t, ErrInvalidToken{"foo", "bar"}, e) }