diff --git a/app/app.go b/app/app.go index 3a42d51aff..1c84e937c0 100644 --- a/app/app.go +++ b/app/app.go @@ -541,6 +541,7 @@ func New( &app.FungibleKeeper, app.StakingKeeper, app.BankKeeper, + app.DistrKeeper, appCodec, storetypes.TransientGasConfig(), ), diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index b9d167dbac..4e7778bec2 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -5,6 +5,7 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdktypes "github.com/cosmos/cosmos-sdk/types" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" @@ -31,6 +32,7 @@ func StatefulContracts( fungibleKeeper *fungiblekeeper.Keeper, stakingKeeper *stakingkeeper.Keeper, bankKeeper bankkeeper.Keeper, + distributionKeeper distrkeeper.Keeper, cdc codec.Codec, gasConfig storetypes.GasConfig, ) (precompiledContracts []evmkeeper.CustomContractFn) { @@ -50,7 +52,15 @@ func StatefulContracts( // Define the staking contract function. if EnabledStatefulContracts[staking.ContractAddress] { stakingContract := func(ctx sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { - return staking.NewIStakingContract(ctx, stakingKeeper, *fungibleKeeper, bankKeeper, cdc, gasConfig) + return staking.NewIStakingContract( + ctx, + stakingKeeper, + *fungibleKeeper, + bankKeeper, + distributionKeeper, + cdc, + gasConfig, + ) } // Append the staking contract to the precompiledContracts slice. diff --git a/precompiles/precompiles_test.go b/precompiles/precompiles_test.go index a0b55572ea..b60d65c314 100644 --- a/precompiles/precompiles_test.go +++ b/precompiles/precompiles_test.go @@ -25,7 +25,14 @@ func Test_StatefulContracts(t *testing.T) { } // StatefulContracts() should return all the enabled contracts. - contracts := StatefulContracts(k, &sdkk.StakingKeeper, sdkk.BankKeeper, appCodec, gasConfig) + contracts := StatefulContracts( + k, + &sdkk.StakingKeeper, + sdkk.BankKeeper, + sdkk.DistributionKeeper, + appCodec, + gasConfig, + ) require.NotNil(t, contracts, "StatefulContracts() should not return a nil slice") require.Len(t, contracts, expectedContracts, "StatefulContracts() should return all the enabled contracts") diff --git a/precompiles/staking/IStaking.abi b/precompiles/staking/IStaking.abi index da1a9e6ffc..1573fcc82a 100644 --- a/precompiles/staking/IStaking.abi +++ b/precompiles/staking/IStaking.abi @@ -1,4 +1,29 @@ [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "claim_address", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ClaimedRewards", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -105,6 +130,30 @@ "name": "Unstake", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "claimRewards", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -164,6 +213,61 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + } + ], + "name": "getDelegatorValidators", + "outputs": [ + { + "internalType": "string[]", + "name": "validators", + "type": "string[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "getRewards", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct DecCoin[]", + "name": "rewards", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/precompiles/staking/IStaking.gen.go b/precompiles/staking/IStaking.gen.go index d4f7495d37..8c40b7e634 100644 --- a/precompiles/staking/IStaking.gen.go +++ b/precompiles/staking/IStaking.gen.go @@ -29,6 +29,12 @@ var ( _ = abi.ConvertType ) +// DecCoin is an auto generated low-level Go binding around an user-defined struct. +type DecCoin struct { + Denom string + Amount *big.Int +} + // Validator is an auto generated low-level Go binding around an user-defined struct. type Validator struct { OperatorAddress string @@ -39,7 +45,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\":\"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\"}]", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"claim_address\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"ClaimedRewards\",\"type\":\"event\"},{\"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\":\"delegator\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"claimRewards\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"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\":\"delegator\",\"type\":\"address\"}],\"name\":\"getDelegatorValidators\",\"outputs\":[{\"internalType\":\"string[]\",\"name\":\"validators\",\"type\":\"string[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getRewards\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"denom\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structDecCoin[]\",\"name\":\"rewards\",\"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. @@ -219,6 +225,68 @@ func (_IStaking *IStakingCallerSession) GetAllValidators() ([]Validator, error) return _IStaking.Contract.GetAllValidators(&_IStaking.CallOpts) } +// GetDelegatorValidators is a free data retrieval call binding the contract method 0xb6a216ae. +// +// Solidity: function getDelegatorValidators(address delegator) view returns(string[] validators) +func (_IStaking *IStakingCaller) GetDelegatorValidators(opts *bind.CallOpts, delegator common.Address) ([]string, error) { + var out []interface{} + err := _IStaking.contract.Call(opts, &out, "getDelegatorValidators", delegator) + + if err != nil { + return *new([]string), err + } + + out0 := *abi.ConvertType(out[0], new([]string)).(*[]string) + + return out0, err + +} + +// GetDelegatorValidators is a free data retrieval call binding the contract method 0xb6a216ae. +// +// Solidity: function getDelegatorValidators(address delegator) view returns(string[] validators) +func (_IStaking *IStakingSession) GetDelegatorValidators(delegator common.Address) ([]string, error) { + return _IStaking.Contract.GetDelegatorValidators(&_IStaking.CallOpts, delegator) +} + +// GetDelegatorValidators is a free data retrieval call binding the contract method 0xb6a216ae. +// +// Solidity: function getDelegatorValidators(address delegator) view returns(string[] validators) +func (_IStaking *IStakingCallerSession) GetDelegatorValidators(delegator common.Address) ([]string, error) { + return _IStaking.Contract.GetDelegatorValidators(&_IStaking.CallOpts, delegator) +} + +// GetRewards is a free data retrieval call binding the contract method 0x93428792. +// +// Solidity: function getRewards(address delegator, string validator) view returns((string,uint256)[] rewards) +func (_IStaking *IStakingCaller) GetRewards(opts *bind.CallOpts, delegator common.Address, validator string) ([]DecCoin, error) { + var out []interface{} + err := _IStaking.contract.Call(opts, &out, "getRewards", delegator, validator) + + if err != nil { + return *new([]DecCoin), err + } + + out0 := *abi.ConvertType(out[0], new([]DecCoin)).(*[]DecCoin) + + return out0, err + +} + +// GetRewards is a free data retrieval call binding the contract method 0x93428792. +// +// Solidity: function getRewards(address delegator, string validator) view returns((string,uint256)[] rewards) +func (_IStaking *IStakingSession) GetRewards(delegator common.Address, validator string) ([]DecCoin, error) { + return _IStaking.Contract.GetRewards(&_IStaking.CallOpts, delegator, validator) +} + +// GetRewards is a free data retrieval call binding the contract method 0x93428792. +// +// Solidity: function getRewards(address delegator, string validator) view returns((string,uint256)[] rewards) +func (_IStaking *IStakingCallerSession) GetRewards(delegator common.Address, validator string) ([]DecCoin, error) { + return _IStaking.Contract.GetRewards(&_IStaking.CallOpts, delegator, validator) +} + // GetShares is a free data retrieval call binding the contract method 0x0d1b3daf. // // Solidity: function getShares(address staker, string validator) view returns(uint256 shares) @@ -250,6 +318,27 @@ func (_IStaking *IStakingCallerSession) GetShares(staker common.Address, validat return _IStaking.Contract.GetShares(&_IStaking.CallOpts, staker, validator) } +// ClaimRewards is a paid mutator transaction binding the contract method 0x54dbdc38. +// +// Solidity: function claimRewards(address delegator, string validator) returns(bool success) +func (_IStaking *IStakingTransactor) ClaimRewards(opts *bind.TransactOpts, delegator common.Address, validator string) (*types.Transaction, error) { + return _IStaking.contract.Transact(opts, "claimRewards", delegator, validator) +} + +// ClaimRewards is a paid mutator transaction binding the contract method 0x54dbdc38. +// +// Solidity: function claimRewards(address delegator, string validator) returns(bool success) +func (_IStaking *IStakingSession) ClaimRewards(delegator common.Address, validator string) (*types.Transaction, error) { + return _IStaking.Contract.ClaimRewards(&_IStaking.TransactOpts, delegator, validator) +} + +// ClaimRewards is a paid mutator transaction binding the contract method 0x54dbdc38. +// +// Solidity: function claimRewards(address delegator, string validator) returns(bool success) +func (_IStaking *IStakingTransactorSession) ClaimRewards(delegator common.Address, validator string) (*types.Transaction, error) { + return _IStaking.Contract.ClaimRewards(&_IStaking.TransactOpts, delegator, validator) +} + // Distribute is a paid mutator transaction binding the contract method 0xfb932108. // // Solidity: function distribute(address zrc20, uint256 amount) returns(bool success) @@ -334,6 +423,160 @@ func (_IStaking *IStakingTransactorSession) Unstake(staker common.Address, valid return _IStaking.Contract.Unstake(&_IStaking.TransactOpts, staker, validator, amount) } +// IStakingClaimedRewardsIterator is returned from FilterClaimedRewards and is used to iterate over the raw logs and unpacked data for ClaimedRewards events raised by the IStaking contract. +type IStakingClaimedRewardsIterator struct { + Event *IStakingClaimedRewards // 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 *IStakingClaimedRewardsIterator) 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(IStakingClaimedRewards) + 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(IStakingClaimedRewards) + 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 *IStakingClaimedRewardsIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IStakingClaimedRewardsIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IStakingClaimedRewards represents a ClaimedRewards event raised by the IStaking contract. +type IStakingClaimedRewards struct { + ClaimAddress common.Address + Zrc20Token common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterClaimedRewards is a free log retrieval operation binding the contract event 0x2ef606d064225d24c1514dc94907c134faee1237445c2f63f410cce0852b2054. +// +// Solidity: event ClaimedRewards(address indexed claim_address, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) FilterClaimedRewards(opts *bind.FilterOpts, claim_address []common.Address, zrc20_token []common.Address) (*IStakingClaimedRewardsIterator, error) { + + var claim_addressRule []interface{} + for _, claim_addressItem := range claim_address { + claim_addressRule = append(claim_addressRule, claim_addressItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _IStaking.contract.FilterLogs(opts, "ClaimedRewards", claim_addressRule, zrc20_tokenRule) + if err != nil { + return nil, err + } + return &IStakingClaimedRewardsIterator{contract: _IStaking.contract, event: "ClaimedRewards", logs: logs, sub: sub}, nil +} + +// WatchClaimedRewards is a free log subscription operation binding the contract event 0x2ef606d064225d24c1514dc94907c134faee1237445c2f63f410cce0852b2054. +// +// Solidity: event ClaimedRewards(address indexed claim_address, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) WatchClaimedRewards(opts *bind.WatchOpts, sink chan<- *IStakingClaimedRewards, claim_address []common.Address, zrc20_token []common.Address) (event.Subscription, error) { + + var claim_addressRule []interface{} + for _, claim_addressItem := range claim_address { + claim_addressRule = append(claim_addressRule, claim_addressItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _IStaking.contract.WatchLogs(opts, "ClaimedRewards", claim_addressRule, 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(IStakingClaimedRewards) + if err := _IStaking.contract.UnpackLog(event, "ClaimedRewards", 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 +} + +// ParseClaimedRewards is a log parse operation binding the contract event 0x2ef606d064225d24c1514dc94907c134faee1237445c2f63f410cce0852b2054. +// +// Solidity: event ClaimedRewards(address indexed claim_address, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) ParseClaimedRewards(log types.Log) (*IStakingClaimedRewards, error) { + event := new(IStakingClaimedRewards) + if err := _IStaking.contract.UnpackLog(event, "ClaimedRewards", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // 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 diff --git a/precompiles/staking/IStaking.json b/precompiles/staking/IStaking.json index d4e0bb75f0..e4e3111f7e 100644 --- a/precompiles/staking/IStaking.json +++ b/precompiles/staking/IStaking.json @@ -1,5 +1,30 @@ { "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "claim_address", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ClaimedRewards", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -106,6 +131,30 @@ "name": "Unstake", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "claimRewards", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -165,6 +214,61 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + } + ], + "name": "getDelegatorValidators", + "outputs": [ + { + "internalType": "string[]", + "name": "validators", + "type": "string[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "string", + "name": "validator", + "type": "string" + } + ], + "name": "getRewards", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct DecCoin[]", + "name": "rewards", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/precompiles/staking/IStaking.sol b/precompiles/staking/IStaking.sol index dece711d71..1b433303cf 100644 --- a/precompiles/staking/IStaking.sol +++ b/precompiles/staking/IStaking.sol @@ -23,6 +23,13 @@ struct Validator { BondStatus bondStatus; } +/// @notice Cosmos coin representation.abi +/// ref: https://github.com/cosmos/cosmos-sdk/blob/470e0859462b28a53adb411843539561d11d7bf5/x/distribution/README.md?plain=1#L139 +struct DecCoin { + string denom; + uint256 amount; +} + interface IStaking { /// @notice Stake event is emitted when stake function is called /// @param staker Staker address @@ -66,6 +73,16 @@ interface IStaking { uint256 amount ); + /// @notice ClaimedRewards is emitted when a delegator claims ZRC20. + /// @param claim_address Distributor address. + /// @param zrc20_token ZRC20 token address. + /// @param amount Claimed amount. + event ClaimedRewards( + address indexed claim_address, + address indexed zrc20_token, + uint256 amount + ); + /// @notice Stake coins to validator /// @param staker Staker address /// @param validator Validator address @@ -123,4 +140,28 @@ interface IStaking { address zrc20, uint256 amount ) external returns (bool success); + + /// @notice Claim ZRC20 staking rewards. + /// @param validator The validator address to claim rewards from. + /// @return success Boolean indicating whether the claim was successful. + function claimRewards( + address delegator, + string memory validator + ) external returns (bool success); + + /// @dev Queries all validators the delegator has delegated to. + /// @param delegator The delegator address to query rewards from. + /// @return validators List of the validators the caller has delegated to. + function getDelegatorValidators( + address delegator + ) external view returns (string[] calldata validators); + + /// @notice Query ZRC20 outstanding staking rewards. + /// @param delegator The delegator address to query rewards from. + /// @param validator The validator address to query rewards from. + /// @return rewards The list of coins rewarded on the validator. + function getRewards( + address delegator, + string memory validator + ) external view returns (DecCoin[] calldata rewards); } diff --git a/precompiles/staking/const.go b/precompiles/staking/const.go index 8500e723f4..a12d7070e1 100644 --- a/precompiles/staking/const.go +++ b/precompiles/staking/const.go @@ -1,17 +1,15 @@ package staking const ( + // State changing methods. + ClaimRewardsMethodName = "claimRewards" + ClaimRewardsEventName = "ClaimedRewards" + ClaimRewardsMethodGas = 10000 + DistributeMethodName = "distribute" DistributeEventName = "Distributed" DistributeMethodGas = 10000 - GetAllValidatorsMethodName = "getAllValidators" - GetSharesMethodName = "getShares" - - MoveStakeMethodName = "moveStake" - MoveStakeEventName = "MoveStake" - MoveStakeMethodGas = 10000 - StakeMethodName = "stake" StakeEventName = "Stake" StakeMethodGas = 10000 @@ -19,4 +17,14 @@ const ( UnstakeMethodName = "unstake" UnstakeEventName = "Unstake" UnstakeMethodGas = 1000 + + MoveStakeMethodName = "moveStake" + MoveStakeEventName = "MoveStake" + MoveStakeMethodGas = 10000 + + // Query methods. + GetAllValidatorsMethodName = "getAllValidators" + GetSharesMethodName = "getShares" + GetRewardsMethodName = "getRewards" + GetValidatorsMethodName = "getDelegatorValidators" ) diff --git a/precompiles/staking/logs.go b/precompiles/staking/logs.go index c8d1db24e2..db6d628968 100644 --- a/precompiles/staking/logs.go +++ b/precompiles/staking/logs.go @@ -147,3 +147,33 @@ func (c *Contract) addDistributeLog( return nil } + +func (c *Contract) addClaimRewardsLog( + ctx sdk.Context, + stateDB vm.StateDB, + delegator common.Address, + zrc20Token common.Address, + amount *big.Int, +) error { + event := c.Abi().Events[ClaimRewardsEventName] + + topics, err := logs.MakeTopics( + event, + []interface{}{delegator}, + []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_claim_rewards.go b/precompiles/staking/method_claim_rewards.go new file mode 100644 index 0000000000..1899dabccd --- /dev/null +++ b/precompiles/staking/method_claim_rewards.go @@ -0,0 +1,152 @@ +package staking + +import ( + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/zeta-chain/node/cmd/zetacored/config" + "github.com/zeta-chain/node/precompiles/bank" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +// claimRewards claims all the rewards for a delegator from a validator. +// As F1 Cosmos distribution scheme implements an all or nothing withdrawal, the precompile will +// withdraw all the rewards for the delegator, filter ZRC20 and unlock them to the delegator EVM address. +func (c *Contract) claimRewards( + 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, + } + } + + delegatorAddr, validatorAddr, err := unpackClaimRewardsArgs(args) + if err != nil { + return nil, err + } + + var ( + // This represents the delegator calling directly the precompile. + callerIsDelegator = contract.CallerAddress == delegatorAddr + + // This represents the delegator calling the precompile through a contract. + originIsDelegator = evm.Origin == delegatorAddr + ) + + // If the delegator is not the origin nor the caller, it's an unauthorized operation. + if !callerIsDelegator && !originIsDelegator { + return nil, precompiletypes.ErrInvalidAddr{ + Got: delegatorAddr.String(), + Reason: "unauthorized to withdraw the delegation rewards for delegator", + } + } + + // Get delegator Cosmos address. + delegatorCosmosAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, delegatorAddr) + if err != nil { + return nil, err + } + + // Get validator Cosmos address. + validatorCosmosAddr, err := sdk.ValAddressFromBech32(validatorAddr) + if err != nil { + return nil, err + } + + // Withdraw all the delegation rewards. + // The F1 Cosmos distribution scheme implements an all or nothing withdrawal. + // The coins could be of multiple denomination, and a mix of ZRC20 and Cosmos coins. + coins, err := c.distributionKeeper.WithdrawDelegationRewards(ctx, delegatorCosmosAddr, validatorCosmosAddr) + if err != nil { + return nil, precompiletypes.ErrUnexpected{ + When: "WithdrawDelegationRewards", + Got: err.Error(), + } + } + + // For all the ZRC20 coins withdrawed: + // - Check the amount to unlock is valid. + // - Burn the Cosmos coins. + // - Unlock the ZRC20 coins. + for _, coin := range coins { + // Filter out invalid coins. + if !coin.IsValid() || !coin.Amount.IsPositive() { + continue + } + + // Filter out non-ZRC20 coins. + if !precompiletypes.CoinIsZRC20(coin.Denom) { + continue + } + + // Notice that instead of returning errors we just skip the coin. This is because there might be + // more than one ZRC20 coin in the delegation rewards, and we want to unlock as many as possible. + // Coins are locked in the bank precompile, so it should be possible to unlock them afterwards. + var ( + zrc20Addr = common.HexToAddress(strings.TrimPrefix(coin.Denom, config.ZRC20DenomPrefix)) + zrc20Amount = coin.Amount.BigInt() + ) + + // Check if bank address has enough ZRC20 balance. + // This check is also made inside UnlockZRC20, but repeat it here to avoid burning the coins. + if err := c.fungibleKeeper.CheckZRC20Balance(ctx, zrc20Addr, bank.ContractAddress, zrc20Amount); err != nil { + continue + } + + coinSet := sdk.NewCoins(coin) + + // Send the coins to the fungible module to burn them. + if err := c.bankKeeper.SendCoinsFromAccountToModule(ctx, delegatorCosmosAddr, fungibletypes.ModuleName, coinSet); err != nil { + continue + } + + if err := c.bankKeeper.BurnCoins(ctx, fungibletypes.ModuleName, coinSet); err != nil { + continue + } + + // Finally, unlock the ZRC20 coins. + if err := c.fungibleKeeper.UnlockZRC20(ctx, zrc20Addr, delegatorAddr, bank.ContractAddress, zrc20Amount); err != nil { + continue + } + + // Emit an event per ZRC20 coin unlocked. + // This keeps events as granular and deterministic as possible. + if err := c.addClaimRewardsLog(ctx, evm.StateDB, delegatorAddr, zrc20Addr, zrc20Amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "AddClaimRewardLog", + Got: err.Error(), + } + } + } + + return method.Outputs.Pack(true) +} + +func unpackClaimRewardsArgs(args []interface{}) (delegator common.Address, validator string, err error) { + delegator, ok := args[0].(common.Address) + if !ok { + return common.Address{}, "", &precompiletypes.ErrInvalidAddr{ + Got: delegator.String(), + } + } + + validator, ok = args[1].(string) + if !ok { + return common.Address{}, "", &precompiletypes.ErrInvalidAddr{ + Got: validator, + } + } + + return delegator, validator, nil +} diff --git a/precompiles/staking/method_get_rewards.go b/precompiles/staking/method_get_rewards.go new file mode 100644 index 0000000000..dbbbfff5e4 --- /dev/null +++ b/precompiles/staking/method_get_rewards.go @@ -0,0 +1,85 @@ +package staking + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + dstrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +// getRewards returns the list of ZRC20 cosmos coins, available for withdrawal by the delegator. +func (c *Contract) getRewards( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 2 { + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + } + } + + delegatorAddr, validatorAddr, err := unpackGetRewardsArgs(args) + if err != nil { + return nil, err + } + + // Get delegator Cosmos address. + delegatorCosmosAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, delegatorAddr) + if err != nil { + return nil, err + } + + // Query the delegation rewards through the distribution keeper querier. + dstrClient := distrkeeper.Querier{Keeper: c.distributionKeeper} + + res, err := dstrClient.DelegationRewards(ctx, &dstrtypes.QueryDelegationRewardsRequest{ + DelegatorAddress: delegatorCosmosAddr.String(), + ValidatorAddress: validatorAddr, + }) + if err != nil { + return nil, precompiletypes.ErrUnexpected{ + When: "DelegationRewards", + Got: err.Error(), + } + } + + coins := res.GetRewards() + if !coins.IsValid() { + return nil, precompiletypes.ErrUnexpected{ + When: "GetRewards", + Got: "invalid coins", + } + } + + zrc20Coins := make([]sdk.DecCoin, 0) + for _, coin := range coins { + if precompiletypes.CoinIsZRC20(coin.Denom) { + zrc20Coins = append(zrc20Coins, coin) + } + } + + return method.Outputs.Pack(zrc20Coins) +} + +func unpackGetRewardsArgs(args []interface{}) (delegator common.Address, validator string, err error) { + delegator, ok := args[0].(common.Address) + if !ok { + return common.Address{}, "", &precompiletypes.ErrInvalidAddr{ + Got: delegator.String(), + } + } + + validator, ok = args[1].(string) + if !ok { + return common.Address{}, "", &precompiletypes.ErrInvalidAddr{ + Got: validator, + } + } + + return delegator, validator, nil +} diff --git a/precompiles/staking/method_get_validators.go b/precompiles/staking/method_get_validators.go new file mode 100644 index 0000000000..1351d8c62f --- /dev/null +++ b/precompiles/staking/method_get_validators.go @@ -0,0 +1,64 @@ +package staking + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + dstrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +// getValidators queries the list of validators for a given delegator. +func (c *Contract) getDelegatorValidators( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 0 { + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 0, + } + } + + delegatorAddr, err := unpackGetValidatorsArgs(args) + if err != nil { + return nil, err + } + + // Get the cosmos address of the caller. + delegatorCosmosAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, delegatorAddr) + if err != nil { + return nil, err + } + + // Query the validator list of the given delegator. + dstrClient := distrkeeper.Querier{Keeper: c.distributionKeeper} + + res, err := dstrClient.DelegatorValidators(ctx, &dstrtypes.QueryDelegatorValidatorsRequest{ + DelegatorAddress: delegatorCosmosAddr.String(), + }) + if err != nil { + return nil, precompiletypes.ErrUnexpected{ + When: "DelegatorValidators", + Got: err.Error(), + } + } + + // Return immediately, no need to check the slice. + // If there are no validators we simply return an empty array to calling contracts. + return method.Outputs.Pack(res.Validators) +} + +func unpackGetValidatorsArgs(args []interface{}) (delegator common.Address, err error) { + delegator, ok := args[0].(common.Address) + if !ok { + return common.Address{}, &precompiletypes.ErrInvalidAddr{ + Got: delegator.String(), + } + } + + return delegator, nil +} diff --git a/precompiles/staking/staking.go b/precompiles/staking/staking.go index 4d8115336a..1dd2f1f654 100644 --- a/precompiles/staking/staking.go +++ b/precompiles/staking/staking.go @@ -6,6 +6,7 @@ import ( 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" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -47,12 +48,20 @@ func initABI() { GasRequiredByMethod[methodID] = MoveStakeMethodGas case DistributeMethodName: GasRequiredByMethod[methodID] = DistributeMethodGas + case ClaimRewardsMethodName: + GasRequiredByMethod[methodID] = ClaimRewardsMethodGas case GetAllValidatorsMethodName: GasRequiredByMethod[methodID] = 0 ViewMethod[methodID] = true case GetSharesMethodName: GasRequiredByMethod[methodID] = 0 ViewMethod[methodID] = true + case GetRewardsMethodName: + GasRequiredByMethod[methodID] = 0 + ViewMethod[methodID] = true + case GetValidatorsMethodName: + GasRequiredByMethod[methodID] = 0 + ViewMethod[methodID] = true default: GasRequiredByMethod[methodID] = 0 } @@ -62,11 +71,12 @@ func initABI() { type Contract struct { precompiletypes.BaseContract - stakingKeeper stakingkeeper.Keeper - fungibleKeeper fungiblekeeper.Keeper - bankKeeper bankkeeper.Keeper - cdc codec.Codec - kvGasConfig storetypes.GasConfig + stakingKeeper stakingkeeper.Keeper + fungibleKeeper fungiblekeeper.Keeper + bankKeeper bankkeeper.Keeper + distributionKeeper distrkeeper.Keeper + cdc codec.Codec + kvGasConfig storetypes.GasConfig } func NewIStakingContract( @@ -74,6 +84,7 @@ func NewIStakingContract( stakingKeeper *stakingkeeper.Keeper, fungibleKeeper fungiblekeeper.Keeper, bankKeeper bankkeeper.Keeper, + distributionKeeper distrkeeper.Keeper, cdc codec.Codec, kvGasConfig storetypes.GasConfig, ) *Contract { @@ -83,12 +94,13 @@ func NewIStakingContract( } return &Contract{ - BaseContract: precompiletypes.NewBaseContract(ContractAddress), - stakingKeeper: *stakingKeeper, - fungibleKeeper: fungibleKeeper, - bankKeeper: bankKeeper, - cdc: cdc, - kvGasConfig: kvGasConfig, + BaseContract: precompiletypes.NewBaseContract(ContractAddress), + stakingKeeper: *stakingKeeper, + fungibleKeeper: fungibleKeeper, + bankKeeper: bankKeeper, + distributionKeeper: distributionKeeper, + cdc: cdc, + kvGasConfig: kvGasConfig, } } @@ -228,6 +240,41 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, err } return res, nil + case GetRewardsMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.getRewards(ctx, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + case GetValidatorsMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.getDelegatorValidators(ctx, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + case ClaimRewardsMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.claimRewards(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, precompiletypes.ErrInvalidMethod{ Method: method.Name, diff --git a/precompiles/staking/staking_test.go b/precompiles/staking/staking_test.go index b7b00ede0d..4c58df2fb9 100644 --- a/precompiles/staking/staking_test.go +++ b/precompiles/staking/staking_test.go @@ -240,6 +240,7 @@ func setup(t *testing.T) (sdk.Context, *Contract, abi.ABI, keeper.SDKKeepers, *v &sdkKeepers.StakingKeeper, *fungibleKeeper, sdkKeepers.BankKeeper, + sdkKeepers.DistributionKeeper, appCodec, gasConfig, ) @@ -313,6 +314,7 @@ func newTestSuite(t *testing.T) testSuite { &sdkKeepers.StakingKeeper, *fungibleKeeper, sdkKeepers.BankKeeper, + sdkKeepers.DistributionKeeper, appCodec, gasConfig, ) diff --git a/precompiles/types/coin.go b/precompiles/types/coin.go index 4219040d42..f8f41eb766 100644 --- a/precompiles/types/coin.go +++ b/precompiles/types/coin.go @@ -2,6 +2,7 @@ package types import ( "math/big" + "strings" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -49,3 +50,8 @@ func CreateCoinSet(zrc20address common.Address, amount *big.Int) (sdk.Coins, err return coinSet, nil } + +// CoinIsZRC20 checks if a given coin is a ZRC20 coin based on its denomination. +func CoinIsZRC20(denom string) bool { + return strings.HasPrefix(denom, config.ZRC20DenomPrefix) +} diff --git a/testutil/keeper/keeper.go b/testutil/keeper/keeper.go index ed3a107dac..f3c91910b9 100644 --- a/testutil/keeper/keeper.go +++ b/testutil/keeper/keeper.go @@ -111,6 +111,7 @@ type SDKKeepers struct { TransferKeeper ibctransferkeeper.Keeper ScopedIBCKeeper capabilitykeeper.ScopedKeeper ScopedTransferKeeper capabilitykeeper.ScopedKeeper + DistributionKeeper distrkeeper.Keeper IBCRouter *porttypes.Router } @@ -150,6 +151,22 @@ var ( ) testTransientKeys = sdk.NewTransientStoreKeys(evmtypes.TransientKey) testMemKeys = sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) + + maccPerms = map[string][]string{ + authtypes.FeeCollectorName: nil, + distrtypes.ModuleName: nil, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + govtypes.ModuleName: {authtypes.Burner}, + //ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + crosschaintypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + //ibccrosschaintypes.ModuleName: nil, + evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + fungibletypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + emissionstypes.ModuleName: nil, + emissionstypes.UndistributedObserverRewardsPool: nil, + emissionstypes.UndistributedTSSRewardsPool: nil, + } ) // ModuleAccountAddrs returns all the app's module account addresses. @@ -401,6 +418,14 @@ func NewSDKKeepersWithKeys( tKeys map[string]*storetypes.TransientStoreKey, allKeys map[string]storetypes.StoreKey, ) SDKKeepers { + accountKeeper := authkeeper.NewAccountKeeper( + cdc, + keys[authtypes.StoreKey], + ethermint.ProtoAccount, + maccPerms, + sdk.GetConfig().GetBech32AccountAddrPrefix(), + authtypes.NewModuleAddress(authtypes.ModuleName).String(), + ) paramsKeeper := paramskeeper.NewKeeper( cdc, fungibletypes.Amino, @@ -470,16 +495,26 @@ func NewSDKKeepersWithKeys( keys[capabilitytypes.StoreKey], memKeys[capabilitytypes.MemStoreKey], ) + dstrKeeper := distrkeeper.NewKeeper( + cdc, + keys[distrtypes.StoreKey], + accountKeeper, + bankKeeper, + stakingKeeper, + authtypes.FeeCollectorName, + authtypes.NewModuleAddress(distrtypes.ModuleName).String(), + ) return SDKKeepers{ - ParamsKeeper: paramsKeeper, - AuthKeeper: authKeeper, - BankKeeper: bankKeeper, - StakingKeeper: stakingKeeper, - FeeMarketKeeper: feeMarketKeeper, - EvmKeeper: evmKeeper, - SlashingKeeper: slashingKeeper, - CapabilityKeeper: capabilityKeeper, + ParamsKeeper: paramsKeeper, + AuthKeeper: authKeeper, + BankKeeper: bankKeeper, + StakingKeeper: stakingKeeper, + FeeMarketKeeper: feeMarketKeeper, + EvmKeeper: evmKeeper, + SlashingKeeper: slashingKeeper, + CapabilityKeeper: capabilityKeeper, + DistributionKeeper: dstrKeeper, } }