diff --git a/app/app.go b/app/app.go index 84b9a49253..7c4186af5c 100644 --- a/app/app.go +++ b/app/app.go @@ -567,7 +567,12 @@ func New( &app.FeeMarketKeeper, tracer, evmSs, - precompiles.StatefulContracts(&app.FungibleKeeper, appCodec, storetypes.TransientGasConfig()), + precompiles.StatefulContracts( + &app.FungibleKeeper, + app.StakingKeeper, + appCodec, + storetypes.TransientGasConfig(), + ), app.ConsensusParamsKeeper, aggregateAllKeys(keys, tKeys, memKeys), ) diff --git a/changelog.md b/changelog.md index 31fe5550fc..3298e433b1 100644 --- a/changelog.md +++ b/changelog.md @@ -14,8 +14,9 @@ * [2681](https://github.com/zeta-chain/node/pull/2681) - implement `MsgUpdateERC20CustodyPauseStatus` to pause or unpause ERC20 Custody contract (to be used for the migration process for smart contract V2) * [2644](https://github.com/zeta-chain/node/pull/2644) - add created_timestamp to cctx status * [2673](https://github.com/zeta-chain/node/pull/2673) - add relayer key importer, encryption and decryption -* [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts. +* [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts * [2788](https://github.com/zeta-chain/node/pull/2788) - add common importable zetacored rpc package +* [2784](https://github.com/zeta-chain/node/pull/2784) - staking precompiled contract ### Refactor diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 40900872f4..fdde8e5840 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -309,7 +309,8 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if !skipPrecompiles { precompiledContractTests = []string{ - e2etests.TestZetaPrecompilesPrototypeName, + e2etests.TestPrecompilesPrototypeName, + e2etests.TestPrecompilesStakingName, } } diff --git a/codecov.yml b/codecov.yml index 474cfe97ee..fedb830848 100644 --- a/codecov.yml +++ b/codecov.yml @@ -81,3 +81,4 @@ ignore: - "precompiles/**/*.json" - "precompiles/**/*.sol" - "precompiles/prototype/IPrototype.go" + - "precompiles/staking/IStaking.go" diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 0e3eda7e54..ed762288bc 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -128,6 +128,11 @@ fund_eth_from_config '.additional_accounts.user_v2_ether_revert.evm_address' 100 # unlock v2 erc20 revert tests accounts fund_eth_from_config '.additional_accounts.user_v2_erc20_revert.evm_address' 10000 "V2 ERC20 revert tester" +# unlock precompile tests accounts +address=$(yq -r '.additional_accounts.user_precompile.evm_address' config.yml) +echo "funding precompile tester address ${address} with 10000 Ether" +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null + # unlock local solana relayer accounts if host solana > /dev/null; then solana_url=$(config_str '.rpcs.solana') diff --git a/contrib/localnet/scripts/start-zetacored.sh b/contrib/localnet/scripts/start-zetacored.sh index f1d3e11872..a7cf53021f 100755 --- a/contrib/localnet/scripts/start-zetacored.sh +++ b/contrib/localnet/scripts/start-zetacored.sh @@ -254,6 +254,9 @@ then # migration tester address=$(yq -r '.additional_accounts.user_migration.bech32_address' /root/config.yml) zetacored add-genesis-account "$address" 100000000000000000000000000azeta +# precompiles tester + address=$(yq -r '.additional_accounts.user_precompile.bech32_address' /root/config.yml) + zetacored add-genesis-account "$address" 100000000000000000000000000azeta # v2 ether tester address=$(yq -r '.additional_accounts.user_v2_ether.bech32_address' /root/config.yml) zetacored add-genesis-account "$address" 100000000000000000000000000azeta diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 988c4fc5b8..7d09bd966c 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -147,7 +147,8 @@ const ( /* Stateful precompiled contracts tests */ - TestZetaPrecompilesPrototypeName = "precompile_contracts_prototype" + TestPrecompilesPrototypeName = "precompile_contracts_prototype" + TestPrecompilesStakingName = "precompile_contracts_staking" ) // AllE2ETests is an ordered list of all e2e tests @@ -814,9 +815,15 @@ var AllE2ETests = []runner.E2ETest{ Stateful precompiled contracts tests */ runner.NewE2ETest( - TestZetaPrecompilesPrototypeName, + TestPrecompilesPrototypeName, "test stateful precompiled contracts prototype", []runner.ArgDefinition{}, - TestPrecompilesRegular, + TestPrecompilesPrototype, + ), + runner.NewE2ETest( + TestPrecompilesStakingName, + "test stateful precompiled contracts staking", + []runner.ArgDefinition{}, + TestPrecompilesStaking, ), } diff --git a/e2e/e2etests/test_precompiles_prototype.go b/e2e/e2etests/test_precompiles_prototype.go index 642539bc9d..0cbb0a25eb 100644 --- a/e2e/e2etests/test_precompiles_prototype.go +++ b/e2e/e2etests/test_precompiles_prototype.go @@ -8,7 +8,7 @@ import ( "github.com/zeta-chain/node/precompiles/prototype" ) -func TestPrecompilesRegular(r *runner.E2ERunner, args []string) { +func TestPrecompilesPrototype(r *runner.E2ERunner, args []string) { require.Len(r, args, 0, "No arguments expected") iPrototype, err := prototype.NewIPrototype(prototype.ContractAddress, r.ZEVMClient) diff --git a/e2e/e2etests/test_precompiles_staking.go b/e2e/e2etests/test_precompiles_staking.go new file mode 100644 index 0000000000..429c63932a --- /dev/null +++ b/e2e/e2etests/test_precompiles_staking.go @@ -0,0 +1,79 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/precompiles/staking" +) + +func TestPrecompilesStaking(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + stakingContract, err := staking.NewIStaking(staking.ContractAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create staking contract caller") + + previousGasLimit := r.ZEVMAuth.GasLimit + r.ZEVMAuth.GasLimit = 10000000 + defer func() { + r.ZEVMAuth.GasLimit = previousGasLimit + }() + + validators, err := stakingContract.GetAllValidators(&bind.CallOpts{}) + require.NoError(r, err) + require.GreaterOrEqual(r, len(validators), 2) + + // shares are 0 for both validators at the start + sharesBeforeVal1, err := stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[0].OperatorAddress) + require.NoError(r, err) + require.Equal(r, int64(0), sharesBeforeVal1.Int64()) + + sharesBeforeVal2, err := stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[1].OperatorAddress) + require.NoError(r, err) + require.Equal(r, int64(0), sharesBeforeVal2.Int64()) + + // stake 3 to validator1 + tx, err := stakingContract.Stake(r.ZEVMAuth, r.ZEVMAuth.From, validators[0].OperatorAddress, big.NewInt(3)) + require.NoError(r, err) + utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + + // check shares are set to 3 + sharesAfterVal1, err := stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[0].OperatorAddress) + require.NoError(r, err) + require.Equal(r, big.NewInt(3e18).String(), sharesAfterVal1.String()) + + // unstake 1 from validator1 + tx, err = stakingContract.Unstake(r.ZEVMAuth, r.ZEVMAuth.From, validators[0].OperatorAddress, big.NewInt(1)) + require.NoError(r, err) + utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + + // check shares are set to 2 + sharesAfterVal1, err = stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[0].OperatorAddress) + require.NoError(r, err) + require.Equal(r, big.NewInt(2e18).String(), sharesAfterVal1.String()) + + // move 1 stake from validator1 to validator2 + tx, err = stakingContract.MoveStake( + r.ZEVMAuth, + r.ZEVMAuth.From, + validators[0].OperatorAddress, + validators[1].OperatorAddress, + big.NewInt(1), + ) + require.NoError(r, err) + + utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + + // check shares for both validator1 and validator2 are 1 + sharesAfterVal1, err = stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[0].OperatorAddress) + require.NoError(r, err) + require.Equal(r, big.NewInt(1e18).String(), sharesAfterVal1.String()) + + sharesAfterVal2, err := stakingContract.GetShares(&bind.CallOpts{}, r.ZEVMAuth.From, validators[1].OperatorAddress) + require.NoError(r, err) + require.Equal(r, big.NewInt(1e18).String(), sharesAfterVal2.String()) +} diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index 788da22e6d..1adf65005f 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -4,13 +4,15 @@ import ( "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdktypes "github.com/cosmos/cosmos-sdk/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" ethparams "github.com/ethereum/go-ethereum/params" evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" "github.com/zeta-chain/node/precompiles/prototype" - "github.com/zeta-chain/node/x/fungible/keeper" + "github.com/zeta-chain/node/precompiles/staking" + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) // EnabledStatefulContracts contains the list of all enabled stateful precompiles. @@ -18,26 +20,38 @@ import ( // Setting to false a contract here will disable it, not being included in the blockchain. var EnabledStatefulContracts = map[common.Address]bool{ prototype.ContractAddress: true, + staking.ContractAddress: true, } // StatefulContracts returns all the registered precompiled contracts. func StatefulContracts( - fungibleKeeper *keeper.Keeper, + fungibleKeeper *fungiblekeeper.Keeper, + stakingKeeper *stakingkeeper.Keeper, cdc codec.Codec, gasConfig storetypes.GasConfig, ) (precompiledContracts []evmkeeper.CustomContractFn) { // Initialize at 0 the custom compiled contracts and the addresses. precompiledContracts = make([]evmkeeper.CustomContractFn, 0) - // Define the regular contract function. + // Define the prototype contract function. if EnabledStatefulContracts[prototype.ContractAddress] { prototypeContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { return prototype.NewIPrototypeContract(fungibleKeeper, cdc, gasConfig) } - // Append the regular contract to the precompiledContracts slice. + // Append the prototype contract to the precompiledContracts slice. precompiledContracts = append(precompiledContracts, prototypeContract) } + // Define the staking contract function. + if EnabledStatefulContracts[staking.ContractAddress] { + stakingContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { + return staking.NewIStakingContract(stakingKeeper, cdc, gasConfig) + } + + // Append the staking contract to the precompiledContracts slice. + precompiledContracts = append(precompiledContracts, stakingContract) + } + return precompiledContracts } diff --git a/precompiles/precompiles_test.go b/precompiles/precompiles_test.go index 3718177358..998240e644 100644 --- a/precompiles/precompiles_test.go +++ b/precompiles/precompiles_test.go @@ -11,7 +11,7 @@ import ( ) func Test_StatefulContracts(t *testing.T) { - k, ctx, _, _ := keeper.FungibleKeeper(t) + k, ctx, sdkk, _ := keeper.FungibleKeeper(t) gasConfig := storetypes.TransientGasConfig() var encoding ethermint.EncodingConfig @@ -25,15 +25,16 @@ func Test_StatefulContracts(t *testing.T) { } // StatefulContracts() should return all the enabled contracts. - contracts := StatefulContracts(k, appCodec, gasConfig) + contracts := StatefulContracts(k, &sdkk.StakingKeeper, appCodec, gasConfig) require.NotNil(t, contracts, "StatefulContracts() should not return a nil slice") require.Len(t, contracts, expectedContracts, "StatefulContracts() should return all the enabled contracts") - // Extract the contract function from the first contract. - customContractFn := contracts[0] - contract := customContractFn(ctx, ethparams.Rules{}) + for _, customContractFn := range contracts { + // Extract the contract function. + contract := customContractFn(ctx, ethparams.Rules{}) - // Check the contract function returns a valid address. - contractAddr := contract.Address() - require.NotNil(t, contractAddr, "The called contract should have a valid address") + // Check the contract function returns a valid address. + contractAddr := contract.Address() + require.NotNil(t, contractAddr, "The called contract should have a valid address") + } } diff --git a/precompiles/prototype/prototype.go b/precompiles/prototype/prototype.go index 730ee6008c..123885ccb4 100644 --- a/precompiles/prototype/prototype.go +++ b/precompiles/prototype/prototype.go @@ -41,6 +41,7 @@ func initABI() { var methodID [4]byte copy(methodID[:], ABI.Methods[methodName].ID[:4]) switch methodName { + // TODO: https://github.com/zeta-chain/node/issues/2812 case Bech32ToHexAddrMethodName: GasRequiredByMethod[methodID] = 500 case Bech32ifyMethodName: diff --git a/precompiles/prototype/prototype_test.go b/precompiles/prototype/prototype_test.go index 2fe97f5501..b54332efae 100644 --- a/precompiles/prototype/prototype_test.go +++ b/precompiles/prototype/prototype_test.go @@ -296,7 +296,6 @@ func Test_GetGasStabilityPoolBalance(t *testing.T) { ) t.Run("should fail with invalid arguments", func(t *testing.T) { - t.Run("invalid number of arguments", func(t *testing.T) { args := []interface{}{int64(1337), "second argument"} _, err := contract.GetGasStabilityPoolBalance(ctx, &methodID, args) diff --git a/precompiles/staking/IStaking.abi b/precompiles/staking/IStaking.abi new file mode 100644 index 0000000000..f8ee31e062 --- /dev/null +++ b/precompiles/staking/IStaking.abi @@ -0,0 +1,153 @@ +[ + { + "inputs": [], + "name": "getAllValidators", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "operatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "consensusPubKey", + "type": "string" + }, + { + "internalType": "bool", + "name": "jailed", + "type": "bool" + }, + { + "internalType": "enum BondStatus", + "name": "bondStatus", + "type": "uint8" + } + ], + "internalType": "struct Validator[]", + "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" + } +] diff --git a/precompiles/staking/IStaking.go b/precompiles/staking/IStaking.go new file mode 100644 index 0000000000..ec3143f41f --- /dev/null +++ b/precompiles/staking/IStaking.go @@ -0,0 +1,314 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package staking + +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 +) + +// Validator is an auto generated low-level Go binding around an user-defined struct. +type Validator struct { + OperatorAddress string + ConsensusPubKey string + Jailed bool + BondStatus uint8 +} + +// IStakingMetaData contains all meta data concerning the IStaking contract. +var IStakingMetaData = &bind.MetaData{ + ABI: "[{\"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. +// Deprecated: Use IStakingMetaData.ABI instead. +var IStakingABI = IStakingMetaData.ABI + +// IStaking is an auto generated Go binding around an Ethereum contract. +type IStaking struct { + IStakingCaller // Read-only binding to the contract + IStakingTransactor // Write-only binding to the contract + IStakingFilterer // Log filterer for contract events +} + +// IStakingCaller is an auto generated read-only Go binding around an Ethereum contract. +type IStakingCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IStakingTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IStakingTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IStakingFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IStakingFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IStakingSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IStakingSession struct { + Contract *IStaking // 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 +} + +// IStakingCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IStakingCallerSession struct { + Contract *IStakingCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IStakingTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IStakingTransactorSession struct { + Contract *IStakingTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IStakingRaw is an auto generated low-level Go binding around an Ethereum contract. +type IStakingRaw struct { + Contract *IStaking // Generic contract binding to access the raw methods on +} + +// IStakingCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IStakingCallerRaw struct { + Contract *IStakingCaller // Generic read-only contract binding to access the raw methods on +} + +// IStakingTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IStakingTransactorRaw struct { + Contract *IStakingTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIStaking creates a new instance of IStaking, bound to a specific deployed contract. +func NewIStaking(address common.Address, backend bind.ContractBackend) (*IStaking, error) { + contract, err := bindIStaking(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IStaking{IStakingCaller: IStakingCaller{contract: contract}, IStakingTransactor: IStakingTransactor{contract: contract}, IStakingFilterer: IStakingFilterer{contract: contract}}, nil +} + +// NewIStakingCaller creates a new read-only instance of IStaking, bound to a specific deployed contract. +func NewIStakingCaller(address common.Address, caller bind.ContractCaller) (*IStakingCaller, error) { + contract, err := bindIStaking(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IStakingCaller{contract: contract}, nil +} + +// NewIStakingTransactor creates a new write-only instance of IStaking, bound to a specific deployed contract. +func NewIStakingTransactor(address common.Address, transactor bind.ContractTransactor) (*IStakingTransactor, error) { + contract, err := bindIStaking(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IStakingTransactor{contract: contract}, nil +} + +// NewIStakingFilterer creates a new log filterer instance of IStaking, bound to a specific deployed contract. +func NewIStakingFilterer(address common.Address, filterer bind.ContractFilterer) (*IStakingFilterer, error) { + contract, err := bindIStaking(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IStakingFilterer{contract: contract}, nil +} + +// bindIStaking binds a generic wrapper to an already deployed contract. +func bindIStaking(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IStakingMetaData.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 (_IStaking *IStakingRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IStaking.Contract.IStakingCaller.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 (_IStaking *IStakingRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IStaking.Contract.IStakingTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IStaking *IStakingRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IStaking.Contract.IStakingTransactor.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 (_IStaking *IStakingCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IStaking.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 (_IStaking *IStakingTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IStaking.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IStaking *IStakingTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IStaking.Contract.contract.Transact(opts, method, params...) +} + +// GetAllValidators is a free data retrieval call binding the contract method 0xf3513a37. +// +// Solidity: function getAllValidators() view returns((string,string,bool,uint8)[] validators) +func (_IStaking *IStakingCaller) GetAllValidators(opts *bind.CallOpts) ([]Validator, error) { + var out []interface{} + err := _IStaking.contract.Call(opts, &out, "getAllValidators") + + if err != nil { + return *new([]Validator), err + } + + out0 := *abi.ConvertType(out[0], new([]Validator)).(*[]Validator) + + return out0, err + +} + +// GetAllValidators is a free data retrieval call binding the contract method 0xf3513a37. +// +// Solidity: function getAllValidators() view returns((string,string,bool,uint8)[] validators) +func (_IStaking *IStakingSession) GetAllValidators() ([]Validator, error) { + return _IStaking.Contract.GetAllValidators(&_IStaking.CallOpts) +} + +// GetAllValidators is a free data retrieval call binding the contract method 0xf3513a37. +// +// Solidity: function getAllValidators() view returns((string,string,bool,uint8)[] validators) +func (_IStaking *IStakingCallerSession) GetAllValidators() ([]Validator, error) { + return _IStaking.Contract.GetAllValidators(&_IStaking.CallOpts) +} + +// GetShares is a free data retrieval call binding the contract method 0x0d1b3daf. +// +// Solidity: function getShares(address staker, string validator) view returns(uint256 shares) +func (_IStaking *IStakingCaller) GetShares(opts *bind.CallOpts, staker common.Address, validator string) (*big.Int, error) { + var out []interface{} + err := _IStaking.contract.Call(opts, &out, "getShares", staker, validator) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetShares is a free data retrieval call binding the contract method 0x0d1b3daf. +// +// Solidity: function getShares(address staker, string validator) view returns(uint256 shares) +func (_IStaking *IStakingSession) GetShares(staker common.Address, validator string) (*big.Int, error) { + return _IStaking.Contract.GetShares(&_IStaking.CallOpts, staker, validator) +} + +// GetShares is a free data retrieval call binding the contract method 0x0d1b3daf. +// +// Solidity: function getShares(address staker, string validator) view returns(uint256 shares) +func (_IStaking *IStakingCallerSession) GetShares(staker common.Address, validator string) (*big.Int, error) { + return _IStaking.Contract.GetShares(&_IStaking.CallOpts, staker, validator) +} + +// 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) +func (_IStaking *IStakingTransactor) MoveStake(opts *bind.TransactOpts, staker common.Address, validatorSrc string, validatorDst string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.contract.Transact(opts, "moveStake", staker, validatorSrc, validatorDst, 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) +func (_IStaking *IStakingSession) MoveStake(staker common.Address, validatorSrc string, validatorDst string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.MoveStake(&_IStaking.TransactOpts, staker, validatorSrc, validatorDst, 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) +func (_IStaking *IStakingTransactorSession) MoveStake(staker common.Address, validatorSrc string, validatorDst string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.MoveStake(&_IStaking.TransactOpts, staker, validatorSrc, validatorDst, amount) +} + +// Stake is a paid mutator transaction binding the contract method 0x90b8436f. +// +// Solidity: function stake(address staker, string validator, uint256 amount) returns(bool success) +func (_IStaking *IStakingTransactor) Stake(opts *bind.TransactOpts, staker common.Address, validator string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.contract.Transact(opts, "stake", staker, validator, amount) +} + +// Stake is a paid mutator transaction binding the contract method 0x90b8436f. +// +// Solidity: function stake(address staker, string validator, uint256 amount) returns(bool success) +func (_IStaking *IStakingSession) Stake(staker common.Address, validator string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.Stake(&_IStaking.TransactOpts, staker, validator, amount) +} + +// Stake is a paid mutator transaction binding the contract method 0x90b8436f. +// +// Solidity: function stake(address staker, string validator, uint256 amount) returns(bool success) +func (_IStaking *IStakingTransactorSession) Stake(staker common.Address, validator string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.Stake(&_IStaking.TransactOpts, staker, validator, amount) +} + +// Unstake is a paid mutator transaction binding the contract method 0x57c6ea3e. +// +// Solidity: function unstake(address staker, string validator, uint256 amount) returns(int64 completionTime) +func (_IStaking *IStakingTransactor) Unstake(opts *bind.TransactOpts, staker common.Address, validator string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.contract.Transact(opts, "unstake", staker, validator, amount) +} + +// Unstake is a paid mutator transaction binding the contract method 0x57c6ea3e. +// +// Solidity: function unstake(address staker, string validator, uint256 amount) returns(int64 completionTime) +func (_IStaking *IStakingSession) Unstake(staker common.Address, validator string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.Unstake(&_IStaking.TransactOpts, staker, validator, amount) +} + +// Unstake is a paid mutator transaction binding the contract method 0x57c6ea3e. +// +// Solidity: function unstake(address staker, string validator, uint256 amount) returns(int64 completionTime) +func (_IStaking *IStakingTransactorSession) Unstake(staker common.Address, validator string, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.Unstake(&_IStaking.TransactOpts, staker, validator, amount) +} diff --git a/precompiles/staking/IStaking.json b/precompiles/staking/IStaking.json new file mode 100644 index 0000000000..bd89798977 --- /dev/null +++ b/precompiles/staking/IStaking.json @@ -0,0 +1,155 @@ +{ + "abi": [ + { + "inputs": [], + "name": "getAllValidators", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "operatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "consensusPubKey", + "type": "string" + }, + { + "internalType": "bool", + "name": "jailed", + "type": "bool" + }, + { + "internalType": "enum BondStatus", + "name": "bondStatus", + "type": "uint8" + } + ], + "internalType": "struct Validator[]", + "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" + } + ] +} diff --git a/precompiles/staking/IStaking.sol b/precompiles/staking/IStaking.sol new file mode 100644 index 0000000000..7924b296c0 --- /dev/null +++ b/precompiles/staking/IStaking.sol @@ -0,0 +1,70 @@ +pragma solidity ^0.8.26; + +/// @dev The IStaking contract's address. +address constant ISTAKING_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000066; // 102 + +/// @dev The IStaking contract's instance. +IStaking constant ISTAKING_CONTRACT = IStaking( + ISTAKING_PRECOMPILE_ADDRESS +); + +/// @notice Bond status for validator +enum BondStatus { + Unspecified, + Unbonded, + Unbonding, + Bonded +} + +/// @notice Validator info +struct Validator { + string operatorAddress; + string consensusPubKey; + bool jailed; + BondStatus bondStatus; +} + +interface IStaking { + /// @notice Stake coins to validator + /// @param staker Staker address + /// @param validator Validator address + /// @param amount Coins amount + /// @return success Staking success + function stake( + address staker, + string memory validator, + uint256 amount + ) external returns (bool success); + + /// @notice Unstake coins from validator + /// @param staker Staker address + /// @param validator Validator address + /// @param amount Coins amount + /// @return completionTime Time when unstaking is done + function unstake( + address staker, + string memory validator, + uint256 amount + ) external returns (int64 completionTime); + + /// @notice Move coins from validatorSrc to validatorDst + /// @param staker Staker address + /// @param validatorSrc Validator from address + /// @param validatorDst Validator to address + /// @param amount Coins amount + /// @return completionTime Time when stake move is done + function moveStake( + address staker, + string memory validatorSrc, + string memory validatorDst, + uint256 amount + ) external returns (int64 completionTime); + + /// @notice Get all validators + /// @return validators All 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); +} diff --git a/precompiles/staking/bindings.go b/precompiles/staking/bindings.go new file mode 100644 index 0000000000..5e182735be --- /dev/null +++ b/precompiles/staking/bindings.go @@ -0,0 +1,7 @@ +//go:generate sh -c "solc IStaking.sol --combined-json abi | jq '.contracts.\"IStaking.sol:IStaking\"' > IStaking.json" +//go:generate sh -c "cat IStaking.json | jq .abi > IStaking.abi" +//go:generate sh -c "abigen --abi IStaking.abi --pkg staking --type IStaking --out IStaking.go" + +package staking + +var _ Contract diff --git a/precompiles/staking/staking.go b/precompiles/staking/staking.go new file mode 100644 index 0000000000..308a1968f3 --- /dev/null +++ b/precompiles/staking/staking.go @@ -0,0 +1,430 @@ +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" + 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" + + ptypes "github.com/zeta-chain/node/precompiles/types" +) + +// method names +const ( + // write + StakeMethodName = "stake" + UnstakeMethodName = "unstake" + MoveStakeMethodName = "moveStake" + + // read + GetAllValidatorsMethodName = "getAllValidators" + GetSharesMethodName = "getShares" +) + +var ( + ABI abi.ABI + ContractAddress = common.HexToAddress("0x0000000000000000000000000000000000000066") + GasRequiredByMethod = map[[4]byte]uint64{} + ViewMethod = map[[4]byte]bool{} +) + +func init() { + initABI() +} + +func initABI() { + if err := ABI.UnmarshalJSON([]byte(IStakingMetaData.ABI)); err != nil { + panic(err) + } + + GasRequiredByMethod = map[[4]byte]uint64{} + for methodName := range ABI.Methods { + var methodID [4]byte + copy(methodID[:], ABI.Methods[methodName].ID[:4]) + switch methodName { + // TODO: https://github.com/zeta-chain/node/issues/2812 + // 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 + case UnstakeMethodName: + GasRequiredByMethod[methodID] = 10000 + case MoveStakeMethodName: + GasRequiredByMethod[methodID] = 10000 + case GetAllValidatorsMethodName: + GasRequiredByMethod[methodID] = 0 + ViewMethod[methodID] = true + case GetSharesMethodName: + GasRequiredByMethod[methodID] = 0 + ViewMethod[methodID] = true + default: + GasRequiredByMethod[methodID] = 0 + } + } +} + +type Contract struct { + ptypes.BaseContract + + stakingKeeper stakingkeeper.Keeper + cdc codec.Codec + kvGasConfig storetypes.GasConfig +} + +func NewIStakingContract( + stakingKeeper *stakingkeeper.Keeper, + cdc codec.Codec, + kvGasConfig storetypes.GasConfig, +) *Contract { + return &Contract{ + BaseContract: ptypes.NewBaseContract(ContractAddress), + stakingKeeper: *stakingKeeper, + cdc: cdc, + kvGasConfig: kvGasConfig, + } +} + +// Address() is required to implement the PrecompiledContract interface. +func (c *Contract) Address() common.Address { + return ContractAddress +} + +// Abi() is required to implement the PrecompiledContract interface. +func (c *Contract) Abi() abi.ABI { + return ABI +} + +// RequiredGas is required to implement the PrecompiledContract interface. +// The gas has to be calculated deterministically based on the input. +func (c *Contract) RequiredGas(input []byte) uint64 { + // get methodID (first 4 bytes) + var methodID [4]byte + copy(methodID[:], input[:4]) + // base cost to prevent large input size + baseCost := uint64(len(input)) * c.kvGasConfig.WriteCostPerByte + if ViewMethod[methodID] { + baseCost = uint64(len(input)) * c.kvGasConfig.ReadCostPerByte + } + + if requiredGas, ok := GasRequiredByMethod[methodID]; ok { + return requiredGas + baseCost + } + + // Can not happen, but return 0 if the method is not found. + return 0 +} + +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(), + 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], + } + } + + staker, err := sdk.AccAddressFromBech32(sdk.AccAddress(stakerAddress.Bytes()).String()) + if err != nil { + return nil, err + } + + validator, err := sdk.ValAddressFromBech32(validatorAddress) + if err != nil { + return nil, err + } + + delegation := c.stakingKeeper.Delegation(ctx, staker, validator) + shares := big.NewInt(0) + if delegation != nil { + shares = delegation.GetShares().BigInt() + } + + return method.Outputs.Pack(shares) +} + +func (c *Contract) Stake( + ctx sdk.Context, + origin common.Address, + 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 origin != stakerAddress { + return nil, fmt.Errorf("origin 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 + } + + return method.Outputs.Pack(true) +} + +func (c *Contract) Unstake( + ctx sdk.Context, + origin common.Address, + 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 origin != stakerAddress { + return nil, fmt.Errorf("origin 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 + } + + return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) +} + +func (c *Contract) MoveStake( + ctx sdk.Context, + origin common.Address, + 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 origin != stakerAddress { + return nil, fmt.Errorf("origin 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 + } + + 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, _ bool) ([]byte, error) { + method, err := ABI.MethodById(contract.Input[:4]) + if err != nil { + return nil, err + } + + args, err := method.Inputs.Unpack(contract.Input[4:]) + if err != nil { + return nil, err + } + + stateDB := evm.StateDB.(ptypes.ExtStateDB) + + switch method.Name { + case GetAllValidatorsMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.GetAllValidators(ctx, method) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + case GetSharesMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.GetShares(ctx, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + case StakeMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.Stake(ctx, evm.Origin, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + case UnstakeMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.Unstake(ctx, evm.Origin, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + case MoveStakeMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.MoveStake(ctx, evm.Origin, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + + default: + return nil, ptypes.ErrInvalidMethod{ + Method: method.Name, + } + } +} diff --git a/precompiles/staking/staking_test.go b/precompiles/staking/staking_test.go new file mode 100644 index 0000000000..6ae2f1fc3f --- /dev/null +++ b/precompiles/staking/staking_test.go @@ -0,0 +1,1073 @@ +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" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + ethermint "github.com/zeta-chain/ethermint/types" + "github.com/zeta-chain/node/cmd/zetacored/config" + "github.com/zeta-chain/node/testutil/keeper" + "github.com/zeta-chain/node/testutil/sample" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +func setup(t *testing.T) (sdk.Context, *Contract, abi.ABI, keeper.SDKKeepers) { + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + + cdc := keeper.NewCodec() + + db := tmdb.NewMemDB() + stateStore := store.NewCommitMultiStore(db) + sdkKeepers := keeper.NewSDKKeepers(cdc, db, stateStore) + 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") + + return ctx, contract, abi, sdkKeepers +} + +func Test_IStakingContract(t *testing.T) { + _, contract, abi, _ := setup(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, + abi.Methods[MoveStakeMethodName], + "moveStake method should be present in the ABI", + ) + + require.NotNil( + t, + abi.Methods[GetAllValidatorsMethodName], + "getAllValidators method should be present in the ABI", + ) + require.NotNil(t, abi.Methods[GetSharesMethodName], "getShares method should be present in the ABI") + }) + + t.Run("should check gas requirements for methods", func(t *testing.T) { + var method [4]byte + + t.Run("stake", func(t *testing.T) { + // ACT + stake := contract.RequiredGas(abi.Methods[StakeMethodName].ID) + // ASSERT + copy(method[:], abi.Methods[StakeMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + stake, + "stake method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + stake, + ) + }) + + t.Run("unstake", func(t *testing.T) { + // ACT + unstake := contract.RequiredGas(abi.Methods[UnstakeMethodName].ID) + // ASSERT + copy(method[:], abi.Methods[UnstakeMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + unstake, + "unstake method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + unstake, + ) + }) + + t.Run("moveStake", func(t *testing.T) { + // ACT + moveStake := contract.RequiredGas(abi.Methods[MoveStakeMethodName].ID) + // ASSERT + copy(method[:], abi.Methods[MoveStakeMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + moveStake, + "moveStake method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + moveStake, + ) + }) + + t.Run("getAllValidators", func(t *testing.T) { + // ACT + getAllValidators := contract.RequiredGas(abi.Methods[GetAllValidatorsMethodName].ID) + // ASSERT + copy(method[:], abi.Methods[GetAllValidatorsMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.ReadCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + getAllValidators, + "getAllValidators method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + getAllValidators, + ) + }) + + t.Run("getShares", func(t *testing.T) { + // ACT + getShares := contract.RequiredGas(abi.Methods[GetSharesMethodName].ID) + // ASSERT + copy(method[:], abi.Methods[GetSharesMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.ReadCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + getShares, + "getShares method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + getShares, + ) + }) + + t.Run("invalid method", func(t *testing.T) { + // ARRANGE + invalidMethodBytes := []byte("invalidMethod") + // ACT + gasInvalidMethod := contract.RequiredGas(invalidMethodBytes) + // ASSERT + require.Equal( + t, + uint64(0), + gasInvalidMethod, + "invalid method should require %d gas, got %d", + uint64(0), + gasInvalidMethod, + ) + }) + }) +} + +func Test_InvalidMethod(t *testing.T) { + _, _, abi, _ := setup(t) + + _, doNotExist := abi.Methods["invalidMethod"] + require.False(t, doNotExist, "invalidMethod should not be present in the ABI") +} + +func Test_InvalidABI(t *testing.T) { + IStakingMetaData.ABI = "invalid json" + defer func() { + if r := recover(); r != nil { + require.IsType(t, &json.SyntaxError{}, r, "expected error type: json.SyntaxError, got: %T", r) + } + }() + + initABI() +} + +func Test_Stake(t *testing.T) { + ctx, contract, abi, sdkKeepers := setup(t) + methodID := abi.Methods[StakeMethodName] + + t.Run("should fail if validator doesn't exist", func(t *testing.T) { + // ARRANGE + 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()) + + args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // ACT + _, err = contract.Stake(ctx, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should stake", func(t *testing.T) { + // ARRANGE + 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).BigInt()} + + // ACT + _, err = contract.Stake(ctx, stakerAddr, &methodID, args) + + // ASSERT + require.NoError(t, err) + }) + + t.Run("should fail if origin is not staker", func(t *testing.T) { + // ARRANGE + 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()) + + originEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + + args := []interface{}{originEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // ACT + _, err = contract.Stake(ctx, stakerAddr, &methodID, args) + + // ASSERT + require.ErrorContains(t, err, "origin is not staker address") + }) + + t.Run("should fail if staking fails", func(t *testing.T) { + // ARRANGE + 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()) + + stakerAddr := common.BytesToAddress(staker.Bytes()) + + args := []interface{}{stakerEthAddr, validator.OperatorAddress, int64(42)} + + // ACT + _, err := contract.Stake(ctx, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if wrong args amount", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if staker is not eth addr", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if validator is not valid string", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if amount is not int64", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) +} + +func Test_Unstake(t *testing.T) { + ctx, contract, abi, sdkKeepers := setup(t) + methodID := abi.Methods[UnstakeMethodName] + + t.Run("should fail if validator doesn't exist", func(t *testing.T) { + // ARRANGE + 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()) + + args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // ACT + _, err = contract.Unstake(ctx, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should unstake", func(t *testing.T) { + // ARRANGE + 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).BigInt()} + + // stake first + stakeMethodID := abi.Methods[StakeMethodName] + _, err = contract.Stake(ctx, stakerAddr, &stakeMethodID, args) + require.NoError(t, err) + + // ACT + _, err = contract.Unstake(ctx, stakerAddr, &methodID, args) + + // ASSERT + require.NoError(t, err) + }) + + t.Run("should fail if origin is not staker", func(t *testing.T) { + // ARRANGE + 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).BigInt()} + + // stake first + stakeMethodID := abi.Methods[StakeMethodName] + _, err = contract.Stake(ctx, stakerAddr, &stakeMethodID, args) + require.NoError(t, err) + + originEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + + // ACT + _, err = contract.Unstake(ctx, originEthAddr, &methodID, args) + + // ASSERT + require.ErrorContains(t, err, "origin is not staker address") + }) + + t.Run("should fail if no previous staking", func(t *testing.T) { + // ARRANGE + 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).BigInt()} + + // ACT + _, err = contract.Unstake(ctx, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if wrong args amount", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if staker is not eth addr", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if validator is not valid string", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if amount is not int64", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &methodID, args) + + // ASSERT + require.Error(t, err) + }) +} + +func Test_MoveStake(t *testing.T) { + ctx, contract, abi, sdkKeepers := setup(t) + methodID := abi.Methods[MoveStakeMethodName] + + t.Run("should fail if validator dest doesn't exist", func(t *testing.T) { + // ARRANGE + 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()) + + argsStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + // stake to validator src + stakeMethodID := abi.Methods[StakeMethodName] + _, err = contract.Stake(ctx, stakerAddr, &stakeMethodID, argsStake) + require.NoError(t, err) + + argsMoveStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + validatorDest.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + // ACT + _, err = contract.MoveStake(ctx, stakerAddr, &methodID, argsMoveStake) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should move stake", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &stakeMethodID, argsStake) + require.NoError(t, err) + + argsMoveStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + validatorDest.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + // ACT + // move stake to validator dest + _, err = contract.MoveStake(ctx, stakerAddr, &methodID, argsMoveStake) + + // ASSERT + require.NoError(t, err) + }) + + t.Run("should fail if staker is invalid arg", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &stakeMethodID, argsStake) + require.NoError(t, err) + + argsMoveStake := []interface{}{ + 42, + validatorSrc.OperatorAddress, + validatorDest.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + // ACT + _, err = contract.MoveStake(ctx, stakerAddr, &methodID, argsMoveStake) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if validator src is invalid arg", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &stakeMethodID, argsStake) + require.NoError(t, err) + + argsMoveStake := []interface{}{ + stakerEthAddr, + 42, + validatorDest.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + // ACT + _, err = contract.MoveStake(ctx, stakerAddr, &methodID, argsMoveStake) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if validator dest is invalid arg", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &stakeMethodID, argsStake) + require.NoError(t, err) + + argsMoveStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + 42, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + // ACT + _, err = contract.MoveStake(ctx, stakerAddr, &methodID, argsMoveStake) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if amount is invalid arg", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &stakeMethodID, argsStake) + require.NoError(t, err) + + argsMoveStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + validatorDest.OperatorAddress, + coins.AmountOf(config.BaseDenom).Uint64(), + } + + // ACT + _, err = contract.MoveStake(ctx, stakerAddr, &methodID, argsMoveStake) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if wrong args amount", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &stakeMethodID, argsStake) + require.NoError(t, err) + + argsMoveStake := []interface{}{stakerEthAddr, validatorSrc.OperatorAddress, validatorDest.OperatorAddress} + + // ACT + _, err = contract.MoveStake(ctx, stakerAddr, &methodID, argsMoveStake) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if origin is not staker", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &stakeMethodID, argsStake) + require.NoError(t, err) + + argsMoveStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + validatorDest.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + originEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + + // ACT + _, err = contract.MoveStake(ctx, originEthAddr, &methodID, argsMoveStake) + + // ASSERT + require.ErrorContains(t, err, "origin is not staker") + }) +} + +func Test_GetAllValidators(t *testing.T) { + ctx, contract, abi, sdkKeepers := setup(t) + methodID := abi.Methods[GetAllValidatorsMethodName] + + t.Run("should return empty array if validators not set", func(t *testing.T) { + // ACT + validators, err := contract.GetAllValidators(ctx, &methodID) + + // 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 + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // ACT + validators, err := contract.GetAllValidators(ctx, &methodID) + + // ASSERT + require.NoError(t, err) + + res, err := methodID.Outputs.Unpack(validators) + require.NoError(t, err) + + require.NotEmpty(t, res[0]) + }) +} + +func Test_GetShares(t *testing.T) { + ctx, contract, abi, sdkKeepers := setup(t) + methodID := abi.Methods[GetSharesMethodName] + + t.Run("should return stakes", func(t *testing.T) { + // ARRANGE + 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, stakerAddr, &stakeMethodID, stakeArgs) + require.NoError(t, err) + + // ASSERT + args := []interface{}{stakerEthAddr, validator.OperatorAddress} + stakes, err := contract.GetShares(ctx, &methodID, args) + 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 + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + args := []interface{}{stakerEthAddr} + + // ACT + _, err := contract.GetShares(ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if invalid staker arg", func(t *testing.T) { + // ARRANGE + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + args := []interface{}{42, validator.OperatorAddress} + + // ACT + _, err := contract.GetShares(ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if invalid val address", func(t *testing.T) { + // ARRANGE + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + args := []interface{}{stakerEthAddr, staker.String()} + + // ACT + _, err := contract.GetShares(ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) +} diff --git a/scripts/bindings-stateful-precompiles.sh b/scripts/bindings-stateful-precompiles.sh index 3083f8bf43..63fe3aa23e 100755 --- a/scripts/bindings-stateful-precompiles.sh +++ b/scripts/bindings-stateful-precompiles.sh @@ -43,8 +43,10 @@ function bindings() { cd $1 go generate > /dev/null 2>&1 echo "Generated bindings for $1" + cd - > /dev/null 2>&1 } # List of bindings to generate bindings ./precompiles/prototype +bindings ./precompiles/staking