From 60f0f1ae77a4a4a29e11bf8f749e0f6f96a2e5b1 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 30 Oct 2024 11:32:59 +0100 Subject: [PATCH] feat: distribute ZRC20 rewards function (#3019) * feat: distribute ZRC20 rewards function * add unit testing first batch * fix should return empty array if validators not set unit test * migrate to new test suite * remove function depending on staking precompile * commit last unit test changes * add e2e base * add distribution query client to e2e * e2e: finish disitribute tests * fix event name typo * Update precompiles/types/coin.go Co-authored-by: Tanmay * first batch of reviews * apply reviews * add nonZRC20 token e2e test * add fee collector balance check * check for err while DepositZRC20 * fix CI * fix gosec * modify CreateCoinSet to accept a common.Address * fix lint --------- Co-authored-by: Tanmay Co-authored-by: Tanmay --- changelog.md | 1 + cmd/zetacored/config/prefixes.go | 3 + cmd/zetae2e/local/local.go | 19 +- .../testdistribute/TestDistribute.abi | 64 + .../testdistribute/TestDistribute.bin | 1 + .../testdistribute/TestDistribute.go | 420 +++++ .../testdistribute/TestDistribute.json | 67 + .../testdistribute/TestDistribute.sol | 43 + e2e/contracts/testdistribute/bindings.go | 8 + e2e/contracts/teststaking/TestStaking.sol | 42 +- e2e/e2etests/e2etests.go | 35 +- .../test_precompiles_bank_through_contract.go | 20 +- e2e/e2etests/test_precompiles_distribute.go | 239 +++ ...precompiles_distribute_through_contract.go | 120 ++ e2e/runner/runner.go | 43 +- e2e/utils/require.go | 2 +- pkg/rpc/clients.go | 14 +- precompiles/bank/bank.go | 47 +- precompiles/bank/bank_test.go | 30 - precompiles/bank/logs.go | 4 +- precompiles/bank/method_balance_of.go | 16 +- precompiles/bank/method_deposit.go | 28 +- precompiles/bank/method_test.go | 12 +- precompiles/bank/method_withdraw.go | 32 +- precompiles/precompiles.go | 4 +- precompiles/prototype/prototype.go | 18 +- precompiles/staking/IStaking.abi | 49 + precompiles/staking/IStaking.gen.go | 177 +- precompiles/staking/IStaking.json | 49 + precompiles/staking/IStaking.sol | 33 +- precompiles/staking/const.go | 22 + precompiles/staking/logs.go | 42 +- precompiles/staking/method_distribute.go | 112 ++ precompiles/staking/method_distribute_test.go | 283 +++ .../staking/method_get_all_validators.go | 27 + .../staking/method_get_all_validators_test.go | 57 + precompiles/staking/method_get_shares.go | 50 + precompiles/staking/method_get_shares_test.go | 115 ++ precompiles/staking/method_move_stake.go | 85 + precompiles/staking/method_move_stake_test.go | 481 +++++ precompiles/staking/method_stake.go | 84 + precompiles/staking/method_stake_test.go | 316 ++++ precompiles/staking/method_unstake.go | 77 + precompiles/staking/method_unstake_test.go | 311 ++++ precompiles/staking/staking.go | 377 +--- precompiles/staking/staking_test.go | 1642 ++++------------- precompiles/types/address.go | 48 + precompiles/types/address_test.go | 68 + precompiles/{bank => types}/coin.go | 26 +- precompiles/{bank => types}/coin_test.go | 7 +- .../keeper/zrc20_cosmos_coins_mapping.go | 3 +- 51 files changed, 4070 insertions(+), 1803 deletions(-) create mode 100644 e2e/contracts/testdistribute/TestDistribute.abi create mode 100644 e2e/contracts/testdistribute/TestDistribute.bin create mode 100644 e2e/contracts/testdistribute/TestDistribute.go create mode 100644 e2e/contracts/testdistribute/TestDistribute.json create mode 100644 e2e/contracts/testdistribute/TestDistribute.sol create mode 100644 e2e/contracts/testdistribute/bindings.go create mode 100644 e2e/e2etests/test_precompiles_distribute.go create mode 100644 e2e/e2etests/test_precompiles_distribute_through_contract.go create mode 100644 precompiles/staking/const.go create mode 100644 precompiles/staking/method_distribute.go create mode 100644 precompiles/staking/method_distribute_test.go create mode 100644 precompiles/staking/method_get_all_validators.go create mode 100644 precompiles/staking/method_get_all_validators_test.go create mode 100644 precompiles/staking/method_get_shares.go create mode 100644 precompiles/staking/method_get_shares_test.go create mode 100644 precompiles/staking/method_move_stake.go create mode 100644 precompiles/staking/method_move_stake_test.go create mode 100644 precompiles/staking/method_stake.go create mode 100644 precompiles/staking/method_stake_test.go create mode 100644 precompiles/staking/method_unstake.go create mode 100644 precompiles/staking/method_unstake_test.go create mode 100644 precompiles/types/address.go create mode 100644 precompiles/types/address_test.go rename precompiles/{bank => types}/coin.go (59%) rename precompiles/{bank => types}/coin_test.go (81%) diff --git a/changelog.md b/changelog.md index 9814a0662b..1d5ceb9114 100644 --- a/changelog.md +++ b/changelog.md @@ -25,6 +25,7 @@ * [3012](https://github.com/zeta-chain/node/pull/3012) - integrate authenticated calls erc20 smart contract functionality into protocol * [3025](https://github.com/zeta-chain/node/pull/3025) - standard memo for Bitcoin inbound * [3028](https://github.com/zeta-chain/node/pull/3028) - whitelist connection gater +* [3019](https://github.com/zeta-chain/node/pull/3019) - add ditribute functions to staking precompile ### Refactor diff --git a/cmd/zetacored/config/prefixes.go b/cmd/zetacored/config/prefixes.go index a96a4c57bc..5fcd9218a5 100644 --- a/cmd/zetacored/config/prefixes.go +++ b/cmd/zetacored/config/prefixes.go @@ -6,6 +6,9 @@ const ( // Bech32Prefix defines the Bech32 prefix used for Cronos Accounts Bech32Prefix = "zeta" + // ZRC20DenomPrefix defines the prefix for ZRC20 tokens when converted to sdk.Coin. + ZRC20DenomPrefix = "zrc20/" + // Bech32PrefixAccAddr defines the Bech32 prefix of an account's address Bech32PrefixAccAddr = Bech32Prefix // Bech32PrefixAccPub defines the Bech32 prefix of an account's public key diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 4c6624451c..dca027f3c8 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -332,14 +332,17 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if !skipPrecompiles { precompiledContractTests = []string{ - e2etests.TestPrecompilesPrototypeName, - e2etests.TestPrecompilesPrototypeThroughContractName, - e2etests.TestPrecompilesStakingName, - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - // e2etests.TestPrecompilesStakingThroughContractName, - e2etests.TestPrecompilesBankName, - e2etests.TestPrecompilesBankFailName, - e2etests.TestPrecompilesBankThroughContractName, + // e2etests.TestPrecompilesPrototypeName, + // e2etests.TestPrecompilesPrototypeThroughContractName, + // e2etests.TestPrecompilesStakingName, + // // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + // // e2etests.TestPrecompilesStakingThroughContractName, + // e2etests.TestPrecompilesBankName, + // e2etests.TestPrecompilesBankFailName, + // e2etests.TestPrecompilesBankThroughContractName, + e2etests.TestPrecompilesDistributeName, + e2etests.TestPrecompilesDistributeNonZRC20Name, + e2etests.TestPrecompilesDistributeThroughContractName, } } diff --git a/e2e/contracts/testdistribute/TestDistribute.abi b/e2e/contracts/testdistribute/TestDistribute.abi new file mode 100644 index 0000000000..26aa07af4e --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.abi @@ -0,0 +1,64 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_distributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Distributed", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "distributeThroughContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/e2e/contracts/testdistribute/TestDistribute.bin b/e2e/contracts/testdistribute/TestDistribute.bin new file mode 100644 index 0000000000..5840bf96e5 --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.bin @@ -0,0 +1 @@ +60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033 diff --git a/e2e/contracts/testdistribute/TestDistribute.go b/e2e/contracts/testdistribute/TestDistribute.go new file mode 100644 index 0000000000..18b4201b1a --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.go @@ -0,0 +1,420 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package testdistribute + +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 +) + +// TestDistributeMetaData contains all meta data concerning the TestDistribute contract. +var TestDistributeMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_distributor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Distributed\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"distributeThroughContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", + Bin: "0x60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033", +} + +// TestDistributeABI is the input ABI used to generate the binding from. +// Deprecated: Use TestDistributeMetaData.ABI instead. +var TestDistributeABI = TestDistributeMetaData.ABI + +// TestDistributeBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use TestDistributeMetaData.Bin instead. +var TestDistributeBin = TestDistributeMetaData.Bin + +// DeployTestDistribute deploys a new Ethereum contract, binding an instance of TestDistribute to it. +func DeployTestDistribute(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *TestDistribute, error) { + parsed, err := TestDistributeMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(TestDistributeBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &TestDistribute{TestDistributeCaller: TestDistributeCaller{contract: contract}, TestDistributeTransactor: TestDistributeTransactor{contract: contract}, TestDistributeFilterer: TestDistributeFilterer{contract: contract}}, nil +} + +// TestDistribute is an auto generated Go binding around an Ethereum contract. +type TestDistribute struct { + TestDistributeCaller // Read-only binding to the contract + TestDistributeTransactor // Write-only binding to the contract + TestDistributeFilterer // Log filterer for contract events +} + +// TestDistributeCaller is an auto generated read-only Go binding around an Ethereum contract. +type TestDistributeCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// TestDistributeTransactor is an auto generated write-only Go binding around an Ethereum contract. +type TestDistributeTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// TestDistributeFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type TestDistributeFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// TestDistributeSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type TestDistributeSession struct { + Contract *TestDistribute // 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 +} + +// TestDistributeCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type TestDistributeCallerSession struct { + Contract *TestDistributeCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// TestDistributeTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type TestDistributeTransactorSession struct { + Contract *TestDistributeTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// TestDistributeRaw is an auto generated low-level Go binding around an Ethereum contract. +type TestDistributeRaw struct { + Contract *TestDistribute // Generic contract binding to access the raw methods on +} + +// TestDistributeCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type TestDistributeCallerRaw struct { + Contract *TestDistributeCaller // Generic read-only contract binding to access the raw methods on +} + +// TestDistributeTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type TestDistributeTransactorRaw struct { + Contract *TestDistributeTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewTestDistribute creates a new instance of TestDistribute, bound to a specific deployed contract. +func NewTestDistribute(address common.Address, backend bind.ContractBackend) (*TestDistribute, error) { + contract, err := bindTestDistribute(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &TestDistribute{TestDistributeCaller: TestDistributeCaller{contract: contract}, TestDistributeTransactor: TestDistributeTransactor{contract: contract}, TestDistributeFilterer: TestDistributeFilterer{contract: contract}}, nil +} + +// NewTestDistributeCaller creates a new read-only instance of TestDistribute, bound to a specific deployed contract. +func NewTestDistributeCaller(address common.Address, caller bind.ContractCaller) (*TestDistributeCaller, error) { + contract, err := bindTestDistribute(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &TestDistributeCaller{contract: contract}, nil +} + +// NewTestDistributeTransactor creates a new write-only instance of TestDistribute, bound to a specific deployed contract. +func NewTestDistributeTransactor(address common.Address, transactor bind.ContractTransactor) (*TestDistributeTransactor, error) { + contract, err := bindTestDistribute(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &TestDistributeTransactor{contract: contract}, nil +} + +// NewTestDistributeFilterer creates a new log filterer instance of TestDistribute, bound to a specific deployed contract. +func NewTestDistributeFilterer(address common.Address, filterer bind.ContractFilterer) (*TestDistributeFilterer, error) { + contract, err := bindTestDistribute(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &TestDistributeFilterer{contract: contract}, nil +} + +// bindTestDistribute binds a generic wrapper to an already deployed contract. +func bindTestDistribute(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := TestDistributeMetaData.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 (_TestDistribute *TestDistributeRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _TestDistribute.Contract.TestDistributeCaller.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 (_TestDistribute *TestDistributeRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _TestDistribute.Contract.TestDistributeTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_TestDistribute *TestDistributeRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _TestDistribute.Contract.TestDistributeTransactor.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 (_TestDistribute *TestDistributeCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _TestDistribute.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 (_TestDistribute *TestDistributeTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _TestDistribute.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_TestDistribute *TestDistributeTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _TestDistribute.Contract.contract.Transact(opts, method, params...) +} + +// DistributeThroughContract is a paid mutator transaction binding the contract method 0x50b54e84. +// +// Solidity: function distributeThroughContract(address zrc20, uint256 amount) returns(bool) +func (_TestDistribute *TestDistributeTransactor) DistributeThroughContract(opts *bind.TransactOpts, zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _TestDistribute.contract.Transact(opts, "distributeThroughContract", zrc20, amount) +} + +// DistributeThroughContract is a paid mutator transaction binding the contract method 0x50b54e84. +// +// Solidity: function distributeThroughContract(address zrc20, uint256 amount) returns(bool) +func (_TestDistribute *TestDistributeSession) DistributeThroughContract(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _TestDistribute.Contract.DistributeThroughContract(&_TestDistribute.TransactOpts, zrc20, amount) +} + +// DistributeThroughContract is a paid mutator transaction binding the contract method 0x50b54e84. +// +// Solidity: function distributeThroughContract(address zrc20, uint256 amount) returns(bool) +func (_TestDistribute *TestDistributeTransactorSession) DistributeThroughContract(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _TestDistribute.Contract.DistributeThroughContract(&_TestDistribute.TransactOpts, zrc20, amount) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_TestDistribute *TestDistributeTransactor) Fallback(opts *bind.TransactOpts, calldata []byte) (*types.Transaction, error) { + return _TestDistribute.contract.RawTransact(opts, calldata) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_TestDistribute *TestDistributeSession) Fallback(calldata []byte) (*types.Transaction, error) { + return _TestDistribute.Contract.Fallback(&_TestDistribute.TransactOpts, calldata) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_TestDistribute *TestDistributeTransactorSession) Fallback(calldata []byte) (*types.Transaction, error) { + return _TestDistribute.Contract.Fallback(&_TestDistribute.TransactOpts, calldata) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_TestDistribute *TestDistributeTransactor) Receive(opts *bind.TransactOpts) (*types.Transaction, error) { + return _TestDistribute.contract.RawTransact(opts, nil) // calldata is disallowed for receive function +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_TestDistribute *TestDistributeSession) Receive() (*types.Transaction, error) { + return _TestDistribute.Contract.Receive(&_TestDistribute.TransactOpts) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_TestDistribute *TestDistributeTransactorSession) Receive() (*types.Transaction, error) { + return _TestDistribute.Contract.Receive(&_TestDistribute.TransactOpts) +} + +// TestDistributeDistributedIterator is returned from FilterDistributed and is used to iterate over the raw logs and unpacked data for Distributed events raised by the TestDistribute contract. +type TestDistributeDistributedIterator struct { + Event *TestDistributeDistributed // 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 *TestDistributeDistributedIterator) 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(TestDistributeDistributed) + 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(TestDistributeDistributed) + 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 *TestDistributeDistributedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *TestDistributeDistributedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// TestDistributeDistributed represents a Distributed event raised by the TestDistribute contract. +type TestDistributeDistributed struct { + Zrc20Distributor common.Address + Zrc20Token common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDistributed is a free log retrieval operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_TestDistribute *TestDistributeFilterer) FilterDistributed(opts *bind.FilterOpts, zrc20_distributor []common.Address, zrc20_token []common.Address) (*TestDistributeDistributedIterator, error) { + + var zrc20_distributorRule []interface{} + for _, zrc20_distributorItem := range zrc20_distributor { + zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _TestDistribute.contract.FilterLogs(opts, "Distributed", zrc20_distributorRule, zrc20_tokenRule) + if err != nil { + return nil, err + } + return &TestDistributeDistributedIterator{contract: _TestDistribute.contract, event: "Distributed", logs: logs, sub: sub}, nil +} + +// WatchDistributed is a free log subscription operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_TestDistribute *TestDistributeFilterer) WatchDistributed(opts *bind.WatchOpts, sink chan<- *TestDistributeDistributed, zrc20_distributor []common.Address, zrc20_token []common.Address) (event.Subscription, error) { + + var zrc20_distributorRule []interface{} + for _, zrc20_distributorItem := range zrc20_distributor { + zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _TestDistribute.contract.WatchLogs(opts, "Distributed", zrc20_distributorRule, zrc20_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(TestDistributeDistributed) + if err := _TestDistribute.contract.UnpackLog(event, "Distributed", 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 +} + +// ParseDistributed is a log parse operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_TestDistribute *TestDistributeFilterer) ParseDistributed(log types.Log) (*TestDistributeDistributed, error) { + event := new(TestDistributeDistributed) + if err := _TestDistribute.contract.UnpackLog(event, "Distributed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/e2e/contracts/testdistribute/TestDistribute.json b/e2e/contracts/testdistribute/TestDistribute.json new file mode 100644 index 0000000000..05ee369e1c --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.json @@ -0,0 +1,67 @@ +{ + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_distributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Distributed", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "distributeThroughContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bin": "60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033" +} diff --git a/e2e/contracts/testdistribute/TestDistribute.sol b/e2e/contracts/testdistribute/TestDistribute.sol new file mode 100644 index 0000000000..5cf2277b88 --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +// @dev Interface to interact with distribute. +interface IDistribute { + function distribute( + address zrc20, + uint256 amount + ) external returns (bool success); +} + +// @dev Call IBank contract functions +contract TestDistribute { + event Distributed( + address indexed zrc20_distributor, + address indexed zrc20_token, + uint256 amount + ); + + IDistribute distr = IDistribute(0x0000000000000000000000000000000000000066); + + address immutable owner; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + function distributeThroughContract( + address zrc20, + uint256 amount + ) external onlyOwner returns (bool) { + return distr.distribute(zrc20, amount); + } + + fallback() external payable {} + + receive() external payable {} +} diff --git a/e2e/contracts/testdistribute/bindings.go b/e2e/contracts/testdistribute/bindings.go new file mode 100644 index 0000000000..765dfb5a8a --- /dev/null +++ b/e2e/contracts/testdistribute/bindings.go @@ -0,0 +1,8 @@ +//go:generate sh -c "solc TestDistribute.sol --combined-json abi,bin | jq '.contracts.\"TestDistribute.sol:TestDistribute\"' > TestDistribute.json" +//go:generate sh -c "cat TestDistribute.json | jq .abi > TestDistribute.abi" +//go:generate sh -c "cat TestDistribute.json | jq .bin | tr -d '\"' > TestDistribute.bin" +//go:generate sh -c "abigen --abi TestDistribute.abi --bin TestDistribute.bin --pkg testdistribute --type TestDistribute --out TestDistribute.go" + +package testdistribute + +var _ TestDistribute diff --git a/e2e/contracts/teststaking/TestStaking.sol b/e2e/contracts/teststaking/TestStaking.sol index 48a3837100..b6235ae658 100644 --- a/e2e/contracts/teststaking/TestStaking.sol +++ b/e2e/contracts/teststaking/TestStaking.sol @@ -36,13 +36,20 @@ interface IStaking { uint256 amount ) external returns (int64 completionTime); - function getAllValidators() external view returns (Validator[] calldata validators); + function getAllValidators() + external + view + returns (Validator[] calldata validators); - function getShares(address staker, string memory validator) external view returns (uint256 shares); + function getShares( + address staker, + string memory validator + ) external view returns (uint256 shares); } interface WZETA { function deposit() external payable; + function withdraw(uint256 wad) external; } @@ -94,18 +101,30 @@ contract TestStaking { wzeta.withdraw(wad); } - function stake(address staker, string memory validator, uint256 amount) external onlyOwner returns (bool) { + function stake( + address staker, + string memory validator, + uint256 amount + ) external onlyOwner returns (bool) { return staking.stake(staker, validator, amount); } - function stakeWithStateUpdate(address staker, string memory validator, uint256 amount) external onlyOwner returns (bool) { + function stakeWithStateUpdate( + address staker, + string memory validator, + uint256 amount + ) external onlyOwner returns (bool) { counter = counter + 1; bool success = staking.stake(staker, validator, amount); counter = counter + 1; return success; } - function stakeAndRevert(address staker, string memory validator, uint256 amount) external onlyOwner returns (bool) { + function stakeAndRevert( + address staker, + string memory validator, + uint256 amount + ) external onlyOwner returns (bool) { counter = counter + 1; staking.stake(staker, validator, amount); counter = counter + 1; @@ -129,15 +148,22 @@ contract TestStaking { return staking.moveStake(staker, validatorSrc, validatorDst, amount); } - function getShares(address staker, string memory validator) external view returns(uint256 shares) { + function getShares( + address staker, + string memory validator + ) external view returns (uint256 shares) { return staking.getShares(staker, validator); } - function getAllValidators() external view returns (Validator[] memory validators) { + function getAllValidators() + external + view + returns (Validator[] memory validators) + { return staking.getAllValidators(); } fallback() external payable {} receive() external payable {} -} \ No newline at end of file +} diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 98eac4397d..713ad935ff 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -172,13 +172,16 @@ const ( /* Stateful precompiled contracts tests */ - TestPrecompilesPrototypeName = "precompile_contracts_prototype" - TestPrecompilesPrototypeThroughContractName = "precompile_contracts_prototype_through_contract" - 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" + TestPrecompilesPrototypeName = "precompile_contracts_prototype" + TestPrecompilesPrototypeThroughContractName = "precompile_contracts_prototype_through_contract" + 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" + TestPrecompilesDistributeName = "precompile_contracts_distribute" + TestPrecompilesDistributeNonZRC20Name = "precompile_contracts_distribute_non_zrc20" + TestPrecompilesDistributeThroughContractName = "precompile_contracts_distribute_through_contract" ) // AllE2ETests is an ordered list of all e2e tests @@ -1041,4 +1044,22 @@ var AllE2ETests = []runner.E2ETest{ []runner.ArgDefinition{}, TestPrecompilesBankThroughContract, ), + runner.NewE2ETest( + TestPrecompilesDistributeName, + "test stateful precompiled contracts distribute", + []runner.ArgDefinition{}, + TestPrecompilesDistribute, + ), + runner.NewE2ETest( + TestPrecompilesDistributeNonZRC20Name, + "test stateful precompiled contracts distribute with non ZRC20 tokens", + []runner.ArgDefinition{}, + TestPrecompilesDistributeNonZRC20, + ), + runner.NewE2ETest( + TestPrecompilesDistributeThroughContractName, + "test stateful precompiled contracts distribute through contract", + []runner.ArgDefinition{}, + TestPrecompilesDistributeThroughContract, + ), } diff --git a/e2e/e2etests/test_precompiles_bank_through_contract.go b/e2e/e2etests/test_precompiles_bank_through_contract.go index 6d6384fd9e..4baa6fefb5 100644 --- a/e2e/e2etests/test_precompiles_bank_through_contract.go +++ b/e2e/e2etests/test_precompiles_bank_through_contract.go @@ -58,7 +58,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { }() // Check initial balances. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -67,7 +67,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Deposit ERC20ZRC20 without allowance should fail") // Check balances, should be the same. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -80,7 +80,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than allowed should fail") // Balances shouldn't change. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -93,7 +93,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than balance should fail") // Balances shouldn't change. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -102,7 +102,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequireTxSuccessful(r, receipt, "Depositting a correct amount should pass") // Balances should be transferred. Bank now locks 500 ZRC20 tokens. - balanceShouldBe(r, 500, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 500, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, bankAddress)) @@ -118,7 +118,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Withdrawing an amount higher than balance should fail") // Balances shouldn't change. - balanceShouldBe(r, 500, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 500, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, bankAddress)) @@ -127,7 +127,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequireTxSuccessful(r, receipt, "Withdraw correct amount should pass") // Balances should be reverted to initial state. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -156,7 +156,11 @@ func checkZRC20Balance(r *runner.E2ERunner, target common.Address) *big.Int { return bankZRC20Balance } -func checkCosmosBalance(r *runner.E2ERunner, bank *testbank.TestBank, zrc20, target common.Address) *big.Int { +func checkCosmosBalanceThroughBank( + r *runner.E2ERunner, + bank *testbank.TestBank, + zrc20, target common.Address, +) *big.Int { balance, err := bank.BalanceOf(&bind.CallOpts{Context: r.Ctx, From: r.ZEVMAuth.From}, zrc20, target) require.NoError(r, err) return balance diff --git a/e2e/e2etests/test_precompiles_distribute.go b/e2e/e2etests/test_precompiles_distribute.go new file mode 100644 index 0000000000..36870e090c --- /dev/null +++ b/e2e/e2etests/test_precompiles_distribute.go @@ -0,0 +1,239 @@ +package e2etests + +import ( + "math/big" + + "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "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" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/precompiles/bank" + "github.com/zeta-chain/node/precompiles/staking" + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func TestPrecompilesDistribute(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + var ( + spenderAddress = r.EVMAddress() + distributeContractAddress = staking.ContractAddress + lockerAddress = bank.ContractAddress + + zrc20Address = r.ERC20ZRC20Addr + zrc20Denom = precompiletypes.ZRC20ToCosmosDenom(zrc20Address) + + oneThousand = big.NewInt(1e3) + oneThousandOne = big.NewInt(1001) + fiveHundred = big.NewInt(500) + fiveHundredOne = big.NewInt(501) + + previousGasLimit = r.ZEVMAuth.GasLimit + ) + + // Set new gas limit to avoid out of gas errors. + r.ZEVMAuth.GasLimit = 10_000_000 + + // Set the test to reset the state after it finishes. + defer resetDistributionTest(r, lockerAddress, previousGasLimit, fiveHundred) + + // Get ERC20ZRC20. + txHash := r.DepositERC20WithAmountAndMessage(spenderAddress, oneThousand, []byte{}) + utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + dstrContract, err := staking.NewIStaking(distributeContractAddress, r.ZEVMClient) + require.NoError(r, err, "failed to create distribute contract caller") + + // DO NOT REMOVE - will be used in a subsequent PR when the ability to withdraw delegator rewards is introduced. + // Get validators through staking contract. + // validators, err := dstrContract.GetAllValidators(&bind.CallOpts{}) + // require.NoError(r, err) + + // Check initial balances. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + tx, err := dstrContract.Distribute(r.ZEVMAuth, zrc20Address, oneThousand) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail when there's no allowance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Allow 500. + approveAllowance(r, distributeContractAddress, fiveHundred) + + // Shouldn't be able to distribute more than allowed. + tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, fiveHundredOne) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than allowed") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Raise the allowance to 1000. + approveAllowance(r, distributeContractAddress, oneThousand) + + // Shouldn't be able to distribute more than owned balance. + tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, oneThousandOne) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than owned balance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Should be able to distribute 500, which is within balance and allowance. + tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, fiveHundred) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "distribute should succeed when distributing within balance and allowance") + + balanceShouldBe(r, 500, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 500, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + eventDitributed, err := dstrContract.ParseDistributed(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, zrc20Address, eventDitributed.Zrc20Token) + require.Equal(r, spenderAddress, eventDitributed.Zrc20Distributor) + require.Equal(r, fiveHundred.Uint64(), eventDitributed.Amount.Uint64()) + + // After one block the rewards should have been distributed and fee collector should have 0 ZRC20 balance. + r.WaitForBlocks(1) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // DO NOT REMOVE THE FOLLOWING CODE + // This section is commented until a following PR introduces the ability to withdraw delegator rewards. + // This validator checks will be used then to complete the whole e2e. + + // res, err := r.DistributionClient.ValidatorDistributionInfo( + // r.Ctx, + // &distributiontypes.QueryValidatorDistributionInfoRequest{ + // ValidatorAddress: validators[0].OperatorAddress, + // }, + // ) + // require.NoError(r, err) + // fmt.Printf("Validator 0 distribution info: %+v\n", res) + + // res2, err := r.DistributionClient.ValidatorOutstandingRewards(r.Ctx, &distributiontypes.QueryValidatorOutstandingRewardsRequest{ + // ValidatorAddress: validators[0].OperatorAddress, + // }) + // require.NoError(r, err) + // fmt.Printf("Validator 0 outstanding rewards: %+v\n", res2) + + // res3, err := r.DistributionClient.ValidatorCommission(r.Ctx, &distributiontypes.QueryValidatorCommissionRequest{ + // ValidatorAddress: validators[0].OperatorAddress, + // }) + // require.NoError(r, err) + // fmt.Printf("Validator 0 commission: %+v\n", res3) + + // // Validator 1 + // res, err = r.DistributionClient.ValidatorDistributionInfo( + // r.Ctx, + // &distributiontypes.QueryValidatorDistributionInfoRequest{ + // ValidatorAddress: validators[1].OperatorAddress, + // }, + // ) + // require.NoError(r, err) + // fmt.Printf("Validator 1 distribution info: %+v\n", res) + + // res2, err = r.DistributionClient.ValidatorOutstandingRewards(r.Ctx, &distributiontypes.QueryValidatorOutstandingRewardsRequest{ + // ValidatorAddress: validators[1].OperatorAddress, + // }) + // require.NoError(r, err) + // fmt.Printf("Validator 1 outstanding rewards: %+v\n", res2) + + // res3, err = r.DistributionClient.ValidatorCommission(r.Ctx, &distributiontypes.QueryValidatorCommissionRequest{ + // ValidatorAddress: validators[1].OperatorAddress, + // }) + // require.NoError(r, err) + // fmt.Printf("Validator 1 commission: %+v\n", res3) +} + +func TestPrecompilesDistributeNonZRC20(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, dstrAddress := r.EVMAddress(), staking.ContractAddress + + // Create a staking contract caller. + dstrContract, err := staking.NewIStaking(dstrAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create staking contract caller") + + // Deposit and approve 50 WZETA for the test. + approveAmount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(50)) + r.DepositAndApproveWZeta(approveAmount) + + // Allow the staking contract to spend 25 WZeta tokens. + tx, err := r.WZeta.Approve(r.ZEVMAuth, dstrAddress, big.NewInt(25)) + require.NoError(r, err, "Error approving allowance for staking contract") + 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 staking in WZeta tokens. Should be 25. + allowance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, dstrAddress) + require.NoError(r, err, "Error retrieving staking allowance") + require.EqualValues(r, uint64(25), allowance.Uint64(), "Error allowance for staking contract") + + // Call Distribute with 25 Non ZRC20 tokens. Should fail. + tx, err = dstrContract.Distribute(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) + require.NoError(r, err, "Error calling staking.distribute()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.Equal(r, uint64(0), receipt.Status, "Non ZRC20 deposit should fail") +} + +// checkCosmosBalance checks the cosmos coin balance for an address. The coin is specified by its denom. +func checkCosmosBalance(r *runner.E2ERunner, address types.AccAddress, denom string) *big.Int { + bal, err := r.BankClient.Balance( + r.Ctx, + &banktypes.QueryBalanceRequest{Address: address.String(), Denom: denom}, + ) + require.NoError(r, err) + + return bal.Balance.Amount.BigInt() +} + +func resetDistributionTest( + r *runner.E2ERunner, + lockerAddress common.Address, + previousGasLimit uint64, + amount *big.Int, +) { + 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, lockerAddress, 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") + + // Reset balance to 0 for spender; this is needed when running upgrade tests where this test runs twice. + tx, err = r.ERC20ZRC20.Transfer( + r.ZEVMAuth, + common.HexToAddress("0x000000000000000000000000000000000000dEaD"), + amount, + ) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Resetting balance failed") +} diff --git a/e2e/e2etests/test_precompiles_distribute_through_contract.go b/e2e/e2etests/test_precompiles_distribute_through_contract.go new file mode 100644 index 0000000000..444d8e6a59 --- /dev/null +++ b/e2e/e2etests/test_precompiles_distribute_through_contract.go @@ -0,0 +1,120 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/contracts/testdistribute" + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/precompiles/bank" + "github.com/zeta-chain/node/precompiles/staking" + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func TestPrecompilesDistributeThroughContract(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + var ( + spenderAddress = r.EVMAddress() + distributeContractAddress = staking.ContractAddress + lockerAddress = bank.ContractAddress + + zrc20Address = r.ERC20ZRC20Addr + zrc20Denom = precompiletypes.ZRC20ToCosmosDenom(zrc20Address) + + oneThousand = big.NewInt(1e3) + oneThousandOne = big.NewInt(1001) + fiveHundred = big.NewInt(500) + fiveHundredOne = big.NewInt(501) + + previousGasLimit = r.ZEVMAuth.GasLimit + ) + + // Get ERC20ZRC20. + txHash := r.DepositERC20WithAmountAndMessage(spenderAddress, oneThousand, []byte{}) + utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + dstrContract, err := staking.NewIStaking(distributeContractAddress, r.ZEVMClient) + require.NoError(r, err, "failed to create distribute contract caller") + + _, tx, testDstrContract, err := testdistribute.DeployTestDistribute(r.ZEVMAuth, r.ZEVMClient) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "deployment of disitributor caller contract failed") + + // Set new gas limit to avoid out of gas errors. + r.ZEVMAuth.GasLimit = 10_000_000 + + // Set the test to reset the state after it finishes. + defer resetDistributionTest(r, lockerAddress, previousGasLimit, fiveHundred) + + // Check initial balances. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + receipt = distributeThroughContract(r, testDstrContract, zrc20Address, oneThousand) + utils.RequiredTxFailed(r, receipt, "distribute should fail when there's no allowance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Allow 500. + approveAllowance(r, distributeContractAddress, fiveHundred) + + receipt = distributeThroughContract(r, testDstrContract, zrc20Address, fiveHundredOne) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than allowed") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Raise the allowance to 1000. + approveAllowance(r, distributeContractAddress, oneThousand) + + // Shouldn't be able to distribute more than owned balance. + receipt = distributeThroughContract(r, testDstrContract, zrc20Address, oneThousandOne) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than owned balance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Should be able to distribute 500, which is within balance and allowance. + receipt = distributeThroughContract(r, testDstrContract, zrc20Address, fiveHundred) + utils.RequireTxSuccessful(r, receipt, "distribute should succeed when distributing within balance and allowance") + + balanceShouldBe(r, 500, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 1000, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 500, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + eventDitributed, err := dstrContract.ParseDistributed(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, zrc20Address, eventDitributed.Zrc20Token) + require.Equal(r, spenderAddress, eventDitributed.Zrc20Distributor) + require.Equal(r, fiveHundred.Uint64(), eventDitributed.Amount.Uint64()) + + // After one block the rewards should have been distributed and fee collector should have 0 ZRC20 balance. + r.WaitForBlocks(1) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) +} + +func distributeThroughContract( + r *runner.E2ERunner, + dstr *testdistribute.TestDistribute, + zrc20Address common.Address, + amount *big.Int, +) *types.Receipt { + tx, err := dstr.DistributeThroughContract(r.ZEVMAuth, zrc20Address, amount) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + return receipt +} diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 03cafb6fc4..cf7a8e6f02 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -9,8 +9,10 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/rpcclient" + "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" @@ -74,6 +76,7 @@ type E2ERunner struct { SolanaDeployerAddress solana.PublicKey TONDeployer *tonrunner.Deployer TONGateway *toncontracts.Gateway + FeeCollectorAddress types.AccAddress // all clients. // a reference to this type is required to enable creating a new E2ERunner. @@ -86,14 +89,15 @@ type E2ERunner struct { SolanaClient *rpc.Client // zetacored grpc clients - AuthorityClient authoritytypes.QueryClient - CctxClient crosschaintypes.QueryClient - FungibleClient fungibletypes.QueryClient - AuthClient authtypes.QueryClient - BankClient banktypes.QueryClient - StakingClient stakingtypes.QueryClient - ObserverClient observertypes.QueryClient - LightclientClient lightclienttypes.QueryClient + AuthorityClient authoritytypes.QueryClient + CctxClient crosschaintypes.QueryClient + FungibleClient fungibletypes.QueryClient + AuthClient authtypes.QueryClient + BankClient banktypes.QueryClient + StakingClient stakingtypes.QueryClient + ObserverClient observertypes.QueryClient + LightclientClient lightclienttypes.QueryClient + DistributionClient distributiontypes.QueryClient // optional zeta (cosmos) client // typically only in test runners that need it @@ -187,18 +191,21 @@ func NewE2ERunner( Account: account, + FeeCollectorAddress: authtypes.NewModuleAddress(authtypes.FeeCollectorName), + Clients: clients, - ZEVMClient: clients.Zevm, - EVMClient: clients.Evm, - AuthorityClient: clients.Zetacore.Authority, - CctxClient: clients.Zetacore.Crosschain, - FungibleClient: clients.Zetacore.Fungible, - AuthClient: clients.Zetacore.Auth, - BankClient: clients.Zetacore.Bank, - StakingClient: clients.Zetacore.Staking, - ObserverClient: clients.Zetacore.Observer, - LightclientClient: clients.Zetacore.Lightclient, + ZEVMClient: clients.Zevm, + EVMClient: clients.Evm, + AuthorityClient: clients.Zetacore.Authority, + CctxClient: clients.Zetacore.Crosschain, + FungibleClient: clients.Zetacore.Fungible, + AuthClient: clients.Zetacore.Auth, + BankClient: clients.Zetacore.Bank, + StakingClient: clients.Zetacore.Staking, + ObserverClient: clients.Zetacore.Observer, + LightclientClient: clients.Zetacore.Lightclient, + DistributionClient: clients.Zetacore.Distribution, EVMAuth: clients.EvmAuth, ZEVMAuth: clients.ZevmAuth, diff --git a/e2e/utils/require.go b/e2e/utils/require.go index 3dedb7dd26..8bf9c5f5d0 100644 --- a/e2e/utils/require.go +++ b/e2e/utils/require.go @@ -38,7 +38,7 @@ func RequireTxSuccessful(t require.TestingT, receipt *ethtypes.Receipt, msgAndAr // RequiredTxFailed checks if the receipt status is failed. // Currently, it accepts eth receipt, but we can make this more generic by using type assertion. func RequiredTxFailed(t require.TestingT, receipt *ethtypes.Receipt, msgAndArgs ...any) { - msg := "receipt status is not successful: %s" + msg := "receipt status is not failed: %s" require.Equal( t, ethtypes.ReceiptStatusFailed, diff --git a/pkg/rpc/clients.go b/pkg/rpc/clients.go index 26ecbe729f..1dc0d7314e 100644 --- a/pkg/rpc/clients.go +++ b/pkg/rpc/clients.go @@ -8,6 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" feemarkettypes "github.com/zeta-chain/ethermint/x/feemarket/types" @@ -35,6 +36,8 @@ type Clients struct { Staking stakingtypes.QueryClient // Upgrade is a github.com/cosmos/cosmos-sdk/x/upgrade/types QueryClient Upgrade upgradetypes.QueryClient + // Distribution is a "github.com/cosmos/cosmos-sdk/x/distribution/types" QueryClient + Distribution distributiontypes.QueryClient // ZetaCore specific clients @@ -65,11 +68,12 @@ type Clients struct { func newClients(ctx client.Context) (Clients, error) { return Clients{ // Cosmos SDK clients - Auth: authtypes.NewQueryClient(ctx), - Bank: banktypes.NewQueryClient(ctx), - Staking: stakingtypes.NewQueryClient(ctx), - Upgrade: upgradetypes.NewQueryClient(ctx), - Authority: authoritytypes.NewQueryClient(ctx), + Auth: authtypes.NewQueryClient(ctx), + Bank: banktypes.NewQueryClient(ctx), + Staking: stakingtypes.NewQueryClient(ctx), + Upgrade: upgradetypes.NewQueryClient(ctx), + Authority: authoritytypes.NewQueryClient(ctx), + Distribution: distributiontypes.NewQueryClient(ctx), // ZetaCore specific clients Crosschain: crosschaintypes.NewQueryClient(ctx), Fungible: fungibletypes.NewQueryClient(ctx), diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 51a559edd3..b92b4a8217 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -11,7 +11,7 @@ import ( "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" + precompiletypes "github.com/zeta-chain/node/precompiles/types" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) @@ -49,7 +49,7 @@ func initABI() { } type Contract struct { - ptypes.BaseContract + precompiletypes.BaseContract bankKeeper bank.Keeper fungibleKeeper fungiblekeeper.Keeper @@ -74,11 +74,12 @@ func NewIBankContract( // This avoids instantiating it every time deposit or withdraw are called. zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() if err != nil { + ctx.Logger().Error("bank contract failed to get ZRC20 ABI", "error", err) return nil } return &Contract{ - BaseContract: ptypes.NewBaseContract(ContractAddress), + BaseContract: precompiletypes.NewBaseContract(ContractAddress), bankKeeper: bankKeeper, fungibleKeeper: fungibleKeeper, zrc20ABI: zrc20ABI, @@ -130,13 +131,13 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return nil, err } - stateDB := evm.StateDB.(ptypes.ExtStateDB) + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) switch method.Name { // Deposit and Withdraw methods are both not allowed in read-only mode. case DepositMethodName, WithdrawMethodName: if readOnly { - return nil, ptypes.ErrWriteMethod{ + return nil, precompiletypes.ErrWriteMethod{ Method: method.Name, } } @@ -175,42 +176,8 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, nil default: - return nil, ptypes.ErrInvalidMethod{ + return nil, precompiletypes.ErrInvalidMethod{ Method: method.Name, } } } - -// 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/bank_test.go b/precompiles/bank/bank_test.go index 518c330a7f..a42bdeb88a 100644 --- a/precompiles/bank/bank_test.go +++ b/precompiles/bank/bank_test.go @@ -2,16 +2,12 @@ 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) { @@ -128,29 +124,3 @@ func Test_InvalidABI(t *testing.T) { 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/logs.go b/precompiles/bank/logs.go index 36b877dfa5..9043cc63f9 100644 --- a/precompiles/bank/logs.go +++ b/precompiles/bank/logs.go @@ -28,8 +28,8 @@ func (c *Contract) addEventLog( topics, err := logs.MakeTopics( event, - []interface{}{common.BytesToAddress(eventData.zrc20Addr.Bytes())}, - []interface{}{common.BytesToAddress(eventData.zrc20Token.Bytes())}, + []interface{}{eventData.zrc20Addr}, + []interface{}{eventData.zrc20Token}, []interface{}{eventData.cosmosCoin}, ) if err != nil { diff --git a/precompiles/bank/method_balance_of.go b/precompiles/bank/method_balance_of.go index e4bc644a2b..85765d2ab8 100644 --- a/precompiles/bank/method_balance_of.go +++ b/precompiles/bank/method_balance_of.go @@ -5,7 +5,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" ) // balanceOf returns the balance of cosmos coins minted by the bank's deposit function, @@ -19,7 +19,7 @@ func (c *Contract) balanceOf( args []interface{}, ) (result []byte, err error) { if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 2, }) @@ -32,7 +32,7 @@ func (c *Contract) balanceOf( } // Get the counterpart cosmos address. - toAddr, err := getCosmosAddress(c.bankKeeper, addr) + toAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, addr) if err != nil { return nil, err } @@ -41,7 +41,7 @@ func (c *Contract) balanceOf( // 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{ + return nil, &precompiletypes.ErrInvalidToken{ Got: zrc20Addr.String(), Reason: "token is not a whitelisted ZRC20", } @@ -49,9 +49,9 @@ func (c *Contract) balanceOf( // 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)) + coin := c.bankKeeper.GetBalance(ctx, toAddr, precompiletypes.ZRC20ToCosmosDenom(zrc20Addr)) if !coin.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ + return nil, &precompiletypes.ErrInvalidCoin{ Got: coin.GetDenom(), Negative: coin.IsNegative(), Nil: coin.IsNil(), @@ -64,14 +64,14 @@ func (c *Contract) balanceOf( 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{ + return common.Address{}, common.Address{}, &precompiletypes.ErrInvalidAddr{ Got: zrc20Addr.String(), } } addr, ok = args[1].(common.Address) if !ok { - return common.Address{}, common.Address{}, &ptypes.ErrInvalidAddr{ + return common.Address{}, common.Address{}, &precompiletypes.ErrInvalidAddr{ Got: addr.String(), } } diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 1d5792d8a3..76c21d926a 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -8,7 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" "github.com/zeta-chain/node/x/fungible/types" ) @@ -34,7 +34,7 @@ func (c *Contract) deposit( // This function is developed using the Check - Effects - Interactions pattern: // 1. Check everything is correct. if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 2, }) @@ -48,13 +48,13 @@ func (c *Contract) deposit( } // Get the correct caller address. - caller, err := getEVMCallerAddress(evm, contract) + caller, err := precompiletypes.GetEVMCallerAddress(evm, contract) if err != nil { return nil, err } // Get the cosmos address of the caller. - toAddr, err := getCosmosAddress(c.bankKeeper, caller) + toAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, caller) if err != nil { return nil, err } @@ -70,7 +70,7 @@ func (c *Contract) deposit( []interface{}{caller}, ) if err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "balanceOf", Got: err.Error(), } @@ -78,13 +78,13 @@ func (c *Contract) deposit( balance, ok := resBalanceOf[0].(*big.Int) if !ok { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ Got: "ZRC20 balanceOf returned an unexpected type", } } if balance.Cmp(amount) < 0 || balance.Cmp(big.NewInt(0)) <= 0 { - return nil, &ptypes.ErrInvalidAmount{ + return nil, &precompiletypes.ErrInvalidAmount{ Got: balance.String(), } } @@ -94,14 +94,14 @@ func (c *Contract) deposit( // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". // - Mint coins to the fungible module. // - Send coins from fungible to the caller. - coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) + coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) if err != nil { return nil, err } // 2. Effect: subtract balance. if err := c.fungibleKeeper.LockZRC20(ctx, c.zrc20ABI, zrc20Addr, c.Address(), caller, c.Address(), amount); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "LockZRC20InBank", Got: err.Error(), } @@ -109,7 +109,7 @@ func (c *Contract) deposit( // 3. Interactions: create cosmos coin and send. if err := c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "MintCoins", Got: err.Error(), } @@ -117,14 +117,14 @@ func (c *Contract) deposit( err = c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet) if err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "SendCoinsFromModuleToAccount", Got: err.Error(), } } if err := c.addEventLog(ctx, evm.StateDB, DepositEventName, eventData{caller, zrc20Addr, toAddr.String(), coinSet.Denoms()[0], amount}); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "AddDepositLog", Got: err.Error(), } @@ -136,14 +136,14 @@ func (c *Contract) deposit( 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{ + return common.Address{}, nil, &precompiletypes.ErrInvalidAddr{ Got: zrc20Addr.String(), } } amount, ok = args[1].(*big.Int) if !ok || amount == nil || amount.Sign() <= 0 { - return common.Address{}, nil, &ptypes.ErrInvalidAmount{ + return common.Address{}, nil, &precompiletypes.ErrInvalidAmount{ Got: amount.String(), } } diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index e416187490..38fc715af6 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -16,7 +16,7 @@ import ( "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" + precompiletypes "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" @@ -46,7 +46,7 @@ func Test_Methods(t *testing.T) { success, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, true) require.ErrorIs( t, - ptypes.ErrWriteMethod{ + precompiletypes.ErrWriteMethod{ Method: "deposit", }, err) @@ -73,7 +73,7 @@ func Test_Methods(t *testing.T) { success, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, true) require.ErrorIs( t, - ptypes.ErrWriteMethod{ + precompiletypes.ErrWriteMethod{ Method: "withdraw", }, err) @@ -101,7 +101,7 @@ func Test_Methods(t *testing.T) { require.Error(t, err) require.ErrorAs( t, - ptypes.ErrInvalidAmount{ + precompiletypes.ErrInvalidAmount{ Got: "0", }, err, @@ -248,7 +248,7 @@ func Test_Methods(t *testing.T) { require.Error(t, err) require.ErrorAs( t, - ptypes.ErrInvalidAmount{ + precompiletypes.ErrInvalidAmount{ Got: "1000", }, err, @@ -459,7 +459,7 @@ func Test_Methods(t *testing.T) { require.Error(t, err) require.ErrorAs( t, - ptypes.ErrInsufficientBalance{ + precompiletypes.ErrInsufficientBalance{ Requested: "501", Got: "500", }, diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index e071ed04cd..a1308146c1 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -9,7 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" "github.com/zeta-chain/node/x/fungible/types" ) @@ -29,7 +29,7 @@ func (c *Contract) withdraw( ) (result []byte, err error) { // 1. Check everything is correct. if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 2, }) @@ -43,50 +43,50 @@ func (c *Contract) withdraw( } // Get the correct caller address. - caller, err := getEVMCallerAddress(evm, contract) + caller, err := precompiletypes.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) + fromAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, caller) if err != nil { return nil, err } // Safety check: token has to be a non-paused whitelisted ZRC20. if err := c.fungibleKeeper.IsValidZRC20(ctx, zrc20Addr); err != nil { - return nil, &ptypes.ErrInvalidToken{ + return nil, &precompiletypes.ErrInvalidToken{ Got: zrc20Addr.String(), Reason: err.Error(), } } // Caller has to have enough cosmos coin balance to withdraw the requested amount. - coin := c.bankKeeper.GetBalance(ctx, fromAddr, ZRC20ToCosmosDenom(zrc20Addr)) + coin := c.bankKeeper.GetBalance(ctx, fromAddr, precompiletypes.ZRC20ToCosmosDenom(zrc20Addr)) if !coin.IsValid() { - return nil, &ptypes.ErrInsufficientBalance{ + return nil, &precompiletypes.ErrInsufficientBalance{ Requested: amount.String(), Got: "invalid coin", } } if coin.Amount.LT(math.NewIntFromBigInt(amount)) { - return nil, &ptypes.ErrInsufficientBalance{ + return nil, &precompiletypes.ErrInsufficientBalance{ Requested: amount.String(), Got: coin.Amount.String(), } } - coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) + coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) if err != nil { return nil, err } // Check if bank address has enough ZRC20 balance. if err := c.fungibleKeeper.CheckZRC20Balance(ctx, c.zrc20ABI, zrc20Addr, c.Address(), amount); err != nil { - return nil, &ptypes.ErrInsufficientBalance{ + return nil, &precompiletypes.ErrInsufficientBalance{ Requested: amount.String(), Got: err.Error(), } @@ -94,14 +94,14 @@ func (c *Contract) withdraw( // 2. Effect: burn cosmos coin balance. if err := c.bankKeeper.SendCoinsFromAccountToModule(ctx, fromAddr, types.ModuleName, coinSet); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "SendCoinsFromAccountToModule", Got: err.Error(), } } if err := c.bankKeeper.BurnCoins(ctx, types.ModuleName, coinSet); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "BurnCoins", Got: err.Error(), } @@ -109,14 +109,14 @@ func (c *Contract) withdraw( // 3. Interactions: send ZRC20. if err := c.fungibleKeeper.UnlockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, c.Address(), amount); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "UnlockZRC20InBank", Got: err.Error(), } } if err := c.addEventLog(ctx, evm.StateDB, WithdrawEventName, eventData{caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount}); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "AddWithdrawLog", Got: err.Error(), } @@ -128,14 +128,14 @@ func (c *Contract) withdraw( 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{ + return common.Address{}, nil, &precompiletypes.ErrInvalidAddr{ Got: zrc20Addr.String(), } } amount, ok = args[1].(*big.Int) if !ok || amount == nil || amount.Sign() <= 0 { - return common.Address{}, nil, &ptypes.ErrInvalidAmount{ + return common.Address{}, nil, &precompiletypes.ErrInvalidAmount{ Got: amount.String(), } } diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index cdd5e2ff74..b9d167dbac 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -49,8 +49,8 @@ func StatefulContracts( // Define the staking contract function. if EnabledStatefulContracts[staking.ContractAddress] { - stakingContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { - return staking.NewIStakingContract(stakingKeeper, cdc, gasConfig) + stakingContract := func(ctx sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { + return staking.NewIStakingContract(ctx, stakingKeeper, *fungibleKeeper, bankKeeper, cdc, gasConfig) } // Append the staking contract to the precompiledContracts slice. diff --git a/precompiles/prototype/prototype.go b/precompiles/prototype/prototype.go index 123885ccb4..31f1bcaaec 100644 --- a/precompiles/prototype/prototype.go +++ b/precompiles/prototype/prototype.go @@ -11,7 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) @@ -55,7 +55,7 @@ func initABI() { } type Contract struct { - ptypes.BaseContract + precompiletypes.BaseContract fungibleKeeper fungiblekeeper.Keeper cdc codec.Codec @@ -68,7 +68,7 @@ func NewIPrototypeContract( kvGasConfig storetypes.GasConfig, ) *Contract { return &Contract{ - BaseContract: ptypes.NewBaseContract(ContractAddress), + BaseContract: precompiletypes.NewBaseContract(ContractAddress), fungibleKeeper: *fungibleKeeper, cdc: cdc, kvGasConfig: kvGasConfig, @@ -106,7 +106,7 @@ func (c *Contract) RequiredGas(input []byte) uint64 { // Bech32ToHexAddr converts a bech32 address to a hex address. func (c *Contract) Bech32ToHexAddr(method *abi.Method, args []interface{}) ([]byte, error) { if len(args) != 1 { - return nil, &ptypes.ErrInvalidNumberOfArgs{ + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 1, } @@ -144,7 +144,7 @@ func (c *Contract) Bech32ToHexAddr(method *abi.Method, args []interface{}) ([]by // Bech32ify converts a hex address to a bech32 address. func (c *Contract) Bech32ify(method *abi.Method, args []interface{}) ([]byte, error) { if len(args) != 2 { - return nil, &ptypes.ErrInvalidNumberOfArgs{ + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 2, } @@ -198,7 +198,7 @@ func (c *Contract) GetGasStabilityPoolBalance( args []interface{}, ) ([]byte, error) { if len(args) != 1 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 1, }) @@ -207,7 +207,7 @@ func (c *Contract) GetGasStabilityPoolBalance( // Unwrap arguments. The chainID is the first and unique argument. chainID, ok := args[0].(int64) if !ok { - return nil, ptypes.ErrInvalidArgument{ + return nil, precompiletypes.ErrInvalidArgument{ Got: chainID, } } @@ -233,7 +233,7 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, _ bool) ([]byte, erro return nil, err } - stateDB := evm.StateDB.(ptypes.ExtStateDB) + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) switch method.Name { case GetGasStabilityPoolBalanceName: @@ -252,7 +252,7 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, _ bool) ([]byte, erro case Bech32ifyMethodName: return c.Bech32ify(method, args) default: - return nil, ptypes.ErrInvalidMethod{ + return nil, precompiletypes.ErrInvalidMethod{ Method: method.Name, } } diff --git a/precompiles/staking/IStaking.abi b/precompiles/staking/IStaking.abi index fceba4d682..da1a9e6ffc 100644 --- a/precompiles/staking/IStaking.abi +++ b/precompiles/staking/IStaking.abi @@ -1,4 +1,29 @@ [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_distributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Distributed", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -80,6 +105,30 @@ "name": "Unstake", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "distribute", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "getAllValidators", diff --git a/precompiles/staking/IStaking.gen.go b/precompiles/staking/IStaking.gen.go index 01226d4ecf..d4f7495d37 100644 --- a/precompiles/staking/IStaking.gen.go +++ b/precompiles/staking/IStaking.gen.go @@ -39,7 +39,7 @@ type Validator struct { // IStakingMetaData contains all meta data concerning the IStaking contract. var IStakingMetaData = &bind.MetaData{ - ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorSrc\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorDst\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"MoveStake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Stake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Unstake\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"getAllValidators\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"operatorAddress\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"consensusPubKey\",\"type\":\"string\"},{\"internalType\":\"bool\",\"name\":\"jailed\",\"type\":\"bool\"},{\"internalType\":\"enumBondStatus\",\"name\":\"bondStatus\",\"type\":\"uint8\"}],\"internalType\":\"structValidator[]\",\"name\":\"validators\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validatorSrc\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"validatorDst\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"moveStake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"stake\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"unstake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_distributor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Distributed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorSrc\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorDst\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"MoveStake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Stake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Unstake\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"distribute\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getAllValidators\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"operatorAddress\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"consensusPubKey\",\"type\":\"string\"},{\"internalType\":\"bool\",\"name\":\"jailed\",\"type\":\"bool\"},{\"internalType\":\"enumBondStatus\",\"name\":\"bondStatus\",\"type\":\"uint8\"}],\"internalType\":\"structValidator[]\",\"name\":\"validators\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validatorSrc\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"validatorDst\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"moveStake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"stake\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"unstake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", } // IStakingABI is the input ABI used to generate the binding from. @@ -250,6 +250,27 @@ func (_IStaking *IStakingCallerSession) GetShares(staker common.Address, validat return _IStaking.Contract.GetShares(&_IStaking.CallOpts, staker, validator) } +// Distribute is a paid mutator transaction binding the contract method 0xfb932108. +// +// Solidity: function distribute(address zrc20, uint256 amount) returns(bool success) +func (_IStaking *IStakingTransactor) Distribute(opts *bind.TransactOpts, zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IStaking.contract.Transact(opts, "distribute", zrc20, amount) +} + +// Distribute is a paid mutator transaction binding the contract method 0xfb932108. +// +// Solidity: function distribute(address zrc20, uint256 amount) returns(bool success) +func (_IStaking *IStakingSession) Distribute(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.Distribute(&_IStaking.TransactOpts, zrc20, amount) +} + +// Distribute is a paid mutator transaction binding the contract method 0xfb932108. +// +// Solidity: function distribute(address zrc20, uint256 amount) returns(bool success) +func (_IStaking *IStakingTransactorSession) Distribute(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.Distribute(&_IStaking.TransactOpts, zrc20, amount) +} + // MoveStake is a paid mutator transaction binding the contract method 0xd11a93d0. // // Solidity: function moveStake(address staker, string validatorSrc, string validatorDst, uint256 amount) returns(int64 completionTime) @@ -313,6 +334,160 @@ func (_IStaking *IStakingTransactorSession) Unstake(staker common.Address, valid return _IStaking.Contract.Unstake(&_IStaking.TransactOpts, staker, validator, amount) } +// IStakingDistributedIterator is returned from FilterDistributed and is used to iterate over the raw logs and unpacked data for Distributed events raised by the IStaking contract. +type IStakingDistributedIterator struct { + Event *IStakingDistributed // 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 *IStakingDistributedIterator) 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(IStakingDistributed) + 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(IStakingDistributed) + 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 *IStakingDistributedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IStakingDistributedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IStakingDistributed represents a Distributed event raised by the IStaking contract. +type IStakingDistributed struct { + Zrc20Distributor common.Address + Zrc20Token common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDistributed is a free log retrieval operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) FilterDistributed(opts *bind.FilterOpts, zrc20_distributor []common.Address, zrc20_token []common.Address) (*IStakingDistributedIterator, error) { + + var zrc20_distributorRule []interface{} + for _, zrc20_distributorItem := range zrc20_distributor { + zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _IStaking.contract.FilterLogs(opts, "Distributed", zrc20_distributorRule, zrc20_tokenRule) + if err != nil { + return nil, err + } + return &IStakingDistributedIterator{contract: _IStaking.contract, event: "Distributed", logs: logs, sub: sub}, nil +} + +// WatchDistributed is a free log subscription operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) WatchDistributed(opts *bind.WatchOpts, sink chan<- *IStakingDistributed, zrc20_distributor []common.Address, zrc20_token []common.Address) (event.Subscription, error) { + + var zrc20_distributorRule []interface{} + for _, zrc20_distributorItem := range zrc20_distributor { + zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _IStaking.contract.WatchLogs(opts, "Distributed", zrc20_distributorRule, zrc20_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(IStakingDistributed) + if err := _IStaking.contract.UnpackLog(event, "Distributed", 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 +} + +// ParseDistributed is a log parse operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) ParseDistributed(log types.Log) (*IStakingDistributed, error) { + event := new(IStakingDistributed) + if err := _IStaking.contract.UnpackLog(event, "Distributed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // IStakingMoveStakeIterator is returned from FilterMoveStake and is used to iterate over the raw logs and unpacked data for MoveStake events raised by the IStaking contract. type IStakingMoveStakeIterator struct { Event *IStakingMoveStake // Event containing the contract specifics and raw log diff --git a/precompiles/staking/IStaking.json b/precompiles/staking/IStaking.json index cff6a7a365..d4e0bb75f0 100644 --- a/precompiles/staking/IStaking.json +++ b/precompiles/staking/IStaking.json @@ -1,5 +1,30 @@ { "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_distributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Distributed", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -81,6 +106,30 @@ "name": "Unstake", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "distribute", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "getAllValidators", diff --git a/precompiles/staking/IStaking.sol b/precompiles/staking/IStaking.sol index c6b2642a4c..dece711d71 100644 --- a/precompiles/staking/IStaking.sol +++ b/precompiles/staking/IStaking.sol @@ -5,9 +5,7 @@ pragma solidity ^0.8.26; address constant ISTAKING_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000066; // 102 /// @dev The IStaking contract's instance. -IStaking constant ISTAKING_CONTRACT = IStaking( - ISTAKING_PRECOMPILE_ADDRESS -); +IStaking constant ISTAKING_CONTRACT = IStaking(ISTAKING_PRECOMPILE_ADDRESS); /// @notice Bond status for validator enum BondStatus { @@ -58,6 +56,16 @@ interface IStaking { uint256 amount ); + /// @notice Distributed event is emitted when distribute function is called successfully. + /// @param zrc20_distributor Distributor address. + /// @param zrc20_token ZRC20 token address. + /// @param amount Distributed amount. + event Distributed( + address indexed zrc20_distributor, + address indexed zrc20_token, + uint256 amount + ); + /// @notice Stake coins to validator /// @param staker Staker address /// @param validator Validator address @@ -95,9 +103,24 @@ interface IStaking { /// @notice Get all validators /// @return validators All validators - function getAllValidators() external view returns (Validator[] calldata validators); + function getAllValidators() + external + view + returns (Validator[] calldata validators); /// @notice Get shares for staker in validator /// @return shares Staker shares in validator - function getShares(address staker, string memory validator) external view returns (uint256 shares); + function getShares( + address staker, + string memory validator + ) external view returns (uint256 shares); + + /// @notice Distribute a ZRC20 token as staking rewards. + /// @param zrc20 The ZRC20 token address to be distributed. + /// @param amount The amount of ZRC20 tokens to distribute. + /// @return success Boolean indicating whether the distribution was successful. + function distribute( + address zrc20, + uint256 amount + ) external returns (bool success); } diff --git a/precompiles/staking/const.go b/precompiles/staking/const.go new file mode 100644 index 0000000000..8500e723f4 --- /dev/null +++ b/precompiles/staking/const.go @@ -0,0 +1,22 @@ +package staking + +const ( + DistributeMethodName = "distribute" + DistributeEventName = "Distributed" + DistributeMethodGas = 10000 + + GetAllValidatorsMethodName = "getAllValidators" + GetSharesMethodName = "getShares" + + MoveStakeMethodName = "moveStake" + MoveStakeEventName = "MoveStake" + MoveStakeMethodGas = 10000 + + StakeMethodName = "stake" + StakeEventName = "Stake" + StakeMethodGas = 10000 + + UnstakeMethodName = "unstake" + UnstakeEventName = "Unstake" + UnstakeMethodGas = 1000 +) diff --git a/precompiles/staking/logs.go b/precompiles/staking/logs.go index ea8e51274c..c8d1db24e2 100644 --- a/precompiles/staking/logs.go +++ b/precompiles/staking/logs.go @@ -10,13 +10,7 @@ import ( "github.com/zeta-chain/node/precompiles/logs" ) -const ( - StakeEventName = "Stake" - UnstakeEventName = "Unstake" - MoveStakeEventName = "MoveStake" -) - -func (c *Contract) AddStakeLog( +func (c *Contract) addStakeLog( ctx sdk.Context, stateDB vm.StateDB, staker common.Address, @@ -49,7 +43,7 @@ func (c *Contract) AddStakeLog( return nil } -func (c *Contract) AddUnstakeLog( +func (c *Contract) addUnstakeLog( ctx sdk.Context, stateDB vm.StateDB, staker common.Address, @@ -81,7 +75,7 @@ func (c *Contract) AddUnstakeLog( return nil } -func (c *Contract) AddMoveStakeLog( +func (c *Contract) addMoveStakeLog( ctx sdk.Context, stateDB vm.StateDB, staker common.Address, @@ -123,3 +117,33 @@ func (c *Contract) AddMoveStakeLog( return nil } + +func (c *Contract) addDistributeLog( + ctx sdk.Context, + stateDB vm.StateDB, + distributor common.Address, + zrc20Token common.Address, + amount *big.Int, +) error { + event := c.Abi().Events[DistributeEventName] + + topics, err := logs.MakeTopics( + event, + []interface{}{distributor}, + []interface{}{zrc20Token}, + ) + if err != nil { + return err + } + + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) + if err != nil { + return err + } + + logs.AddLog(ctx, c.Address(), stateDB, topics, data) + + return nil +} diff --git a/precompiles/staking/method_distribute.go b/precompiles/staking/method_distribute.go new file mode 100644 index 0000000000..1fa7351505 --- /dev/null +++ b/precompiles/staking/method_distribute.go @@ -0,0 +1,112 @@ +package staking + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/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/node/precompiles/bank" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +// function distribute(address zrc20, uint256 amount) external returns (bool success) +func (c *Contract) distribute( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 2 { + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + } + } + + // Unpack arguments and check if they are valid. + zrc20Addr, amount, err := unpackDistributeArgs(args) + if err != nil { + return nil, err + } + + // Get the original caller address. Necessary for LockZRC20 to work. + caller, err := precompiletypes.GetEVMCallerAddress(evm, contract) + if err != nil { + return nil, err + } + + // Create the coinSet in advance, if this step fails do not lock ZRC20. + coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) + if err != nil { + return nil, err + } + + // LockZRC20 locks the ZRC20 under the locker address. + // It performs all the necessary checks such as allowance in order to execute a transferFrom. + // - spender is the staking contract address (c.Address()). + // - owner is the caller address. + // - locker is the bank address. Assets are locked under this address to prevent liquidity fragmentation. + if err := c.fungibleKeeper.LockZRC20(ctx, c.zrc20ABI, zrc20Addr, c.Address(), caller, bank.ContractAddress, amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "LockZRC20InBank", + Got: err.Error(), + } + } + + // With the ZRC20 locked, proceed to mint the cosmos coins. + if err := c.bankKeeper.MintCoins(ctx, fungibletypes.ModuleName, coinSet); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "MintCoins", + Got: err.Error(), + } + } + + // Send the coins to the FeePool. + if err := c.bankKeeper.SendCoinsFromModuleToModule(ctx, fungibletypes.ModuleName, authtypes.FeeCollectorName, coinSet); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "SendCoinsFromModuleToModule", + Got: err.Error(), + } + } + + // Log similar message as in abci DistributeValidatorRewards function. + ctx.Logger().Info( + "Distributing ZRC20 Validator Rewards", + "Total", amount.String(), + "Fee_collector", authtypes.FeeCollectorName, + "Denom", precompiletypes.ZRC20ToCosmosDenom(zrc20Addr), + ) + + if err := c.addDistributeLog(ctx, evm.StateDB, caller, zrc20Addr, amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "AddDistributeLog", + Got: err.Error(), + } + } + + return method.Outputs.Pack(true) +} + +func unpackDistributeArgs(args []interface{}) (zrc20Addr common.Address, amount *big.Int, err error) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return common.Address{}, nil, &precompiletypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + amount, ok = args[1].(*big.Int) + if !ok || amount == nil || amount.Sign() <= 0 { + return common.Address{}, nil, &precompiletypes.ErrInvalidAmount{ + Got: amount.String(), + } + } + + return zrc20Addr, amount, nil +} diff --git a/precompiles/staking/method_distribute_test.go b/precompiles/staking/method_distribute_test.go new file mode 100644 index 0000000000..dddb467b74 --- /dev/null +++ b/precompiles/staking/method_distribute_test.go @@ -0,0 +1,283 @@ +package staking + +import ( + "math/big" + "testing" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func Test_Distribute(t *testing.T) { + feeCollectorAddress := authtypes.NewModuleAddress(authtypes.FeeCollectorName).String() + + t.Run("should fail to run distribute as read only method", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(0)}..., + ) + + // Call method as read only. + result, err := s.contract.Run(s.mockEVM, s.mockVMContract, true) + + // Check error and result. + require.ErrorIs(t, err, precompiletypes.ErrWriteMethod{ + Method: DistributeMethodName, + }) + + // Result is empty as the write check is done before executing distribute() function. + // On-chain this would look like reverting, so staticcall is properly reverted. + require.Empty(t, result) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute with 0 token balance", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(0)}..., + ) + + // Call method. + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.ErrorAs( + t, + precompiletypes.ErrInvalidAmount{ + Got: "0", + }, + err, + ) + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute with 0 allowance", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(1000)}..., + ) + + // Call method. + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.Error(t, err) + require.Contains(t, err.Error(), "invalid allowance, got 0") + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute 0 token", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Allow staking to spend ZRC20 tokens. + allowStaking(t, s, big.NewInt(1000)) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(0)}..., + ) + + // Call method. + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.Error(t, err) + require.Contains(t, err.Error(), "invalid token amount: 0") + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute more than allowed to staking", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Allow staking to spend ZRC20 tokens. + allowStaking(t, s, big.NewInt(999)) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(1000)}..., + ) + + // Call method. + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.Error(t, err) + require.Contains(t, err.Error(), "invalid allowance, got 999, wanted 1000") + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute more than user balance", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Allow staking to spend ZRC20 tokens. + allowStaking(t, s, big.NewInt(100000)) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(1001)}..., + ) + + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.Error(t, err) + require.Contains(t, err.Error(), "execution reverted") + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should distribute and lock ZRC20", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Allow staking to spend ZRC20 tokens. + allowStaking(t, s, big.NewInt(1000)) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(1000)}..., + ) + + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.NoError(t, err) + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.True(t, ok) + }) +} diff --git a/precompiles/staking/method_get_all_validators.go b/precompiles/staking/method_get_all_validators.go new file mode 100644 index 0000000000..2187f2858a --- /dev/null +++ b/precompiles/staking/method_get_all_validators.go @@ -0,0 +1,27 @@ +package staking + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" +) + +func (c *Contract) GetAllValidators( + ctx sdk.Context, + method *abi.Method, +) ([]byte, error) { + validators := c.stakingKeeper.GetAllValidators(ctx) + + validatorsRes := make([]Validator, len(validators)) + for i, v := range validators { + validatorsRes[i] = Validator{ + OperatorAddress: v.OperatorAddress, + ConsensusPubKey: v.ConsensusPubkey.String(), + // Safe casting from int32 to uint8, as BondStatus is an enum. + // #nosec G115 always in range + BondStatus: uint8(v.Status), + Jailed: v.Jailed, + } + } + + return method.Outputs.Pack(validatorsRes) +} diff --git a/precompiles/staking/method_get_all_validators_test.go b/precompiles/staking/method_get_all_validators_test.go new file mode 100644 index 0000000000..8a80793de3 --- /dev/null +++ b/precompiles/staking/method_get_all_validators_test.go @@ -0,0 +1,57 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_GetAllValidators(t *testing.T) { + t.Run("should return empty array if validators not set", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + + // Clean all validators. + validatorsList := s.sdkKeepers.StakingKeeper.GetAllValidators(s.ctx) + for _, v := range validatorsList { + s.sdkKeepers.StakingKeeper.RemoveValidator(s.ctx, v.GetOperator()) + } + + methodID := s.contractABI.Methods[GetAllValidatorsMethodName] + s.mockVMContract.Input = methodID.ID + + // ACT + validators, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.NoError(t, err) + + res, err := methodID.Outputs.Unpack(validators) + require.NoError(t, err) + + require.Empty(t, res[0]) + }) + + t.Run("should return validators if set", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetAllValidatorsMethodName] + s.mockVMContract.Input = methodID.ID + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + // ACT + validators, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.NoError(t, err) + + res, err := methodID.Outputs.Unpack(validators) + require.NoError(t, err) + + require.NotEmpty(t, res[0]) + }) +} diff --git a/precompiles/staking/method_get_shares.go b/precompiles/staking/method_get_shares.go new file mode 100644 index 0000000000..fcf6ce7d1f --- /dev/null +++ b/precompiles/staking/method_get_shares.go @@ -0,0 +1,50 @@ +package staking + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) GetShares( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 2 { + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + stakerAddress, ok := args[0].(common.Address) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[0], + } + } + + validatorAddress, ok := args[1].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[1], + } + } + + validator, err := sdk.ValAddressFromBech32(validatorAddress) + if err != nil { + return nil, err + } + + delegation := c.stakingKeeper.Delegation(ctx, sdk.AccAddress(stakerAddress.Bytes()), validator) + shares := big.NewInt(0) + if delegation != nil { + shares = delegation.GetShares().BigInt() + } + + return method.Outputs.Pack(shares) +} diff --git a/precompiles/staking/method_get_shares_test.go b/precompiles/staking/method_get_shares_test.go new file mode 100644 index 0000000000..d0038886f4 --- /dev/null +++ b/precompiles/staking/method_get_shares_test.go @@ -0,0 +1,115 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_GetShares(t *testing.T) { + // Disabled temporarily because the staking functions were disabled. + // Issue: https://github.com/zeta-chain/node/issues/3009 + // t.Run("should return stakes", func(t *testing.T) { + // // ARRANGE + // s := newTestSuite(t) + // methodID := s.contractABI.Methods[GetSharesMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := s.sdkKeepers.BankKeeper.MintCoins(s.ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = s.sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // stakeArgs := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // stakeMethodID := s.contractABI.Methods[StakeMethodName] + + // // ACT + // _, err = s.contract.Stake(s.ctx, s.mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, stakeArgs) + // require.NoError(t, err) + + // // ASSERT + // args := []interface{}{stakerEthAddr, validator.OperatorAddress} + // s.mockVMContract.Input = packInputArgs(t, methodID, args...) + // stakes, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + // require.NoError(t, err) + + // res, err := methodID.Outputs.Unpack(stakes) + // require.NoError(t, err) + // require.Equal( + // t, + // fmt.Sprintf("%d000000000000000000", coins.AmountOf(config.BaseDenom).BigInt().Int64()), + // res[0].(*big.Int).String(), + // ) + // }) + + t.Run("should fail if wrong args amount", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetSharesMethodName] + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + args := []interface{}{stakerEthAddr} + + // ACT + _, err := s.contract.GetShares(s.ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if invalid staker arg", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetSharesMethodName] + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + args := []interface{}{42, validator.OperatorAddress} + + // ACT + _, err := s.contract.GetShares(s.ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if invalid val address", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetSharesMethodName] + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + args := []interface{}{stakerEthAddr, staker.String()} + + // ACT + _, err := s.contract.GetShares(s.ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if invalid val address format", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetSharesMethodName] + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + args := []interface{}{stakerEthAddr, 42} + + // ACT + _, err := s.contract.GetShares(s.ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) +} diff --git a/precompiles/staking/method_move_stake.go b/precompiles/staking/method_move_stake.go new file mode 100644 index 0000000000..71c43bdd7d --- /dev/null +++ b/precompiles/staking/method_move_stake.go @@ -0,0 +1,85 @@ +package staking + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) MoveStake( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 4 { + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 4, + }) + } + + stakerAddress, ok := args[0].(common.Address) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[0], + } + } + + if contract.CallerAddress != stakerAddress { + return nil, fmt.Errorf("caller is not staker address") + } + + validatorSrcAddress, ok := args[1].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[1], + } + } + + validatorDstAddress, ok := args[2].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[2], + } + } + + amount, ok := args[3].(*big.Int) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[3], + } + } + + msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) + res, err := msgServer.BeginRedelegate(ctx, &stakingtypes.MsgBeginRedelegate{ + DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), + ValidatorSrcAddress: validatorSrcAddress, + ValidatorDstAddress: validatorDstAddress, + Amount: sdk.Coin{ + Denom: c.stakingKeeper.BondDenom(ctx), + Amount: math.NewIntFromBigInt(amount), + }, + }) + if err != nil { + return nil, err + } + + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) + err = c.addMoveStakeLog(ctx, stateDB, stakerAddress, validatorSrcAddress, validatorDstAddress, amount) + if err != nil { + return nil, err + } + + return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) +} diff --git a/precompiles/staking/method_move_stake_test.go b/precompiles/staking/method_move_stake_test.go new file mode 100644 index 0000000000..8882442069 --- /dev/null +++ b/precompiles/staking/method_move_stake_test.go @@ -0,0 +1,481 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd/zetacored/config" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/sample" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +func Test_MoveStake(t *testing.T) { + // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + t.Run("should fail with error disabled", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[MoveStakeMethodName] + r := rand.New(rand.NewSource(42)) + validatorSrc := sample.Validator(t, r) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validatorSrc) + validatorDest := sample.Validator(t, r) + + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + coins := sample.Coins() + err := s.sdkKeepers.BankKeeper.MintCoins(s.ctx, fungibletypes.ModuleName, sample.Coins()) + require.NoError(t, err) + err = s.sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, fungibletypes.ModuleName, staker, coins) + require.NoError(t, err) + + stakerAddr := common.BytesToAddress(staker.Bytes()) + s.mockVMContract.CallerAddress = stakerAddr + + argsStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + // stake to validator src + stakeMethodID := s.contractABI.Methods[StakeMethodName] + s.mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + require.Error(t, err) + require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ + Method: StakeMethodName, + }) + + argsMoveStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + validatorDest.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + s.mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // ACT + _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.Error(t, err) + require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ + Method: MoveStakeMethodName, + }) + }) + + // t.Run("should fail in read only method", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, true) + + // // ASSERT + // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: MoveStakeMethodName}) + // }) + + // t.Run("should fail if validator dest doesn't exist", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should move stake", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // // ACT + // // move stake to validator dest + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.NoError(t, err) + // }) + + // t.Run("should fail if staker is invalid arg", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // 42, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if validator src is invalid arg", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // 42, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if validator dest is invalid arg", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // 42, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if amount is invalid arg", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).Uint64(), + // } + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if wrong args amount", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{stakerEthAddr, validatorSrc.OperatorAddress, validatorDest.OperatorAddress} + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if caller is not staker", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // callerEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + // mockVMContract.CallerAddress = callerEthAddr + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.ErrorContains(t, err, "caller is not staker") + // }) +} diff --git a/precompiles/staking/method_stake.go b/precompiles/staking/method_stake.go new file mode 100644 index 0000000000..c19fa6c618 --- /dev/null +++ b/precompiles/staking/method_stake.go @@ -0,0 +1,84 @@ +package staking + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) Stake( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 3 { + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 3, + }) + } + + stakerAddress, ok := args[0].(common.Address) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[0], + } + } + + if contract.CallerAddress != stakerAddress { + return nil, fmt.Errorf("caller is not staker address") + } + + validatorAddress, ok := args[1].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[1], + } + } + + amount, ok := args[2].(*big.Int) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[2], + } + } + + msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) + _, err := msgServer.Delegate(ctx, &stakingtypes.MsgDelegate{ + DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), + ValidatorAddress: validatorAddress, + Amount: sdk.Coin{ + Denom: c.stakingKeeper.BondDenom(ctx), + Amount: math.NewIntFromBigInt(amount), + }, + }) + if err != nil { + return nil, err + } + + // if caller is not the same as origin it means call is coming through smart contract, + // and because state of smart contract calling precompile might be updated as well + // manually reduce amount in stateDB, so it is properly reflected in bank module + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) + if contract.CallerAddress != evm.Origin { + stateDB.SubBalance(stakerAddress, amount) + } + + err = c.addStakeLog(ctx, stateDB, stakerAddress, validatorAddress, amount) + if err != nil { + return nil, err + } + + return method.Outputs.Pack(true) +} diff --git a/precompiles/staking/method_stake_test.go b/precompiles/staking/method_stake_test.go new file mode 100644 index 0000000000..bd5558abdb --- /dev/null +++ b/precompiles/staking/method_stake_test.go @@ -0,0 +1,316 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd/zetacored/config" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/sample" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +func Test_Stake(t *testing.T) { + // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + t.Run("should fail with error disabled", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[StakeMethodName] + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + coins := sample.Coins() + err := s.sdkKeepers.BankKeeper.MintCoins(s.ctx, fungibletypes.ModuleName, sample.Coins()) + require.NoError(t, err) + err = s.sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, fungibletypes.ModuleName, staker, coins) + require.NoError(t, err) + + stakerAddr := common.BytesToAddress(staker.Bytes()) + s.mockVMContract.CallerAddress = stakerAddr + args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + s.mockVMContract.Input = packInputArgs(t, methodID, args...) + + // ACT + _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.Error(t, err) + require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ + Method: StakeMethodName, + }) + }) + + // t.Run("should fail in read only mode", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, true) + + // // ASSERT + // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: StakeMethodName}) + // }) + + // t.Run("should fail if validator doesn't exist", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should stake", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.NoError(t, err) + // }) + + // t.Run("should fail if no input args", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // mockVMContract.Input = methodID.ID + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if caller is not staker", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // nonStakerAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + // args := []interface{}{nonStakerAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.ErrorContains(t, err, "caller is not staker address") + // }) + + // t.Run("should fail if staking fails", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // // staker without funds + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err := contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if wrong args amount", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress} + + // // ACT + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if staker is not eth addr", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{staker, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // // ACT + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if validator is not valid string", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, 42, coins.AmountOf(config.BaseDenom).BigInt()} + + // // ACT + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if amount is not int64", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).Uint64()} + + // // ACT + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) +} diff --git a/precompiles/staking/method_unstake.go b/precompiles/staking/method_unstake.go new file mode 100644 index 0000000000..4efb41993b --- /dev/null +++ b/precompiles/staking/method_unstake.go @@ -0,0 +1,77 @@ +package staking + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) Unstake( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 3 { + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 3, + }) + } + + stakerAddress, ok := args[0].(common.Address) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[0], + } + } + + if contract.CallerAddress != stakerAddress { + return nil, fmt.Errorf("caller is not staker address") + } + + validatorAddress, ok := args[1].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[1], + } + } + + amount, ok := args[2].(*big.Int) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[2], + } + } + + msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) + res, err := msgServer.Undelegate(ctx, &stakingtypes.MsgUndelegate{ + DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), + ValidatorAddress: validatorAddress, + Amount: sdk.Coin{ + Denom: c.stakingKeeper.BondDenom(ctx), + Amount: math.NewIntFromBigInt(amount), + }, + }) + if err != nil { + return nil, err + } + + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) + err = c.addUnstakeLog(ctx, stateDB, stakerAddress, validatorAddress, amount) + if err != nil { + return nil, err + } + + return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) +} diff --git a/precompiles/staking/method_unstake_test.go b/precompiles/staking/method_unstake_test.go new file mode 100644 index 0000000000..e770020946 --- /dev/null +++ b/precompiles/staking/method_unstake_test.go @@ -0,0 +1,311 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd/zetacored/config" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/sample" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +func Test_Unstake(t *testing.T) { + // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + t.Run("should fail with error disabled", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[UnstakeMethodName] + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + coins := sample.Coins() + err := s.sdkKeepers.BankKeeper.MintCoins(s.ctx, fungibletypes.ModuleName, sample.Coins()) + require.NoError(t, err) + err = s.sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, fungibletypes.ModuleName, staker, coins) + require.NoError(t, err) + + stakerAddr := common.BytesToAddress(staker.Bytes()) + s.mockVMContract.CallerAddress = stakerAddr + + args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + s.mockVMContract.Input = packInputArgs(t, methodID, args...) + + // ACT + _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.Error(t, err) + require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ + Method: UnstakeMethodName, + }) + }) + + // t.Run("should fail in read only method", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, true) + + // // ASSERT + // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: UnstakeMethodName}) + // }) + + // t.Run("should fail if validator doesn't exist", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should unstake", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // // stake first + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, args...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // // ACT + // mockVMContract.Input = packInputArgs(t, methodID, args...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.NoError(t, err) + // }) + + // t.Run("should fail if caller is not staker", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // // stake first + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, args...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // callerEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + // mockVMContract.CallerAddress = callerEthAddr + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.ErrorContains(t, err, "caller is not staker address") + // }) + + // t.Run("should fail if no previous staking", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if wrong args amount", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress} + + // // ACT + // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if staker is not eth addr", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{staker, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // // ACT + // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if validator is not valid string", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, 42, coins.AmountOf(config.BaseDenom).BigInt()} + + // // ACT + // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if amount is not int64", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).Uint64()} + + // // ACT + // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) +} diff --git a/precompiles/staking/staking.go b/precompiles/staking/staking.go index d29082e545..3408a2b09e 100644 --- a/precompiles/staking/staking.go +++ b/precompiles/staking/staking.go @@ -1,32 +1,19 @@ package staking 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" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/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" -) - -// method names -const ( - // write - StakeMethodName = "stake" - UnstakeMethodName = "unstake" - MoveStakeMethodName = "moveStake" - - // read - GetAllValidatorsMethodName = "getAllValidators" - GetSharesMethodName = "getShares" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) var ( @@ -54,11 +41,13 @@ func initABI() { // just temporary flat values, double check these flat values // can we just use WriteCostFlat/ReadCostFlat from gas config for flat values? case StakeMethodName: - GasRequiredByMethod[methodID] = 10000 + GasRequiredByMethod[methodID] = StakeMethodGas case UnstakeMethodName: - GasRequiredByMethod[methodID] = 10000 + GasRequiredByMethod[methodID] = UnstakeMethodGas case MoveStakeMethodName: - GasRequiredByMethod[methodID] = 10000 + GasRequiredByMethod[methodID] = MoveStakeMethodGas + case DistributeMethodName: + GasRequiredByMethod[methodID] = DistributeMethodGas case GetAllValidatorsMethodName: GasRequiredByMethod[methodID] = 0 ViewMethod[methodID] = true @@ -72,23 +61,45 @@ func initABI() { } type Contract struct { - ptypes.BaseContract - - stakingKeeper stakingkeeper.Keeper - cdc codec.Codec - kvGasConfig storetypes.GasConfig + precompiletypes.BaseContract + + stakingKeeper stakingkeeper.Keeper + fungibleKeeper fungiblekeeper.Keeper + bankKeeper bankkeeper.Keeper + zrc20ABI *abi.ABI + cdc codec.Codec + kvGasConfig storetypes.GasConfig } func NewIStakingContract( + ctx sdk.Context, stakingKeeper *stakingkeeper.Keeper, + fungibleKeeper fungiblekeeper.Keeper, + bankKeeper bankkeeper.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() + if err != nil { + ctx.Logger().Error("staking contract failed to get ZRC20 ABI", "error", err) + return nil + } + return &Contract{ - BaseContract: ptypes.NewBaseContract(ContractAddress), - stakingKeeper: *stakingKeeper, - cdc: cdc, - kvGasConfig: kvGasConfig, + BaseContract: precompiletypes.NewBaseContract(ContractAddress), + stakingKeeper: *stakingKeeper, + fungibleKeeper: fungibleKeeper, + bankKeeper: bankKeeper, + zrc20ABI: zrc20ABI, + cdc: cdc, + kvGasConfig: kvGasConfig, } } @@ -122,263 +133,6 @@ func (c *Contract) RequiredGas(input []byte) uint64 { return 0 } -func (c *Contract) GetAllValidators( - ctx sdk.Context, - method *abi.Method, -) ([]byte, error) { - validators := c.stakingKeeper.GetAllValidators(ctx) - - validatorsRes := make([]Validator, len(validators)) - for i, v := range validators { - validatorsRes[i] = Validator{ - OperatorAddress: v.OperatorAddress, - ConsensusPubKey: v.ConsensusPubkey.String(), - // #nosec G115 enum always in range - BondStatus: uint8(v.Status), - Jailed: v.Jailed, - } - } - - return method.Outputs.Pack(validatorsRes) -} - -func (c *Contract) GetShares( - ctx sdk.Context, - method *abi.Method, - args []interface{}, -) ([]byte, error) { - if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 2, - }) - } - stakerAddress, ok := args[0].(common.Address) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[0], - } - } - - validatorAddress, ok := args[1].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[1], - } - } - - validator, err := sdk.ValAddressFromBech32(validatorAddress) - if err != nil { - return nil, err - } - - delegation := c.stakingKeeper.Delegation(ctx, sdk.AccAddress(stakerAddress.Bytes()), validator) - shares := big.NewInt(0) - if delegation != nil { - shares = delegation.GetShares().BigInt() - } - - return method.Outputs.Pack(shares) -} - -func (c *Contract) Stake( - ctx sdk.Context, - evm *vm.EVM, - contract *vm.Contract, - method *abi.Method, - args []interface{}, -) ([]byte, error) { - if len(args) != 3 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 3, - }) - } - - stakerAddress, ok := args[0].(common.Address) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[0], - } - } - - if contract.CallerAddress != stakerAddress { - return nil, fmt.Errorf("caller is not staker address") - } - - validatorAddress, ok := args[1].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[1], - } - } - - amount, ok := args[2].(*big.Int) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[2], - } - } - - msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) - _, err := msgServer.Delegate(ctx, &stakingtypes.MsgDelegate{ - DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), - ValidatorAddress: validatorAddress, - Amount: sdk.Coin{ - Denom: c.stakingKeeper.BondDenom(ctx), - Amount: math.NewIntFromBigInt(amount), - }, - }) - if err != nil { - return nil, err - } - - // if caller is not the same as origin it means call is coming through smart contract, - // and because state of smart contract calling precompile might be updated as well - // manually reduce amount in stateDB, so it is properly reflected in bank module - stateDB := evm.StateDB.(ptypes.ExtStateDB) - if contract.CallerAddress != evm.Origin { - stateDB.SubBalance(stakerAddress, amount) - } - - err = c.AddStakeLog(ctx, stateDB, stakerAddress, validatorAddress, amount) - if err != nil { - return nil, err - } - - return method.Outputs.Pack(true) -} - -func (c *Contract) Unstake( - ctx sdk.Context, - evm *vm.EVM, - contract *vm.Contract, - method *abi.Method, - args []interface{}, -) ([]byte, error) { - if len(args) != 3 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 3, - }) - } - - stakerAddress, ok := args[0].(common.Address) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[0], - } - } - - if contract.CallerAddress != stakerAddress { - return nil, fmt.Errorf("caller is not staker address") - } - - validatorAddress, ok := args[1].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[1], - } - } - - amount, ok := args[2].(*big.Int) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[2], - } - } - - msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) - res, err := msgServer.Undelegate(ctx, &stakingtypes.MsgUndelegate{ - DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), - ValidatorAddress: validatorAddress, - Amount: sdk.Coin{ - Denom: c.stakingKeeper.BondDenom(ctx), - Amount: math.NewIntFromBigInt(amount), - }, - }) - if err != nil { - return nil, err - } - - stateDB := evm.StateDB.(ptypes.ExtStateDB) - err = c.AddUnstakeLog(ctx, stateDB, stakerAddress, validatorAddress, amount) - if err != nil { - return nil, err - } - - return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) -} - -func (c *Contract) MoveStake( - ctx sdk.Context, - evm *vm.EVM, - contract *vm.Contract, - method *abi.Method, - args []interface{}, -) ([]byte, error) { - if len(args) != 4 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 4, - }) - } - - stakerAddress, ok := args[0].(common.Address) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[0], - } - } - - if contract.CallerAddress != stakerAddress { - return nil, fmt.Errorf("caller is not staker address") - } - - validatorSrcAddress, ok := args[1].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[1], - } - } - - validatorDstAddress, ok := args[2].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[2], - } - } - - amount, ok := args[3].(*big.Int) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[3], - } - } - - msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) - res, err := msgServer.BeginRedelegate(ctx, &stakingtypes.MsgBeginRedelegate{ - DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), - ValidatorSrcAddress: validatorSrcAddress, - ValidatorDstAddress: validatorDstAddress, - Amount: sdk.Coin{ - Denom: c.stakingKeeper.BondDenom(ctx), - Amount: math.NewIntFromBigInt(amount), - }, - }) - if err != nil { - return nil, err - } - - stateDB := evm.StateDB.(ptypes.ExtStateDB) - err = c.AddMoveStakeLog(ctx, stateDB, stakerAddress, validatorSrcAddress, validatorDstAddress, amount) - if err != nil { - return nil, err - } - - return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) -} - // 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) { @@ -392,7 +146,14 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return nil, err } - stateDB := evm.StateDB.(ptypes.ExtStateDB) + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) + + // If the method is not a view method, it should not be executed in read-only mode. + if _, isViewMethod := ViewMethod[[4]byte(method.ID)]; !isViewMethod && readOnly { + return nil, precompiletypes.ErrWriteMethod{ + Method: method.Name, + } + } switch method.Name { case GetAllValidatorsMethodName: @@ -417,17 +178,11 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, nil case StakeMethodName: // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - return nil, ptypes.ErrDisabledMethod{ + return nil, precompiletypes.ErrDisabledMethod{ Method: method.Name, } //nolint:govet - if readOnly { - return nil, ptypes.ErrWriteMethod{ - Method: method.Name, - } - } - var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { res, err = c.Stake(ctx, evm, contract, method, args) @@ -439,17 +194,11 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, nil case UnstakeMethodName: // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - return nil, ptypes.ErrDisabledMethod{ + return nil, precompiletypes.ErrDisabledMethod{ Method: method.Name, } //nolint:govet - if readOnly { - return nil, ptypes.ErrWriteMethod{ - Method: method.Name, - } - } - var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { res, err = c.Unstake(ctx, evm, contract, method, args) @@ -461,17 +210,11 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, nil case MoveStakeMethodName: // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - return nil, ptypes.ErrDisabledMethod{ + return nil, precompiletypes.ErrDisabledMethod{ Method: method.Name, } //nolint:govet - if readOnly { - return nil, ptypes.ErrWriteMethod{ - Method: method.Name, - } - } - var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { res, err = c.MoveStake(ctx, evm, contract, method, args) @@ -481,9 +224,23 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return nil, err } return res, nil + case DistributeMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.distribute(ctx, evm, contract, method, args) + return err + }) + if execErr != nil { + res, errPack := method.Outputs.Pack(false) + if errPack != nil { + return nil, errPack + } + return res, err + } + return res, nil default: - return nil, ptypes.ErrInvalidMethod{ + return nil, precompiletypes.ErrInvalidMethod{ Method: method.Name, } } diff --git a/precompiles/staking/staking_test.go b/precompiles/staking/staking_test.go index aaea87f94d..b7b00ede0d 100644 --- a/precompiles/staking/staking_test.go +++ b/precompiles/staking/staking_test.go @@ -2,17 +2,16 @@ package staking import ( "encoding/json" - "fmt" "testing" "math/big" - "math/rand" tmdb "github.com/cometbft/cometbft-db" "github.com/cosmos/cosmos-sdk/store" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -20,103 +19,39 @@ import ( "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/cmd/zetacored/config" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" "github.com/zeta-chain/node/precompiles/prototype" - 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" + "github.com/zeta-chain/protocol-contracts/v2/pkg/gatewayzevm.sol" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" ) -func setup(t *testing.T) (sdk.Context, *Contract, abi.ABI, keeper.SDKKeepers, *vm.EVM, *vm.Contract) { - var encoding ethermint.EncodingConfig - appCodec := encoding.Codec - - cdc := keeper.NewCodec() - - db := tmdb.NewMemDB() - stateStore := store.NewCommitMultiStore(db) - keys, memKeys, tkeys, allKeys := keeper.StoreKeys() - sdkKeepers := keeper.NewSDKKeepersWithKeys(cdc, keys, memKeys, tkeys, allKeys) - for _, key := range keys { - stateStore.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db) - } - for _, key := range tkeys { - stateStore.MountStoreWithDB(key, storetypes.StoreTypeTransient, nil) - } - for _, key := range memKeys { - stateStore.MountStoreWithDB(key, storetypes.StoreTypeMemory, nil) - } - - gasConfig := storetypes.TransientGasConfig() - ctx := keeper.NewContext(stateStore) - - require.NoError(t, stateStore.LoadLatestVersion()) - - stakingGenesisState := stakingtypes.DefaultGenesisState() - stakingGenesisState.Params.BondDenom = config.BaseDenom - sdkKeepers.StakingKeeper.InitGenesis(ctx, stakingGenesisState) - - contract := NewIStakingContract(&sdkKeepers.StakingKeeper, appCodec, gasConfig) - require.NotNil(t, contract, "NewIStakingContract() 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 ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract -} - -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 -} - func Test_IStakingContract(t *testing.T) { - _, contract, abi, _, _, _ := setup(t) + s := newTestSuite(t) gasConfig := storetypes.TransientGasConfig() t.Run("should check methods are present in ABI", func(t *testing.T) { - require.NotNil(t, abi.Methods[StakeMethodName], "stake method should be present in the ABI") - require.NotNil(t, abi.Methods[UnstakeMethodName], "unstake method should be present in the ABI") + require.NotNil(t, s.contractABI.Methods[StakeMethodName], "stake method should be present in the ABI") + require.NotNil(t, s.contractABI.Methods[UnstakeMethodName], "unstake method should be present in the ABI") require.NotNil( t, - abi.Methods[MoveStakeMethodName], + s.contractABI.Methods[MoveStakeMethodName], "moveStake method should be present in the ABI", ) require.NotNil( t, - abi.Methods[GetAllValidatorsMethodName], + s.contractABI.Methods[GetAllValidatorsMethodName], "getAllValidators method should be present in the ABI", ) - require.NotNil(t, abi.Methods[GetSharesMethodName], "getShares method should be present in the ABI") + require.NotNil(t, s.contractABI.Methods[GetSharesMethodName], "getShares method should be present in the ABI") }) t.Run("should check gas requirements for methods", func(t *testing.T) { @@ -124,9 +59,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("stake", func(t *testing.T) { // ACT - stake := contract.RequiredGas(abi.Methods[StakeMethodName].ID) + stake := s.contract.RequiredGas(s.contractABI.Methods[StakeMethodName].ID) // ASSERT - copy(method[:], abi.Methods[StakeMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[StakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -140,9 +75,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("unstake", func(t *testing.T) { // ACT - unstake := contract.RequiredGas(abi.Methods[UnstakeMethodName].ID) + unstake := s.contract.RequiredGas(s.contractABI.Methods[UnstakeMethodName].ID) // ASSERT - copy(method[:], abi.Methods[UnstakeMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[UnstakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -156,9 +91,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("moveStake", func(t *testing.T) { // ACT - moveStake := contract.RequiredGas(abi.Methods[MoveStakeMethodName].ID) + moveStake := s.contract.RequiredGas(s.contractABI.Methods[MoveStakeMethodName].ID) // ASSERT - copy(method[:], abi.Methods[MoveStakeMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[MoveStakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -172,9 +107,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("getAllValidators", func(t *testing.T) { // ACT - getAllValidators := contract.RequiredGas(abi.Methods[GetAllValidatorsMethodName].ID) + getAllValidators := s.contract.RequiredGas(s.contractABI.Methods[GetAllValidatorsMethodName].ID) // ASSERT - copy(method[:], abi.Methods[GetAllValidatorsMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[GetAllValidatorsMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.ReadCostPerByte require.Equal( t, @@ -188,9 +123,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("getShares", func(t *testing.T) { // ACT - getShares := contract.RequiredGas(abi.Methods[GetSharesMethodName].ID) + getShares := s.contract.RequiredGas(s.contractABI.Methods[GetSharesMethodName].ID) // ASSERT - copy(method[:], abi.Methods[GetSharesMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[GetSharesMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.ReadCostPerByte require.Equal( t, @@ -206,7 +141,7 @@ func Test_IStakingContract(t *testing.T) { // ARRANGE invalidMethodBytes := []byte("invalidMethod") // ACT - gasInvalidMethod := contract.RequiredGas(invalidMethodBytes) + gasInvalidMethod := s.contract.RequiredGas(invalidMethodBytes) // ASSERT require.Equal( t, @@ -221,9 +156,9 @@ func Test_IStakingContract(t *testing.T) { } func Test_InvalidMethod(t *testing.T) { - _, _, abi, _, _, _ := setup(t) + s := newTestSuite(t) - _, doNotExist := abi.Methods["invalidMethod"] + _, doNotExist := s.contractABI.Methods["invalidMethod"] require.False(t, doNotExist, "invalidMethod should not be present in the ABI") } @@ -238,1238 +173,379 @@ func Test_InvalidABI(t *testing.T) { initABI() } -func Test_Stake(t *testing.T) { - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - t.Run("should fail with error disabled", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[StakeMethodName] - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - coins := sample.Coins() - err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - require.NoError(t, err) - err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - require.NoError(t, err) - - stakerAddr := common.BytesToAddress(staker.Bytes()) - mockVMContract.CallerAddress = stakerAddr - args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - mockVMContract.Input = packInputArgs(t, methodID, args...) - - // ACT - _, err = contract.Run(mockEVM, mockVMContract, false) - - // ASSERT - require.Error(t, err) - require.ErrorIs(t, err, ptypes.ErrDisabledMethod{ - Method: StakeMethodName, - }) - }) +func Test_RunInvalidMethod(t *testing.T) { + // ARRANGE + s := newTestSuite(t) - // t.Run("should fail in read only mode", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, true) - - // // ASSERT - // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: StakeMethodName}) - // }) - - // t.Run("should fail if validator doesn't exist", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should stake", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.NoError(t, err) - // }) - - // t.Run("should fail if no input args", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // mockVMContract.Input = methodID.ID - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if caller is not staker", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // nonStakerAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) - // args := []interface{}{nonStakerAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.ErrorContains(t, err, "caller is not staker address") - // }) - - // t.Run("should fail if staking fails", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // // staker without funds - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err := contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if wrong args amount", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress} - - // // ACT - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if staker is not eth addr", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{staker, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - - // // ACT - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if validator is not valid string", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, 42, coins.AmountOf(config.BaseDenom).BigInt()} - - // // ACT - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if amount is not int64", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).Uint64()} - - // // ACT - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) -} + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + gasConfig := storetypes.TransientGasConfig() -func Test_Unstake(t *testing.T) { - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - t.Run("should fail with error disabled", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[UnstakeMethodName] - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - coins := sample.Coins() - err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - require.NoError(t, err) - err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - require.NoError(t, err) - - stakerAddr := common.BytesToAddress(staker.Bytes()) - mockVMContract.CallerAddress = stakerAddr - - args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - mockVMContract.Input = packInputArgs(t, methodID, args...) - - // ACT - _, err = contract.Run(mockEVM, mockVMContract, false) - - // ASSERT - require.Error(t, err) - require.ErrorIs(t, err, ptypes.ErrDisabledMethod{ - Method: UnstakeMethodName, - }) - }) + prototype := prototype.NewIPrototypeContract(s.fungibleKeeper, appCodec, gasConfig) + + prototypeAbi := prototype.Abi() + methodID := prototypeAbi.Methods["bech32ToHexAddr"] + args := []interface{}{"123"} + s.mockVMContract.Input = packInputArgs(t, methodID, args...) - // t.Run("should fail in read only method", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, true) - - // // ASSERT - // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: UnstakeMethodName}) - // }) - - // t.Run("should fail if validator doesn't exist", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should unstake", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - - // // stake first - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, args...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // // ACT - // mockVMContract.Input = packInputArgs(t, methodID, args...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.NoError(t, err) - // }) - - // t.Run("should fail if caller is not staker", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // // stake first - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, args...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // callerEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) - // mockVMContract.CallerAddress = callerEthAddr - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.ErrorContains(t, err, "caller is not staker address") - // }) - - // t.Run("should fail if no previous staking", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if wrong args amount", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress} - - // // ACT - // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if staker is not eth addr", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{staker, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - - // // ACT - // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if validator is not valid string", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, 42, coins.AmountOf(config.BaseDenom).BigInt()} - - // // ACT - // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if amount is not int64", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).Uint64()} - - // // ACT - // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) + // ACT + _, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.Error(t, err) } -func Test_MoveStake(t *testing.T) { - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - t.Run("should fail with error disabled", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[MoveStakeMethodName] - r := rand.New(rand.NewSource(42)) - validatorSrc := sample.Validator(t, r) - sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - validatorDest := sample.Validator(t, r) - - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - coins := sample.Coins() - err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - require.NoError(t, err) - err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - require.NoError(t, err) - - stakerAddr := common.BytesToAddress(staker.Bytes()) - mockVMContract.CallerAddress = stakerAddr - - argsStake := []interface{}{ - stakerEthAddr, - validatorSrc.OperatorAddress, - coins.AmountOf(config.BaseDenom).BigInt(), - } +func setup(t *testing.T) (sdk.Context, *Contract, abi.ABI, keeper.SDKKeepers, *vm.EVM, *vm.Contract) { + // Initialize state. + // Get sdk keepers initialized with this state and the context. + cdc := keeper.NewCodec() + db := tmdb.NewMemDB() + stateStore := store.NewCommitMultiStore(db) + keys, memKeys, tkeys, allKeys := keeper.StoreKeys() - // stake to validator src - stakeMethodID := abi.Methods[StakeMethodName] - mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - _, err = contract.Run(mockEVM, mockVMContract, false) - require.Error(t, err) - require.ErrorIs(t, err, ptypes.ErrDisabledMethod{ - Method: StakeMethodName, - }) + sdkKeepers := keeper.NewSDKKeepersWithKeys(cdc, keys, memKeys, tkeys, allKeys) - argsMoveStake := []interface{}{ - stakerEthAddr, - validatorSrc.OperatorAddress, - validatorDest.OperatorAddress, - coins.AmountOf(config.BaseDenom).BigInt(), - } - mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + for _, key := range keys { + stateStore.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db) + } + for _, key := range tkeys { + stateStore.MountStoreWithDB(key, storetypes.StoreTypeTransient, nil) + } + for _, key := range memKeys { + stateStore.MountStoreWithDB(key, storetypes.StoreTypeMemory, nil) + } - // ACT - _, err = contract.Run(mockEVM, mockVMContract, false) + require.NoError(t, stateStore.LoadLatestVersion()) - // ASSERT - require.Error(t, err) - require.ErrorIs(t, err, ptypes.ErrDisabledMethod{ - Method: MoveStakeMethodName, - }) - }) + ctx := keeper.NewContext(stateStore) - // t.Run("should fail in read only method", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, true) - - // // ASSERT - // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: MoveStakeMethodName}) - // }) - - // t.Run("should fail if validator dest doesn't exist", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should move stake", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) - - // // ACT - // // move stake to validator dest - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.NoError(t, err) - // }) - - // t.Run("should fail if staker is invalid arg", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // 42, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if validator src is invalid arg", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // 42, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if validator dest is invalid arg", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // 42, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if amount is invalid arg", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).Uint64(), - // } - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if wrong args amount", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{stakerEthAddr, validatorSrc.OperatorAddress, validatorDest.OperatorAddress} - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if caller is not staker", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) - - // callerEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) - // mockVMContract.CallerAddress = callerEthAddr - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.ErrorContains(t, err, "caller is not staker") - // }) + // Intiliaze codecs and gas config. + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + gasConfig := storetypes.TransientGasConfig() + + stakingGenesisState := stakingtypes.DefaultGenesisState() + stakingGenesisState.Params.BondDenom = config.BaseDenom + sdkKeepers.StakingKeeper.InitGenesis(ctx, stakingGenesisState) + + // Get the fungible keeper. + fungibleKeeper, _, _, _ := keeper.FungibleKeeper(t) + + accAddress := sdk.AccAddress(ContractAddress.Bytes()) + fungibleKeeper.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + + // Initialize staking contract. + contract := NewIStakingContract( + ctx, + &sdkKeepers.StakingKeeper, + *fungibleKeeper, + sdkKeepers.BankKeeper, + appCodec, + gasConfig, + ) + require.NotNil(t, contract, "NewIStakingContract() 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 ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract } -func Test_GetAllValidators(t *testing.T) { - t.Run("should return empty array if validators not set", func(t *testing.T) { - // ARRANGE - _, contract, abi, _, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[GetAllValidatorsMethodName] - mockVMContract.Input = methodID.ID +/* + Complete Test Suite + TODO: Migrate all staking tests to this suite. +*/ + +type testSuite struct { + ctx sdk.Context + contract *Contract + contractABI *abi.ABI + fungibleKeeper *fungiblekeeper.Keeper + sdkKeepers keeper.SDKKeepers + mockEVM *vm.EVM + mockVMContract *vm.Contract + methodID abi.Method + defaultCaller common.Address + defaultLocker common.Address + zrc20Address common.Address + zrc20ABI *abi.ABI +} - // ACT - validators, err := contract.Run(mockEVM, mockVMContract, false) +func newTestSuite(t *testing.T) testSuite { + // Initialize basic parameters to mock the chain. + fungibleKeeper, ctx, sdkKeepers, _ := keeper.FungibleKeeper(t) + chainID := getValidChainID(t) - // ASSERT - require.NoError(t, err) + // 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) - res, err := methodID.Outputs.Unpack(validators) - require.NoError(t, err) + // 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") - require.Empty(t, res[0]) - }) + // Keepers and chain configuration. + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + gasConfig := storetypes.TransientGasConfig() - t.Run("should return validators if set", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[GetAllValidatorsMethodName] - mockVMContract.Input = methodID.ID - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + // Create the staking contract. + contract := NewIStakingContract( + ctx, + &sdkKeepers.StakingKeeper, + *fungibleKeeper, + sdkKeepers.BankKeeper, + appCodec, + gasConfig, + ) + require.NotNil(t, contract, "NewIStakingContract() should not return a nil contract") - // ACT - validators, err := contract.Run(mockEVM, mockVMContract, false) + accAddress := sdk.AccAddress(ContractAddress.Bytes()) + fungibleKeeper.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) - // ASSERT - require.NoError(t, err) + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") - res, err := methodID.Outputs.Unpack(validators) - require.NoError(t, err) + address := contract.Address() + require.NotNil(t, address, "contract address should not be nil") - require.NotEmpty(t, res[0]) - }) + 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, + ) + + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + // Default locker is the bank address. + locker := common.HexToAddress("0x0000000000000000000000000000000000000067") + + // Set default caller. + caller := fungibletypes.ModuleAddressEVM + mockVMContract.CallerAddress = caller + mockEVM.Origin = caller + + return testSuite{ + ctx, + contract, + &abi, + fungibleKeeper, + sdkKeepers, + mockEVM, + mockVMContract, + abi.Methods[DistributeMethodName], + caller, + locker, + zrc20Address, + zrc20ABI, + } } -func Test_GetShares(t *testing.T) { - t.Run("should return stakes", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[GetSharesMethodName] - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - coins := sample.Coins() - err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - require.NoError(t, err) - err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - require.NoError(t, err) - - stakerAddr := common.BytesToAddress(staker.Bytes()) - - stakeArgs := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - - stakeMethodID := abi.Methods[StakeMethodName] - - // ACT - _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, stakeArgs) - require.NoError(t, err) - - // ASSERT - args := []interface{}{stakerEthAddr, validator.OperatorAddress} - mockVMContract.Input = packInputArgs(t, methodID, args...) - stakes, err := contract.Run(mockEVM, mockVMContract, false) - require.NoError(t, err) - - res, err := methodID.Outputs.Unpack(stakes) - require.NoError(t, err) - require.Equal( - t, - fmt.Sprintf("%d000000000000000000", coins.AmountOf(config.BaseDenom).BigInt().Int64()), - res[0].(*big.Int).String(), - ) - }) +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...) +} - t.Run("should fail if wrong args amount", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, _, _, _ := setup(t) - methodID := abi.Methods[GetSharesMethodName] - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - args := []interface{}{stakerEthAddr} +func allowStaking(t *testing.T, ts testSuite, amount *big.Int) { + resAllowance, err := callEVM( + t, + ts.ctx, + ts.fungibleKeeper, + ts.zrc20ABI, + fungibletypes.ModuleAddressEVM, + ts.zrc20Address, + "approve", + []interface{}{ts.contract.Address(), amount}, + ) + require.NoError(t, err, "error allowing staking to spend ZRC20 tokens") - // ACT - _, err := contract.GetShares(ctx, &methodID, args) + allowed, ok := resAllowance[0].(bool) + require.True(t, ok) + require.True(t, allowed) +} - // ASSERT - require.Error(t, err) - }) +func callEVM( + t *testing.T, + 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 + true, // noEthereumTxEvent + method, // method + args..., // args + ) + require.NoError(t, err, "CallEVM error") + require.Equal(t, "", res.VmError, "res.VmError should be empty") - t.Run("should fail if invalid staker arg", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, _, _, _ := setup(t) - methodID := abi.Methods[GetSharesMethodName] - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - args := []interface{}{42, validator.OperatorAddress} + ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) + require.NoError(t, err, "Unpack error") - // ACT - _, err := contract.GetShares(ctx, &methodID, args) + return ret, nil +} - // ASSERT - require.Error(t, err) - }) +// 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 +} - t.Run("should fail if invalid val address", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, _, _, _ := setup(t) - methodID := abi.Methods[GetSharesMethodName] - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - args := []interface{}{stakerEthAddr, staker.String()} +// 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) +} - // ACT - _, err := contract.GetShares(ctx, &methodID, args) +// 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 - // ASSERT - require.Error(t, err) - }) + wzeta, err = k.DeployWZETA(ctx) + require.NoError(t, err) + require.NotEmpty(t, wzeta) + assertContractDeployment(t, evmk, ctx, wzeta) - t.Run("should fail if invalid val address format", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, _, _, _ := setup(t) - methodID := abi.Methods[GetSharesMethodName] - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - args := []interface{}{stakerEthAddr, 42} + uniswapV2Factory, err = k.DeployUniswapV2Factory(ctx) + require.NoError(t, err) + require.NotEmpty(t, uniswapV2Factory) + assertContractDeployment(t, evmk, ctx, uniswapV2Factory) - // ACT - _, err := contract.GetShares(ctx, &methodID, args) + uniswapV2Router, err = k.DeployUniswapV2Router02(ctx, uniswapV2Factory, wzeta) + require.NoError(t, err) + require.NotEmpty(t, uniswapV2Router) + assertContractDeployment(t, evmk, ctx, uniswapV2Router) - // ASSERT - require.Error(t, err) - }) + 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 } -func Test_RunInvalidMethod(t *testing.T) { - // ARRANGE - _, contract, _, _, mockEVM, mockVMContract := setup(t) - k, _, _, _ := keeper.FungibleKeeper(t) +// 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) - var encoding ethermint.EncodingConfig - appCodec := encoding.Codec - gasConfig := storetypes.TransientGasConfig() + // Deploy the proxy contract + gatewayABI, err := gatewayzevm.GatewayZEVMMetaData.GetAbi() + require.NoError(t, err) - prototype := prototype.NewIPrototypeContract(k, appCodec, gasConfig) + // Encode the initializer data + initializerData, err := gatewayABI.Pack("initialize", wzeta, admin) + require.NoError(t, err) - prototypeAbi := prototype.Abi() - methodID := prototypeAbi.Methods["bech32ToHexAddr"] - args := []interface{}{"123"} - mockVMContract.Input = packInputArgs(t, methodID, args...) + gatewayContract, err := k.DeployContract(ctx, erc1967proxy.ERC1967ProxyMetaData, implAddr, initializerData) + require.NoError(t, err) + require.NotEmpty(t, gatewayContract) + assertContractDeployment(t, *evmk, ctx, gatewayContract) - // ACT - _, err := contract.Run(mockEVM, mockVMContract, false) + // 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) - // ASSERT - require.Error(t, err) + return gatewayContract +} + +type contractRef struct { + address common.Address +} + +func (c contractRef) Address() common.Address { + return c.address } diff --git a/precompiles/types/address.go b/precompiles/types/address.go new file mode 100644 index 0000000000..5e6654cc5b --- /dev/null +++ b/precompiles/types/address.go @@ -0,0 +1,48 @@ +package types + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" +) + +// 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) { + if evm == nil || contract == nil { + return common.Address{}, errors.New("invalid input: evm or contract is nil") + } + + 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, &ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "empty address", + } + } + + if bankKeeper.BlockedAddr(toAddr) { + return nil, &ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "destination address blocked by bank keeper", + } + } + + return toAddr, nil +} diff --git a/precompiles/types/address_test.go b/precompiles/types/address_test.go new file mode 100644 index 0000000000..801df4f8d4 --- /dev/null +++ b/precompiles/types/address_test.go @@ -0,0 +1,68 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_GetEVMCallerAddress(t *testing.T) { + t.Run("should raise error when evm is nil", func(t *testing.T) { + _, mockVMContract := setupMockEVMAndContract(common.Address{}) + caller, err := GetEVMCallerAddress(nil, &mockVMContract) + require.Error(t, err) + require.Equal(t, common.Address{}, caller, "address should be zeroed") + }) + + t.Run("should raise error when contract is nil", func(t *testing.T) { + mockEVM, _ := setupMockEVMAndContract(common.Address{}) + caller, err := GetEVMCallerAddress(&mockEVM, nil) + require.Error(t, err) + require.Equal(t, common.Address{}, caller, "address should be zeroed") + }) + + // When contract.CallerAddress == evm.Origin, caller is set to contract.CallerAddress. + t.Run("when caller address equals origin", func(t *testing.T) { + mockEVM, mockVMContract := setupMockEVMAndContract(common.Address{}) + caller, err := GetEVMCallerAddress(&mockEVM, &mockVMContract) + require.NoError(t, err) + require.Equal(t, common.Address{}, caller, "address should be the same") + }) + + // When contract.CallerAddress != evm.Origin, caller should be set to evm.Origin. + t.Run("when caller address equals origin", func(t *testing.T) { + mockEVM, mockVMContract := setupMockEVMAndContract(sample.EthAddress()) + caller, err := GetEVMCallerAddress(&mockEVM, &mockVMContract) + require.NoError(t, err) + require.Equal(t, mockEVM.Origin, caller, "address should be evm.Origin") + }) +} + +func setupMockEVMAndContract(address common.Address) (vm.EVM, vm.Contract) { + mockEVM := vm.EVM{ + TxContext: vm.TxContext{ + Origin: address, + }, + } + + mockVMContract := vm.NewContract( + contractRef{address: common.Address{}}, + contractRef{address: common.Address{}}, + big.NewInt(0), + 0, + ) + + return mockEVM, *mockVMContract +} + +type contractRef struct { + address common.Address +} + +func (c contractRef) Address() common.Address { + return c.address +} diff --git a/precompiles/bank/coin.go b/precompiles/types/coin.go similarity index 59% rename from precompiles/bank/coin.go rename to precompiles/types/coin.go index 121ecdc1ee..4219040d42 100644 --- a/precompiles/bank/coin.go +++ b/precompiles/types/coin.go @@ -1,4 +1,4 @@ -package bank +package types import ( "math/big" @@ -7,19 +7,27 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" - ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/cmd/zetacored/config" ) // ZRC20ToCosmosDenom returns the cosmos coin address for a given ZRC20 address. -// This is converted to "zevm/{ZRC20Address}". +// This is converted to "zrc20/{ZRC20Address}". func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { - return ZRC20DenomPrefix + ZRC20Address.String() + return config.ZRC20DenomPrefix + ZRC20Address.String() } -func createCoinSet(tokenDenom string, amount *big.Int) (sdk.Coins, error) { - coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) +func CreateCoinSet(zrc20address common.Address, amount *big.Int) (sdk.Coins, error) { + defer func() { + if r := recover(); r != nil { + return + } + }() + + denom := ZRC20ToCosmosDenom(zrc20address) + + coin := sdk.NewCoin(denom, math.NewIntFromBigInt(amount)) if !coin.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ + return nil, &ErrInvalidCoin{ Got: coin.GetDenom(), Negative: coin.IsNegative(), Nil: coin.IsNil(), @@ -28,10 +36,10 @@ func createCoinSet(tokenDenom string, amount *big.Int) (sdk.Coins, error) { // 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. + // But coinSet will only contain one coin, always. coinSet := sdk.NewCoins(coin) if !coinSet.IsValid() || coinSet.Empty() || coinSet.IsAnyNil() || coinSet == nil { - return nil, &ptypes.ErrInvalidCoin{ + return nil, &ErrInvalidCoin{ Got: coinSet.String(), Negative: coinSet.IsAnyNegative(), Nil: coinSet.IsAnyNil(), diff --git a/precompiles/bank/coin_test.go b/precompiles/types/coin_test.go similarity index 81% rename from precompiles/bank/coin_test.go rename to precompiles/types/coin_test.go index 0a9669e0a6..6a8aac7d46 100644 --- a/precompiles/bank/coin_test.go +++ b/precompiles/types/coin_test.go @@ -1,4 +1,4 @@ -package bank +package types import ( "math/big" @@ -16,10 +16,11 @@ func Test_ZRC20ToCosmosDenom(t *testing.T) { } func Test_createCoinSet(t *testing.T) { - tokenDenom := "zrc20/0x0000000000000000000000000000000000003039" + tokenAddr := common.HexToAddress("0x0000000000000000000000000000000000003039") + tokenDenom := ZRC20ToCosmosDenom(tokenAddr) amount := big.NewInt(100) - coinSet, err := createCoinSet(tokenDenom, amount) + coinSet, err := CreateCoinSet(tokenAddr, amount) require.NoError(t, err, "createCoinSet should not return an error") require.NotNil(t, coinSet, "coinSet should not be nil") diff --git a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go index f7d152f749..7ac3d7ef22 100644 --- a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go +++ b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go @@ -24,8 +24,9 @@ func (k Keeper) LockZRC20( amount *big.Int, ) error { // owner is the EOA owner of the ZRC20 tokens. + // spender is the EOA allowed to spend ZRC20 on owner's behalf. // locker is the address that will lock the ZRC20 tokens, i.e: bank precompile. - if err := k.CheckZRC20Allowance(ctx, zrc20ABI, owner, locker, zrc20Address, amount); err != nil { + if err := k.CheckZRC20Allowance(ctx, zrc20ABI, owner, spender, zrc20Address, amount); err != nil { return errors.Wrap(err, "failed allowance check") }