diff --git a/app/precompiles.go b/app/precompiles.go index d44454d8ab..bf1b9d751e 100644 --- a/app/precompiles.go +++ b/app/precompiles.go @@ -3,17 +3,22 @@ package app import ( putils "github.com/sei-protocol/sei-chain/precompiles/utils" "github.com/sei-protocol/sei-chain/sei-cosmos/client" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec" bankkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/keeper" govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" stakingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/keeper" wasmkeeper "github.com/sei-protocol/sei-chain/sei-wasmd/x/wasm/keeper" + mintkeeper "github.com/sei-protocol/sei-chain/x/mint/keeper" ) type PrecompileKeepers struct { putils.BankKeeper putils.BankMsgServer + putils.BankQuerier putils.EVMKeeper putils.AccountKeeper + putils.AuthQuerier + putils.AuthzQuerier putils.OracleKeeper putils.WasmdKeeper putils.WasmdViewKeeper @@ -21,40 +26,64 @@ type PrecompileKeepers struct { putils.StakingQuerier putils.GovKeeper putils.GovMsgServer + putils.GovQuerier putils.DistributionKeeper + putils.DistributionQuerier + putils.EvidenceQuerier + putils.FeegrantQuerier + putils.MintQuerier + putils.ParamsQuerier + putils.SlashingQuerier + putils.UpgradeQuerier putils.TransferKeeper putils.ClientKeeper putils.ConnectionKeeper putils.ChannelKeeper txConf client.TxConfig + cdc codec.Codec } func NewPrecompileKeepers(a *App) *PrecompileKeepers { return &PrecompileKeepers{ - BankKeeper: a.BankKeeper, - BankMsgServer: bankkeeper.NewMsgServerImpl(a.BankKeeper), - EVMKeeper: &a.EvmKeeper, - AccountKeeper: a.AccountKeeper, - OracleKeeper: a.OracleKeeper, - WasmdKeeper: wasmkeeper.NewDefaultPermissionKeeper(a.WasmKeeper), - WasmdViewKeeper: a.WasmKeeper, - StakingKeeper: stakingkeeper.NewMsgServerImpl(a.StakingKeeper), - StakingQuerier: stakingkeeper.Querier{Keeper: a.StakingKeeper}, - GovKeeper: a.GovKeeper, - GovMsgServer: govkeeper.NewMsgServerImpl(a.GovKeeper), - DistributionKeeper: a.DistrKeeper, - TransferKeeper: a.TransferKeeper, - ClientKeeper: a.IBCKeeper.ClientKeeper, - ConnectionKeeper: a.IBCKeeper.ConnectionKeeper, - ChannelKeeper: a.IBCKeeper.ChannelKeeper, - txConf: a.GetTxConfig(), + BankKeeper: a.BankKeeper, + BankMsgServer: bankkeeper.NewMsgServerImpl(a.BankKeeper), + BankQuerier: a.BankKeeper, + EVMKeeper: &a.EvmKeeper, + AccountKeeper: a.AccountKeeper, + AuthQuerier: a.AccountKeeper, + AuthzQuerier: a.AuthzKeeper, + OracleKeeper: a.OracleKeeper, + WasmdKeeper: wasmkeeper.NewDefaultPermissionKeeper(a.WasmKeeper), + WasmdViewKeeper: a.WasmKeeper, + StakingKeeper: stakingkeeper.NewMsgServerImpl(a.StakingKeeper), + StakingQuerier: stakingkeeper.Querier{Keeper: a.StakingKeeper}, + GovKeeper: a.GovKeeper, + GovMsgServer: govkeeper.NewMsgServerImpl(a.GovKeeper), + GovQuerier: a.GovKeeper, + DistributionKeeper: a.DistrKeeper, + DistributionQuerier: a.DistrKeeper, + EvidenceQuerier: a.EvidenceKeeper, + FeegrantQuerier: a.FeeGrantKeeper, + MintQuerier: mintkeeper.NewQuerier(a.MintKeeper), + ParamsQuerier: a.ParamsKeeper, + SlashingQuerier: a.SlashingKeeper, + UpgradeQuerier: a.UpgradeKeeper, + TransferKeeper: a.TransferKeeper, + ClientKeeper: a.IBCKeeper.ClientKeeper, + ConnectionKeeper: a.IBCKeeper.ConnectionKeeper, + ChannelKeeper: a.IBCKeeper.ChannelKeeper, + txConf: a.GetTxConfig(), + cdc: a.appCodec, } } func (pk *PrecompileKeepers) BankK() putils.BankKeeper { return pk.BankKeeper } func (pk *PrecompileKeepers) BankMS() putils.BankMsgServer { return pk.BankMsgServer } +func (pk *PrecompileKeepers) BankQ() putils.BankQuerier { return pk.BankQuerier } func (pk *PrecompileKeepers) EVMK() putils.EVMKeeper { return pk.EVMKeeper } func (pk *PrecompileKeepers) AccountK() putils.AccountKeeper { return pk.AccountKeeper } +func (pk *PrecompileKeepers) AuthQ() putils.AuthQuerier { return pk.AuthQuerier } +func (pk *PrecompileKeepers) AuthzQ() putils.AuthzQuerier { return pk.AuthzQuerier } func (pk *PrecompileKeepers) OracleK() putils.OracleKeeper { return pk.OracleKeeper } func (pk *PrecompileKeepers) WasmdK() putils.WasmdKeeper { return pk.WasmdKeeper } func (pk *PrecompileKeepers) WasmdVK() putils.WasmdViewKeeper { return pk.WasmdViewKeeper } @@ -62,9 +91,20 @@ func (pk *PrecompileKeepers) StakingK() putils.StakingKeeper { return func (pk *PrecompileKeepers) StakingQ() putils.StakingQuerier { return pk.StakingQuerier } func (pk *PrecompileKeepers) GovK() putils.GovKeeper { return pk.GovKeeper } func (pk *PrecompileKeepers) GovMS() putils.GovMsgServer { return pk.GovMsgServer } +func (pk *PrecompileKeepers) GovQ() putils.GovQuerier { return pk.GovQuerier } func (pk *PrecompileKeepers) DistributionK() putils.DistributionKeeper { return pk.DistributionKeeper } -func (pk *PrecompileKeepers) TransferK() putils.TransferKeeper { return pk.TransferKeeper } -func (pk *PrecompileKeepers) ClientK() putils.ClientKeeper { return pk.ClientKeeper } -func (pk *PrecompileKeepers) ConnectionK() putils.ConnectionKeeper { return pk.ConnectionKeeper } -func (pk *PrecompileKeepers) ChannelK() putils.ChannelKeeper { return pk.ChannelKeeper } -func (pk *PrecompileKeepers) TxConfig() client.TxConfig { return pk.txConf } +func (pk *PrecompileKeepers) DistributionQ() putils.DistributionQuerier { + return pk.DistributionQuerier +} +func (pk *PrecompileKeepers) EvidenceQ() putils.EvidenceQuerier { return pk.EvidenceQuerier } +func (pk *PrecompileKeepers) FeegrantQ() putils.FeegrantQuerier { return pk.FeegrantQuerier } +func (pk *PrecompileKeepers) MintQ() putils.MintQuerier { return pk.MintQuerier } +func (pk *PrecompileKeepers) ParamsQ() putils.ParamsQuerier { return pk.ParamsQuerier } +func (pk *PrecompileKeepers) SlashingQ() putils.SlashingQuerier { return pk.SlashingQuerier } +func (pk *PrecompileKeepers) UpgradeQ() putils.UpgradeQuerier { return pk.UpgradeQuerier } +func (pk *PrecompileKeepers) TransferK() putils.TransferKeeper { return pk.TransferKeeper } +func (pk *PrecompileKeepers) ClientK() putils.ClientKeeper { return pk.ClientKeeper } +func (pk *PrecompileKeepers) ConnectionK() putils.ConnectionKeeper { return pk.ConnectionKeeper } +func (pk *PrecompileKeepers) ChannelK() putils.ChannelKeeper { return pk.ChannelKeeper } +func (pk *PrecompileKeepers) TxConfig() client.TxConfig { return pk.txConf } +func (pk *PrecompileKeepers) Codec() codec.Codec { return pk.cdc } diff --git a/giga/executor/precompiles/failfast.go b/giga/executor/precompiles/failfast.go index 352ac778c9..a90396c173 100644 --- a/giga/executor/precompiles/failfast.go +++ b/giga/executor/precompiles/failfast.go @@ -7,17 +7,25 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" "github.com/sei-protocol/sei-chain/precompiles/addr" + "github.com/sei-protocol/sei-chain/precompiles/auth" + "github.com/sei-protocol/sei-chain/precompiles/authz" "github.com/sei-protocol/sei-chain/precompiles/bank" "github.com/sei-protocol/sei-chain/precompiles/distribution" + "github.com/sei-protocol/sei-chain/precompiles/evidence" + "github.com/sei-protocol/sei-chain/precompiles/feegrant" "github.com/sei-protocol/sei-chain/precompiles/gov" "github.com/sei-protocol/sei-chain/precompiles/ibc" "github.com/sei-protocol/sei-chain/precompiles/json" + "github.com/sei-protocol/sei-chain/precompiles/mint" "github.com/sei-protocol/sei-chain/precompiles/oracle" "github.com/sei-protocol/sei-chain/precompiles/p256" + "github.com/sei-protocol/sei-chain/precompiles/params" "github.com/sei-protocol/sei-chain/precompiles/pointer" "github.com/sei-protocol/sei-chain/precompiles/pointerview" + "github.com/sei-protocol/sei-chain/precompiles/slashing" "github.com/sei-protocol/sei-chain/precompiles/solo" "github.com/sei-protocol/sei-chain/precompiles/staking" + "github.com/sei-protocol/sei-chain/precompiles/upgrade" "github.com/sei-protocol/sei-chain/precompiles/wasmd" ) @@ -35,6 +43,14 @@ var FailFastPrecompileAddresses = []common.Address{ common.HexToAddress(pointer.PointerAddress), common.HexToAddress(solo.SoloAddress), common.HexToAddress(p256.P256VerifyAddress), + common.HexToAddress(auth.AuthAddress), + common.HexToAddress(authz.AuthzAddress), + common.HexToAddress(evidence.EvidenceAddress), + common.HexToAddress(feegrant.FeegrantAddress), + common.HexToAddress(mint.MintAddress), + common.HexToAddress(params.ParamsAddress), + common.HexToAddress(slashing.SlashingAddress), + common.HexToAddress(upgrade.UpgradeAddress), } // InvalidPrecompileCallError is an error type that implements vm.AbortError, diff --git a/precompiles/addr/addr.go b/precompiles/addr/addr.go index bef0ecf447..ec8f435654 100644 --- a/precompiles/addr/addr.go +++ b/precompiles/addr/addr.go @@ -256,7 +256,7 @@ func (p PrecompileExecutor) associateAddresses(ctx sdk.Context, method *abi.Meth func (PrecompileExecutor) IsTransaction(method string) bool { switch method { - case Associate: + case Associate, AssociatePubKey: return true default: return false diff --git a/precompiles/auth/Auth.sol b/precompiles/auth/Auth.sol new file mode 100644 index 0000000000..ea4808f9fb --- /dev/null +++ b/precompiles/auth/Auth.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant AUTH_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000100D; + +IAuth constant AUTH_CONTRACT = IAuth(AUTH_PRECOMPILE_ADDRESS); + +interface IAuth { + // Queries + + /** + * @notice Get account information for the given address + * @param addr The EVM address (must be associated with a Sei address) + * @return account Account details + */ + function account( + address addr + ) external view returns (Account memory account); + + /** + * @notice Get all accounts, paginated + * @param pageKey Pagination key (empty bytes for the first page) + * @return response Accounts response with pagination + */ + function accounts( + bytes memory pageKey + ) external view returns (AccountsResponse memory response); + + /** + * @notice Get the auth module parameters + * @return params Auth module parameters + */ + function params() external view returns (AuthParams memory params); + + /** + * @notice Get the next account number + * @return count The next account number + */ + function nextAccountNumber() external view returns (uint64 count); + + // Structs + + struct Account { + string accountAddress; + uint64 accountNumber; + uint64 sequence; + } + + struct AccountsResponse { + Account[] accounts; + bytes nextKey; + } + + struct AuthParams { + uint64 maxMemoCharacters; + uint64 txSigLimit; + uint64 txSizeCostPerByte; + uint64 sigVerifyCostEd25519; + uint64 sigVerifyCostSecp256k1; + bool disableSeqnoCheck; + } +} diff --git a/precompiles/auth/abi.json b/precompiles/auth/abi.json new file mode 100644 index 0000000000..29e2f4d20f --- /dev/null +++ b/precompiles/auth/abi.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"account","outputs":[{"components":[{"internalType":"string","name":"accountAddress","type":"string"},{"internalType":"uint64","name":"accountNumber","type":"uint64"},{"internalType":"uint64","name":"sequence","type":"uint64"}],"internalType":"struct IAuth.Account","name":"account","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"accounts","outputs":[{"components":[{"components":[{"internalType":"string","name":"accountAddress","type":"string"},{"internalType":"uint64","name":"accountNumber","type":"uint64"},{"internalType":"uint64","name":"sequence","type":"uint64"}],"internalType":"struct IAuth.Account[]","name":"accounts","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IAuth.AccountsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextAccountNumber","outputs":[{"internalType":"uint64","name":"count","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"params","outputs":[{"components":[{"internalType":"uint64","name":"maxMemoCharacters","type":"uint64"},{"internalType":"uint64","name":"txSigLimit","type":"uint64"},{"internalType":"uint64","name":"txSizeCostPerByte","type":"uint64"},{"internalType":"uint64","name":"sigVerifyCostEd25519","type":"uint64"},{"internalType":"uint64","name":"sigVerifyCostSecp256k1","type":"uint64"},{"internalType":"bool","name":"disableSeqnoCheck","type":"bool"}],"internalType":"struct IAuth.AuthParams","name":"params","type":"tuple"}],"stateMutability":"view","type":"function"}] diff --git a/precompiles/auth/auth.go b/precompiles/auth/auth.go new file mode 100644 index 0000000000..4ab9a84259 --- /dev/null +++ b/precompiles/auth/auth.go @@ -0,0 +1,268 @@ +package auth + +import ( + "embed" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" + authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" +) + +const ( + AccountMethod = "account" + AccountsMethod = "accounts" + ParamsMethod = "params" + NextAccountNumberMethod = "nextAccountNumber" +) + +const ( + AuthAddress = "0x000000000000000000000000000000000000100D" +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +type PrecompileExecutor struct { + evmKeeper utils.EVMKeeper + accountKeeper utils.AccountKeeper + authQuerier utils.AuthQuerier + cdc codec.Codec + + AccountID []byte + AccountsID []byte + ParamsID []byte + NextAccountNumberID []byte +} + +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { + newAbi := pcommon.MustGetABI(f, "abi.json") + + p := &PrecompileExecutor{ + evmKeeper: keepers.EVMK(), + accountKeeper: keepers.AccountK(), + authQuerier: keepers.AuthQ(), + cdc: keepers.Codec(), + } + + for name, m := range newAbi.Methods { + switch name { + case AccountMethod: + p.AccountID = m.ID + case AccountsMethod: + p.AccountsID = m.ID + case ParamsMethod: + p.ParamsID = m.ID + case NextAccountNumberMethod: + p.NextAccountNumberID = m.ID + } + } + + return pcommon.NewDynamicGasPrecompile(newAbi, p, common.HexToAddress(AuthAddress), "auth"), nil +} + +// RequiredGas returns the required bare minimum gas to execute the precompile. +func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (e.g. auth's NextAccountNumber increments the global + // account number counter, gov's Tally deletes votes), so run every + // view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } + switch method.Name { + case AccountMethod: + return p.account(ctx, method, args, value) + case AccountsMethod: + return p.accounts(ctx, method, args, value) + case ParamsMethod: + return p.params(ctx, method, args, value) + case NextAccountNumberMethod: + return p.nextAccountNumber(ctx, method, args, value) + } + return +} + +type Account struct { + AccountAddress string + AccountNumber uint64 + Sequence uint64 +} + +type AccountsResponse struct { + Accounts []Account + NextKey []byte +} + +type AuthParams struct { + MaxMemoCharacters uint64 + TxSigLimit uint64 + TxSizeCostPerByte uint64 + SigVerifyCostEd25519 uint64 + SigVerifyCostSecp256k1 uint64 + DisableSeqnoCheck bool +} + +func (p PrecompileExecutor) account(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + seiAddr, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + acc := p.accountKeeper.GetAccount(ctx, seiAddr) + if acc == nil { + return nil, 0, errors.New("account not found") + } + + account := Account{ + AccountAddress: acc.GetAddress().String(), + AccountNumber: acc.GetAccountNumber(), + Sequence: acc.GetSequence(), + } + + bz, err := method.Outputs.Pack(account) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) accounts(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + req := &authtypes.QueryAccountsRequest{ + Pagination: &query.PageRequest{ + Key: args[0].([]byte), + }, + } + + resp, err := p.authQuerier.Accounts(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + res := AccountsResponse{ + Accounts: make([]Account, len(resp.Accounts)), + } + for i, accountAny := range resp.Accounts { + acc, ok := accountAny.GetCachedValue().(authtypes.AccountI) + if !ok || acc == nil { + var unpacked authtypes.AccountI + if err := p.cdc.UnpackAny(accountAny, &unpacked); err != nil { + return nil, 0, err + } + acc = unpacked + } + res.Accounts[i] = Account{ + AccountAddress: acc.GetAddress().String(), + AccountNumber: acc.GetAccountNumber(), + Sequence: acc.GetSequence(), + } + } + if resp.Pagination != nil { + res.NextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(res) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) params(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + + resp, err := p.authQuerier.Params(sdk.WrapSDKContext(ctx), &authtypes.QueryParamsRequest{}) + if err != nil { + return nil, 0, err + } + + params := AuthParams{ + MaxMemoCharacters: resp.Params.MaxMemoCharacters, + TxSigLimit: resp.Params.TxSigLimit, + TxSizeCostPerByte: resp.Params.TxSizeCostPerByte, + SigVerifyCostEd25519: resp.Params.SigVerifyCostED25519, + SigVerifyCostSecp256k1: resp.Params.SigVerifyCostSecp256k1, + DisableSeqnoCheck: resp.Params.DisableSeqnoCheck, + } + + bz, err := method.Outputs.Pack(params) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) nextAccountNumber(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + + // GetNextAccountNumber increments and persists the global counter as a + // side effect; Execute already branched the context, so the write is + // discarded and this method stays a read-only view. + resp, err := p.authQuerier.NextAccountNumber(sdk.WrapSDKContext(ctx), &authtypes.QueryNextAccountNumberRequest{}) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(resp.Count) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper +} + +func (PrecompileExecutor) IsTransaction(string) bool { + return false +} diff --git a/precompiles/auth/auth_test.go b/precompiles/auth/auth_test.go new file mode 100644 index 0000000000..de4ab43153 --- /dev/null +++ b/precompiles/auth/auth_test.go @@ -0,0 +1,166 @@ +package auth_test + +import ( + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/auth" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" + "github.com/stretchr/testify/require" +) + +func TestAccount(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + seiAddr, evmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, seiAddr, evmAddr) + + p, err := auth.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: evmAddr}} + executor := p.GetExecutor().(*auth.PrecompileExecutor) + + method, err := p.ABI.MethodById(executor.AccountID) + require.Nil(t, err) + args, err := method.Inputs.Pack(evmAddr) + require.Nil(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, evmAddr, evmAddr, append(executor.AccountID, args...), 1000000, nil, nil, true, false) + require.Nil(t, err) + + acc := testApp.AccountKeeper.GetAccount(ctx, seiAddr) + require.NotNil(t, acc) + expected, err := method.Outputs.Pack(auth.Account{ + AccountAddress: seiAddr.String(), + AccountNumber: acc.GetAccountNumber(), + Sequence: acc.GetSequence(), + }) + require.Nil(t, err) + require.Equal(t, expected, ret) + + // unassociated address should error + _, unassociatedEvmAddr := testkeeper.MockAddressPair() + args, err = method.Inputs.Pack(unassociatedEvmAddr) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, evmAddr, evmAddr, append(executor.AccountID, args...), 1000000, nil, nil, true, false) + require.NotNil(t, err) + + // associated address without an account should error with "account not found" + missingSeiAddr, missingEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, missingSeiAddr, missingEvmAddr) + missingAcc := testApp.AccountKeeper.GetAccount(ctx, missingSeiAddr) + require.NotNil(t, missingAcc) + testApp.AccountKeeper.RemoveAccount(ctx, missingAcc) + args, err = method.Inputs.Pack(missingEvmAddr) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, evmAddr, evmAddr, append(executor.AccountID, args...), 1000000, nil, nil, true, false) + require.NotNil(t, err) +} + +func TestAccounts(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + seiAddr, evmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, seiAddr, evmAddr) + + p, err := auth.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: evmAddr}} + executor := p.GetExecutor().(*auth.PrecompileExecutor) + + method, err := p.ABI.MethodById(executor.AccountsID) + require.Nil(t, err) + args, err := method.Inputs.Pack([]byte{}) + require.Nil(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, evmAddr, evmAddr, append(executor.AccountsID, args...), 1000000, nil, nil, true, false) + require.Nil(t, err) + + outputs, err := method.Outputs.Unpack(ret) + require.Nil(t, err) + require.Len(t, outputs, 1) + + response := reflect.ValueOf(outputs[0]) + accounts := response.FieldByName("Accounts") + require.True(t, accounts.Len() > 0) + found := false + for i := 0; i < accounts.Len(); i++ { + account := accounts.Index(i) + if account.FieldByName("AccountAddress").String() == seiAddr.String() { + found = true + break + } + } + require.True(t, found) +} + +func TestParams(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + seiAddr, evmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, seiAddr, evmAddr) + + p, err := auth.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: evmAddr}} + executor := p.GetExecutor().(*auth.PrecompileExecutor) + + method, err := p.ABI.MethodById(executor.ParamsID) + require.Nil(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, evmAddr, evmAddr, executor.ParamsID, 1000000, nil, nil, true, false) + require.Nil(t, err) + + params := testApp.AccountKeeper.GetParams(ctx) + expected, err := method.Outputs.Pack(auth.AuthParams{ + MaxMemoCharacters: params.MaxMemoCharacters, + TxSigLimit: params.TxSigLimit, + TxSizeCostPerByte: params.TxSizeCostPerByte, + SigVerifyCostEd25519: params.SigVerifyCostED25519, + SigVerifyCostSecp256k1: params.SigVerifyCostSecp256k1, + DisableSeqnoCheck: params.DisableSeqnoCheck, + }) + require.Nil(t, err) + require.Equal(t, expected, ret) +} + +func TestNextAccountNumber(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + seiAddr, evmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, seiAddr, evmAddr) + + p, err := auth.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: evmAddr}} + executor := p.GetExecutor().(*auth.PrecompileExecutor) + + method, err := p.ABI.MethodById(executor.NextAccountNumberID) + require.Nil(t, err) + queryCount := func() uint64 { + ret, _, err := p.RunAndCalculateGas(&evm, evmAddr, evmAddr, executor.NextAccountNumberID, 1000000, nil, nil, true, false) + require.Nil(t, err) + outputs, err := method.Outputs.Unpack(ret) + require.Nil(t, err) + require.Len(t, outputs, 1) + count, ok := outputs[0].(uint64) + require.True(t, ok) + return count + } + + count := queryCount() + require.True(t, count > 0) + // The query is a view: it must not increment the persisted counter, so + // repeated calls return the same value. + require.Equal(t, count, queryCount()) + // The keeper hands out exactly the number the query reported. + require.Equal(t, count, testApp.AccountKeeper.GetNextAccountNumber(ctx)) +} diff --git a/precompiles/auth/setup.go b/precompiles/auth/setup.go new file mode 100644 index 0000000000..ec88692c1c --- /dev/null +++ b/precompiles/auth/setup.go @@ -0,0 +1,21 @@ +// Code generated by scripts/bump_version; DO NOT EDIT. + +package auth + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/utils" +) + +func GetVersioned(latestUpgrade string, keepers utils.Keepers) utils.VersionedPrecompiles { + return utils.VersionedPrecompiles{ + latestUpgrade: check(NewPrecompile(keepers)), + } +} + +func check(p vm.PrecompiledContract, err error) vm.PrecompiledContract { + if err != nil { + panic(err) + } + return p +} diff --git a/precompiles/auth/versions b/precompiles/auth/versions new file mode 100644 index 0000000000..5ff3284364 --- /dev/null +++ b/precompiles/auth/versions @@ -0,0 +1 @@ +v6.6 diff --git a/precompiles/authz/Authz.sol b/precompiles/authz/Authz.sol new file mode 100644 index 0000000000..e475869aad --- /dev/null +++ b/precompiles/authz/Authz.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant AUTHZ_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000100e; + +IAuthz constant AUTHZ_CONTRACT = IAuthz(AUTHZ_PRECOMPILE_ADDRESS); + +interface IAuthz { + // Queries + + /** + * @notice Get grants between a granter and a grantee, optionally filtered by message type URL + * @param granter The granter's EVM address (must be associated with a Sei address) + * @param grantee The grantee's EVM address (must be associated with a Sei address) + * @param msgTypeUrl The message type URL to filter by (empty string for all) + * @param pageKey Pagination key (empty bytes for the first page) + * @return response Grants response with pagination + */ + function grants( + address granter, + address grantee, + string memory msgTypeUrl, + bytes memory pageKey + ) external view returns (GrantsResponse memory response); + + /** + * @notice Get all grants granted by a granter + * @param granter The granter's EVM address (must be associated with a Sei address) + * @param pageKey Pagination key (empty bytes for the first page) + * @return response Grant authorizations response with pagination + */ + function granterGrants( + address granter, + bytes memory pageKey + ) external view returns (GrantAuthorizationsResponse memory response); + + /** + * @notice Get all grants granted to a grantee + * @param grantee The grantee's EVM address (must be associated with a Sei address) + * @param pageKey Pagination key (empty bytes for the first page) + * @return response Grant authorizations response with pagination + */ + function granteeGrants( + address grantee, + bytes memory pageKey + ) external view returns (GrantAuthorizationsResponse memory response); + + // Structs + + struct Grant { + bytes authorization; + int64 expiration; + } + + struct GrantsResponse { + Grant[] grants; + bytes nextKey; + } + + struct GrantAuthorization { + string granter; + string grantee; + bytes authorization; + int64 expiration; + } + + struct GrantAuthorizationsResponse { + GrantAuthorization[] grants; + bytes nextKey; + } +} diff --git a/precompiles/authz/abi.json b/precompiles/authz/abi.json new file mode 100644 index 0000000000..17e9770acf --- /dev/null +++ b/precompiles/authz/abi.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"grantee","type":"address"},{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"granteeGrants","outputs":[{"components":[{"components":[{"internalType":"string","name":"granter","type":"string"},{"internalType":"string","name":"grantee","type":"string"},{"internalType":"bytes","name":"authorization","type":"bytes"},{"internalType":"int64","name":"expiration","type":"int64"}],"internalType":"struct IAuthz.GrantAuthorization[]","name":"grants","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IAuthz.GrantAuthorizationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"granter","type":"address"},{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"granterGrants","outputs":[{"components":[{"components":[{"internalType":"string","name":"granter","type":"string"},{"internalType":"string","name":"grantee","type":"string"},{"internalType":"bytes","name":"authorization","type":"bytes"},{"internalType":"int64","name":"expiration","type":"int64"}],"internalType":"struct IAuthz.GrantAuthorization[]","name":"grants","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IAuthz.GrantAuthorizationsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"granter","type":"address"},{"internalType":"address","name":"grantee","type":"address"},{"internalType":"string","name":"msgTypeUrl","type":"string"},{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"grants","outputs":[{"components":[{"components":[{"internalType":"bytes","name":"authorization","type":"bytes"},{"internalType":"int64","name":"expiration","type":"int64"}],"internalType":"struct IAuthz.Grant[]","name":"grants","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"internalType":"struct IAuthz.GrantsResponse","name":"response","type":"tuple"}],"stateMutability":"view","type":"function"}] diff --git a/precompiles/authz/authz.go b/precompiles/authz/authz.go new file mode 100644 index 0000000000..30afd111bc --- /dev/null +++ b/precompiles/authz/authz.go @@ -0,0 +1,291 @@ +package authz + +import ( + "embed" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" + authztypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/authz" +) + +const ( + GrantsMethod = "grants" + GranterGrantsMethod = "granterGrants" + GranteeGrantsMethod = "granteeGrants" +) + +const ( + AuthzAddress = "0x000000000000000000000000000000000000100E" +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +type PrecompileExecutor struct { + evmKeeper utils.EVMKeeper + authzQuerier utils.AuthzQuerier + cdc codec.Codec + + GrantsID []byte + GranterGrantsID []byte + GranteeGrantsID []byte +} + +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { + newAbi := pcommon.MustGetABI(f, "abi.json") + + p := &PrecompileExecutor{ + evmKeeper: keepers.EVMK(), + authzQuerier: keepers.AuthzQ(), + cdc: keepers.Codec(), + } + + for name, m := range newAbi.Methods { + switch name { + case GrantsMethod: + p.GrantsID = m.ID + case GranterGrantsMethod: + p.GranterGrantsID = m.ID + case GranteeGrantsMethod: + p.GranteeGrantsID = m.ID + } + } + + return pcommon.NewDynamicGasPrecompile(newAbi, p, common.HexToAddress(AuthzAddress), "authz"), nil +} + +// RequiredGas returns the required bare minimum gas to execute the precompile. +func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (e.g. auth's NextAccountNumber increments the global + // account number counter, gov's Tally deletes votes), so run every + // view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } + switch method.Name { + case GrantsMethod: + return p.grants(ctx, method, args, value) + case GranterGrantsMethod: + return p.granterGrants(ctx, method, args, value) + case GranteeGrantsMethod: + return p.granteeGrants(ctx, method, args, value) + } + return +} + +type Grant struct { + Authorization []byte + Expiration int64 +} + +type GrantsResponse struct { + Grants []Grant + NextKey []byte +} + +type GrantAuthorization struct { + Granter string + Grantee string + Authorization []byte + Expiration int64 +} + +type GrantAuthorizationsResponse struct { + Grants []GrantAuthorization + NextKey []byte +} + +func (p PrecompileExecutor) grants(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 4); err != nil { + return nil, 0, err + } + + granter, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[1], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + msgTypeUrl := args[2].(string) + req := &authztypes.QueryGrantsRequest{ + Granter: granter.String(), + Grantee: grantee.String(), + MsgTypeUrl: msgTypeUrl, + Pagination: &query.PageRequest{ + Key: args[3].([]byte), + }, + } + + resp, err := p.authzQuerier.Grants(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + res := GrantsResponse{ + Grants: make([]Grant, len(resp.Grants)), + } + for i, grant := range resp.Grants { + authorizationJSON, err := p.cdc.MarshalAsJSON(grant.Authorization) + if err != nil { + return nil, 0, err + } + res.Grants[i] = Grant{ + Authorization: authorizationJSON, + Expiration: grant.Expiration.Unix(), + } + } + if resp.Pagination != nil { + res.NextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(res) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) granterGrants(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + granter, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + req := &authztypes.QueryGranterGrantsRequest{ + Granter: granter.String(), + Pagination: &query.PageRequest{ + Key: args[1].([]byte), + }, + } + + resp, err := p.authzQuerier.GranterGrants(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + grants, err := p.convertGrantAuthorizations(resp.Grants) + if err != nil { + return nil, 0, err + } + + res := GrantAuthorizationsResponse{ + Grants: grants, + } + if resp.Pagination != nil { + res.NextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(res) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) granteeGrants(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + req := &authztypes.QueryGranteeGrantsRequest{ + Grantee: grantee.String(), + Pagination: &query.PageRequest{ + Key: args[1].([]byte), + }, + } + + resp, err := p.authzQuerier.GranteeGrants(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + grants, err := p.convertGrantAuthorizations(resp.Grants) + if err != nil { + return nil, 0, err + } + + res := GrantAuthorizationsResponse{ + Grants: grants, + } + if resp.Pagination != nil { + res.NextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(res) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) convertGrantAuthorizations(grants []*authztypes.GrantAuthorization) ([]GrantAuthorization, error) { + res := make([]GrantAuthorization, len(grants)) + for i, grant := range grants { + authorizationJSON, err := p.cdc.MarshalAsJSON(grant.Authorization) + if err != nil { + return nil, err + } + res[i] = GrantAuthorization{ + Granter: grant.Granter, + Grantee: grant.Grantee, + Authorization: authorizationJSON, + Expiration: grant.Expiration.Unix(), + } + } + return res, nil +} + +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper +} + +func (PrecompileExecutor) IsTransaction(string) bool { + return false +} diff --git a/precompiles/authz/authz_test.go b/precompiles/authz/authz_test.go new file mode 100644 index 0000000000..bc0707fc0c --- /dev/null +++ b/precompiles/authz/authz_test.go @@ -0,0 +1,147 @@ +package authz_test + +import ( + "reflect" + "testing" + "time" + + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/authz" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" + "github.com/stretchr/testify/require" +) + +func TestGrants(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + granterSeiAddr, granterEvmAddr := testkeeper.MockAddressPair() + granteeSeiAddr, granteeEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, granterSeiAddr, granterEvmAddr) + k.SetAddressMapping(ctx, granteeSeiAddr, granteeEvmAddr) + + expiration := time.Unix(1893456000, 0).UTC() + authorization := banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000)))) + require.Nil(t, testApp.AuthzKeeper.SaveGrant(ctx, granteeSeiAddr, granterSeiAddr, authorization, expiration)) + + p, err := authz.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: granterEvmAddr}} + executor := p.GetExecutor().(*authz.PrecompileExecutor) + + method, err := p.ABI.MethodById(executor.GrantsID) + require.Nil(t, err) + args, err := method.Inputs.Pack(granterEvmAddr, granteeEvmAddr, "", []byte{}) + require.Nil(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantsID, args...), 1000000, nil, nil, true, false) + require.Nil(t, err) + + outputs, err := method.Outputs.Unpack(ret) + require.Nil(t, err) + require.Len(t, outputs, 1) + + response := reflect.ValueOf(outputs[0]) + grants := response.FieldByName("Grants") + require.Equal(t, 1, grants.Len()) + grant := grants.Index(0) + authorizationJSON := string(grant.FieldByName("Authorization").Bytes()) + require.Contains(t, authorizationJSON, "@type") + require.Contains(t, authorizationJSON, "SendAuthorization") + require.Equal(t, expiration.Unix(), grant.FieldByName("Expiration").Int()) + + // unassociated granter should error + _, unassociatedEvmAddr := testkeeper.MockAddressPair() + args, err = method.Inputs.Pack(unassociatedEvmAddr, granteeEvmAddr, "", []byte{}) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantsID, args...), 1000000, nil, nil, true, false) + require.NotNil(t, err) +} + +func TestGranterGrants(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + granterSeiAddr, granterEvmAddr := testkeeper.MockAddressPair() + granteeSeiAddr, granteeEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, granterSeiAddr, granterEvmAddr) + k.SetAddressMapping(ctx, granteeSeiAddr, granteeEvmAddr) + + expiration := time.Unix(1893456000, 0).UTC() + authorization := banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(2000)))) + require.Nil(t, testApp.AuthzKeeper.SaveGrant(ctx, granteeSeiAddr, granterSeiAddr, authorization, expiration)) + + p, err := authz.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: granterEvmAddr}} + executor := p.GetExecutor().(*authz.PrecompileExecutor) + + method, err := p.ABI.MethodById(executor.GranterGrantsID) + require.Nil(t, err) + args, err := method.Inputs.Pack(granterEvmAddr, []byte{}) + require.Nil(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GranterGrantsID, args...), 1000000, nil, nil, true, false) + require.Nil(t, err) + + outputs, err := method.Outputs.Unpack(ret) + require.Nil(t, err) + require.Len(t, outputs, 1) + + response := reflect.ValueOf(outputs[0]) + grants := response.FieldByName("Grants") + require.Equal(t, 1, grants.Len()) + grant := grants.Index(0) + require.Equal(t, granterSeiAddr.String(), grant.FieldByName("Granter").String()) + require.Equal(t, granteeSeiAddr.String(), grant.FieldByName("Grantee").String()) + authorizationJSON := string(grant.FieldByName("Authorization").Bytes()) + require.Contains(t, authorizationJSON, "@type") + require.Contains(t, authorizationJSON, "SendAuthorization") + require.Equal(t, expiration.Unix(), grant.FieldByName("Expiration").Int()) +} + +func TestGranteeGrants(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + granterSeiAddr, granterEvmAddr := testkeeper.MockAddressPair() + granteeSeiAddr, granteeEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, granterSeiAddr, granterEvmAddr) + k.SetAddressMapping(ctx, granteeSeiAddr, granteeEvmAddr) + + expiration := time.Unix(1893456000, 0).UTC() + authorization := banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(3000)))) + require.Nil(t, testApp.AuthzKeeper.SaveGrant(ctx, granteeSeiAddr, granterSeiAddr, authorization, expiration)) + + p, err := authz.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: granteeEvmAddr}} + executor := p.GetExecutor().(*authz.PrecompileExecutor) + + method, err := p.ABI.MethodById(executor.GranteeGrantsID) + require.Nil(t, err) + args, err := method.Inputs.Pack(granteeEvmAddr, []byte{}) + require.Nil(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, granteeEvmAddr, granteeEvmAddr, append(executor.GranteeGrantsID, args...), 1000000, nil, nil, true, false) + require.Nil(t, err) + + outputs, err := method.Outputs.Unpack(ret) + require.Nil(t, err) + require.Len(t, outputs, 1) + + response := reflect.ValueOf(outputs[0]) + grants := response.FieldByName("Grants") + require.Equal(t, 1, grants.Len()) + grant := grants.Index(0) + require.Equal(t, granterSeiAddr.String(), grant.FieldByName("Granter").String()) + require.Equal(t, granteeSeiAddr.String(), grant.FieldByName("Grantee").String()) + authorizationJSON := string(grant.FieldByName("Authorization").Bytes()) + require.Contains(t, authorizationJSON, "@type") + require.Contains(t, authorizationJSON, "SendAuthorization") + require.Equal(t, expiration.Unix(), grant.FieldByName("Expiration").Int()) +} diff --git a/precompiles/authz/setup.go b/precompiles/authz/setup.go new file mode 100644 index 0000000000..c281c6c991 --- /dev/null +++ b/precompiles/authz/setup.go @@ -0,0 +1,21 @@ +// Code generated by scripts/bump_version; DO NOT EDIT. + +package authz + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/utils" +) + +func GetVersioned(latestUpgrade string, keepers utils.Keepers) utils.VersionedPrecompiles { + return utils.VersionedPrecompiles{ + latestUpgrade: check(NewPrecompile(keepers)), + } +} + +func check(p vm.PrecompiledContract, err error) vm.PrecompiledContract { + if err != nil { + panic(err) + } + return p +} diff --git a/precompiles/authz/versions b/precompiles/authz/versions new file mode 100644 index 0000000000..5ff3284364 --- /dev/null +++ b/precompiles/authz/versions @@ -0,0 +1 @@ +v6.6 diff --git a/precompiles/bank/Bank.sol b/precompiles/bank/Bank.sol index e57a083efe..3436ef854d 100644 --- a/precompiles/bank/Bank.sol +++ b/precompiles/bank/Bank.sol @@ -50,4 +50,48 @@ interface IBank { function supply( string memory denom ) external view returns (uint256 response); + + function spendableBalances( + address acc, + bytes memory pageKey + ) external view returns (Coin[] memory balances, bytes memory nextKey); + + function totalSupply( + bytes memory pageKey + ) external view returns (Coin[] memory supply, bytes memory nextKey); + + struct SendEnabled { + string denom; + bool enabled; + } + + struct Params { + SendEnabled[] sendEnabled; + bool defaultSendEnabled; + } + + function params() external view returns (Params memory params); + + struct DenomUnit { + string denom; + uint32 exponent; + string[] aliases; + } + + struct Metadata { + string description; + DenomUnit[] denomUnits; + string base; + string display; + string name; + string symbol; + } + + function denomMetadata( + string memory denom + ) external view returns (Metadata memory metadata); + + function denomsMetadata( + bytes memory pageKey + ) external view returns (Metadata[] memory metadatas, bytes memory nextKey); } diff --git a/precompiles/bank/abi.json b/precompiles/bank/abi.json index 844ac48943..9c408cdbee 100644 --- a/precompiles/bank/abi.json +++ b/precompiles/bank/abi.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"acc","type":"address"}],"name":"all_balances","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IBank.Coin[]","name":"response","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"acc","type":"address"},{"internalType":"string","name":"denom","type":"string"}],"name":"balance","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"decimals","outputs":[{"internalType":"uint8","name":"response","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"name","outputs":[{"internalType":"string","name":"response","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"fromAddress","type":"address"},{"internalType":"address","name":"toAddress","type":"address"},{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"send","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"toNativeAddress","type":"string"}],"name":"sendNative","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"supply","outputs":[{"internalType":"uint256","name":"response","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"symbol","outputs":[{"internalType":"string","name":"response","type":"string"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"acc","type":"address"}],"name":"all_balances","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IBank.Coin[]","name":"response","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"acc","type":"address"},{"internalType":"string","name":"denom","type":"string"}],"name":"balance","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"decimals","outputs":[{"internalType":"uint8","name":"response","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"denomMetadata","outputs":[{"components":[{"internalType":"string","name":"description","type":"string"},{"components":[{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint32","name":"exponent","type":"uint32"},{"internalType":"string[]","name":"aliases","type":"string[]"}],"internalType":"struct IBank.DenomUnit[]","name":"denomUnits","type":"tuple[]"},{"internalType":"string","name":"base","type":"string"},{"internalType":"string","name":"display","type":"string"},{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"}],"internalType":"struct IBank.Metadata","name":"metadata","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"denomsMetadata","outputs":[{"components":[{"internalType":"string","name":"description","type":"string"},{"components":[{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint32","name":"exponent","type":"uint32"},{"internalType":"string[]","name":"aliases","type":"string[]"}],"internalType":"struct IBank.DenomUnit[]","name":"denomUnits","type":"tuple[]"},{"internalType":"string","name":"base","type":"string"},{"internalType":"string","name":"display","type":"string"},{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"}],"internalType":"struct IBank.Metadata[]","name":"metadatas","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"name","outputs":[{"internalType":"string","name":"response","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"params","outputs":[{"components":[{"components":[{"internalType":"string","name":"denom","type":"string"},{"internalType":"bool","name":"enabled","type":"bool"}],"internalType":"struct IBank.SendEnabled[]","name":"sendEnabled","type":"tuple[]"},{"internalType":"bool","name":"defaultSendEnabled","type":"bool"}],"internalType":"struct IBank.Params","name":"params","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"fromAddress","type":"address"},{"internalType":"address","name":"toAddress","type":"address"},{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"send","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"toNativeAddress","type":"string"}],"name":"sendNative","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"acc","type":"address"},{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"spendableBalances","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IBank.Coin[]","name":"balances","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"supply","outputs":[{"internalType":"uint256","name":"response","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"denom","type":"string"}],"name":"symbol","outputs":[{"internalType":"string","name":"response","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"totalSupply","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IBank.Coin[]","name":"supply","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index ff79766c0e..7478d3284e 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -13,20 +13,26 @@ import ( pcommon "github.com/sei-protocol/sei-chain/precompiles/common" putils "github.com/sei-protocol/sei-chain/precompiles/utils" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" "github.com/sei-protocol/sei-chain/utils" "github.com/sei-protocol/sei-chain/utils/metrics" ) const ( - SendMethod = "send" - SendNativeMethod = "sendNative" - BalanceMethod = "balance" - AllBalancesMethod = "all_balances" - NameMethod = "name" - SymbolMethod = "symbol" - DecimalsMethod = "decimals" - SupplyMethod = "supply" + SendMethod = "send" + SendNativeMethod = "sendNative" + BalanceMethod = "balance" + AllBalancesMethod = "all_balances" + NameMethod = "name" + SymbolMethod = "symbol" + DecimalsMethod = "decimals" + SupplyMethod = "supply" + SpendableBalancesMethod = "spendableBalances" + TotalSupplyMethod = "totalSupply" + ParamsMethod = "params" + DenomMetadataMethod = "denomMetadata" + DenomsMetadataMethod = "denomsMetadata" ) const ( @@ -42,17 +48,23 @@ type PrecompileExecutor struct { accountKeeper putils.AccountKeeper bankKeeper putils.BankKeeper bankMsgServer putils.BankMsgServer + bankQuerier putils.BankQuerier evmKeeper putils.EVMKeeper address common.Address - SendID []byte - SendNativeID []byte - BalanceID []byte - AllBalancesID []byte - NameID []byte - SymbolID []byte - DecimalsID []byte - SupplyID []byte + SendID []byte + SendNativeID []byte + BalanceID []byte + AllBalancesID []byte + NameID []byte + SymbolID []byte + DecimalsID []byte + SupplyID []byte + SpendableBalancesID []byte + TotalSupplyID []byte + ParamsID []byte + DenomMetadataID []byte + DenomsMetadataID []byte } type CoinBalance struct { @@ -60,6 +72,31 @@ type CoinBalance struct { Denom string } +type SendEnabled struct { + Denom string + Enabled bool +} + +type Params struct { + SendEnabled []SendEnabled + DefaultSendEnabled bool +} + +type DenomUnit struct { + Denom string + Exponent uint32 + Aliases []string +} + +type Metadata struct { + Description string + DenomUnits []DenomUnit + Base string + Display string + Name string + Symbol string +} + func GetABI() abi.ABI { return pcommon.MustGetABI(f, "abi.json") } @@ -69,6 +106,7 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error p := &PrecompileExecutor{ bankKeeper: keepers.BankK(), bankMsgServer: keepers.BankMS(), + bankQuerier: keepers.BankQ(), evmKeeper: keepers.EVMK(), accountKeeper: keepers.AccountK(), address: common.HexToAddress(BankAddress), @@ -92,6 +130,16 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error p.DecimalsID = m.ID case SupplyMethod: p.SupplyID = m.ID + case SpendableBalancesMethod: + p.SpendableBalancesID = m.ID + case TotalSupplyMethod: + p.TotalSupplyID = m.ID + case ParamsMethod: + p.ParamsID = m.ID + case DenomMetadataMethod: + p.DenomMetadataID = m.ID + case DenomsMetadataMethod: + p.DenomsMetadataID = m.ID } } @@ -110,6 +158,12 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller err = fmt.Errorf("execution reverted: %v", r) } }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects, so run every view on a branched context and discard + // the writes. + ctx, _ = ctx.CacheContext() + } switch method.Name { case SendMethod: return p.send(ctx, caller, method, args, value, readOnly) @@ -126,7 +180,17 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller case DecimalsMethod: return p.decimals(ctx, method, args, value) case SupplyMethod: + return p.supply(ctx, method, args, value) + case SpendableBalancesMethod: + return p.spendableBalances(ctx, method, args, value) + case TotalSupplyMethod: return p.totalSupply(ctx, method, args, value) + case ParamsMethod: + return p.params(ctx, method, args, value) + case DenomMetadataMethod: + return p.denomMetadata(ctx, method, args, value) + case DenomsMetadataMethod: + return p.denomsMetadata(ctx, method, args, value) } return } @@ -342,7 +406,7 @@ func (p PrecompileExecutor) decimals(ctx sdk.Context, method *abi.Method, _ []in return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err } -func (p PrecompileExecutor) totalSupply(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { +func (p PrecompileExecutor) supply(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { if err := pcommon.ValidateNonPayable(value); err != nil { return nil, 0, err } @@ -357,6 +421,213 @@ func (p PrecompileExecutor) totalSupply(ctx sdk.Context, method *abi.Method, arg return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err } +func (p PrecompileExecutor) spendableBalances(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + seiAddr, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + req := &banktypes.QuerySpendableBalancesRequest{ + Address: seiAddr.String(), + Pagination: &query.PageRequest{ + Key: args[1].([]byte), + }, + } + + resp, err := p.bankQuerier.SpendableBalances(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + balances := make([]CoinBalance, 0, len(resp.Balances)) + for _, coin := range resp.Balances { + balances = append(balances, CoinBalance{ + Amount: coin.Amount.BigInt(), + Denom: coin.Denom, + }) + } + var nextKey []byte + if resp.Pagination != nil { + nextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(balances, nextKey) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) totalSupply(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + req := &banktypes.QueryTotalSupplyRequest{ + Pagination: &query.PageRequest{ + Key: args[0].([]byte), + }, + } + + resp, err := p.bankQuerier.TotalSupply(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + supply := make([]CoinBalance, 0, len(resp.Supply)) + for _, coin := range resp.Supply { + supply = append(supply, CoinBalance{ + Amount: coin.Amount.BigInt(), + Denom: coin.Denom, + }) + } + var nextKey []byte + if resp.Pagination != nil { + nextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(supply, nextKey) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) params(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + + resp, err := p.bankQuerier.Params(sdk.WrapSDKContext(ctx), &banktypes.QueryParamsRequest{}) + if err != nil { + return nil, 0, err + } + + sendEnabled := make([]SendEnabled, 0, len(resp.Params.SendEnabled)) + for _, se := range resp.Params.SendEnabled { + if se == nil { + continue + } + sendEnabled = append(sendEnabled, SendEnabled{ + Denom: se.Denom, + Enabled: se.Enabled, + }) + } + params := Params{ + SendEnabled: sendEnabled, + DefaultSendEnabled: resp.Params.DefaultSendEnabled, + } + + bz, err := method.Outputs.Pack(params) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) denomMetadata(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + req := &banktypes.QueryDenomMetadataRequest{ + Denom: args[0].(string), + } + + resp, err := p.bankQuerier.DenomMetadata(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(convertMetadataToPrecompileType(resp.Metadata)) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) denomsMetadata(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + req := &banktypes.QueryDenomsMetadataRequest{ + Pagination: &query.PageRequest{ + Key: args[0].([]byte), + }, + } + + resp, err := p.bankQuerier.DenomsMetadata(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + metadatas := make([]Metadata, 0, len(resp.Metadatas)) + for _, metadata := range resp.Metadatas { + metadatas = append(metadatas, convertMetadataToPrecompileType(metadata)) + } + var nextKey []byte + if resp.Pagination != nil { + nextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(metadatas, nextKey) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func convertMetadataToPrecompileType(metadata banktypes.Metadata) Metadata { + denomUnits := make([]DenomUnit, 0, len(metadata.DenomUnits)) + for _, denomUnit := range metadata.DenomUnits { + if denomUnit == nil { + continue + } + aliases := denomUnit.Aliases + if aliases == nil { + aliases = []string{} + } + denomUnits = append(denomUnits, DenomUnit{ + Denom: denomUnit.Denom, + Exponent: denomUnit.Exponent, + Aliases: aliases, + }) + } + return Metadata{ + Description: metadata.Description, + DenomUnits: denomUnits, + Base: metadata.Base, + Display: metadata.Display, + Name: metadata.Name, + Symbol: metadata.Symbol, + } +} + func (p PrecompileExecutor) accAddressFromArg(ctx sdk.Context, arg interface{}) (sdk.AccAddress, error) { addr := arg.(common.Address) if addr == (common.Address{}) { diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go index a1cae321e8..240747e0cb 100644 --- a/precompiles/bank/bank_test.go +++ b/precompiles/bank/bank_test.go @@ -397,3 +397,229 @@ func TestAddress(t *testing.T) { require.Nil(t, err) require.Equal(t, common.HexToAddress(bank.BankAddress), p.Address()) } + +func TestSpendableBalancesQuery(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + seiAddr, evmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, seiAddr, evmAddr) + err := k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin("uspendablequery", sdk.NewInt(5000)))) + require.Nil(t, err) + err = k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, seiAddr, sdk.NewCoins(sdk.NewCoin("uspendablequery", sdk.NewInt(5000)))) + require.Nil(t, err) + + p, err := bank.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + spendableBalances, err := p.ABI.MethodById(p.GetExecutor().(*bank.PrecompileExecutor).SpendableBalancesID) + require.Nil(t, err) + args, err := spendableBalances.Inputs.Pack(evmAddr, []byte{}) + require.Nil(t, err) + res, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(p.GetExecutor().(*bank.PrecompileExecutor).SpendableBalancesID, args...), 100000, nil, nil, false, false) + require.Nil(t, err) + outputs, err := spendableBalances.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 2, len(outputs)) + balances := outputs[0].([]struct { + Amount *big.Int `json:"amount"` + Denom string `json:"denom"` + }) + require.Equal(t, 1, len(balances)) + require.Equal(t, bank.CoinBalance{ + Amount: big.NewInt(5000), + Denom: "uspendablequery", + }, bank.CoinBalance(balances[0])) + require.Empty(t, outputs[1].([]byte)) +} + +func TestTotalSupplyQuery(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + err := k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin("utotalsupplyquery", sdk.NewInt(123456)))) + require.Nil(t, err) + + p, err := bank.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + totalSupply, err := p.ABI.MethodById(p.GetExecutor().(*bank.PrecompileExecutor).TotalSupplyID) + require.Nil(t, err) + args, err := totalSupply.Inputs.Pack([]byte{}) + require.Nil(t, err) + res, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(p.GetExecutor().(*bank.PrecompileExecutor).TotalSupplyID, args...), 100000, nil, nil, false, false) + require.Nil(t, err) + outputs, err := totalSupply.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 2, len(outputs)) + supply := outputs[0].([]struct { + Amount *big.Int `json:"amount"` + Denom string `json:"denom"` + }) + found := false + for _, coin := range supply { + if coin.Denom == "utotalsupplyquery" { + found = true + require.Equal(t, big.NewInt(123456), coin.Amount) + } + } + require.True(t, found) +} + +func TestBankParamsQuery(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + p, err := bank.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + params, err := p.ABI.MethodById(p.GetExecutor().(*bank.PrecompileExecutor).ParamsID) + require.Nil(t, err) + res, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, p.GetExecutor().(*bank.PrecompileExecutor).ParamsID, 100000, nil, nil, false, false) + require.Nil(t, err) + outputs, err := params.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 1, len(outputs)) + parsedParams := outputs[0].(struct { + SendEnabled []struct { + Denom string `json:"denom"` + Enabled bool `json:"enabled"` + } `json:"sendEnabled"` + DefaultSendEnabled bool `json:"defaultSendEnabled"` + }) + expectedParams := k.BankKeeper().GetParams(ctx) + require.Equal(t, expectedParams.DefaultSendEnabled, parsedParams.DefaultSendEnabled) + require.Equal(t, len(expectedParams.SendEnabled), len(parsedParams.SendEnabled)) +} + +func TestDenomMetadataQuery(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + metadata := banktypes.Metadata{ + Description: "Test denom metadata", + DenomUnits: []*banktypes.DenomUnit{ + {Denom: "udenommetaquery", Exponent: 0, Aliases: []string{"microdenommetaquery"}}, + {Denom: "denommetaquery", Exponent: 6, Aliases: []string{"DENOMMETAQUERY"}}, + }, + Base: "udenommetaquery", + Display: "denommetaquery", + Name: "Denom Meta Query", + Symbol: "DMQ", + } + k.BankKeeper().SetDenomMetaData(ctx, metadata) + + p, err := bank.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + denomMetadata, err := p.ABI.MethodById(p.GetExecutor().(*bank.PrecompileExecutor).DenomMetadataID) + require.Nil(t, err) + args, err := denomMetadata.Inputs.Pack("udenommetaquery") + require.Nil(t, err) + res, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(p.GetExecutor().(*bank.PrecompileExecutor).DenomMetadataID, args...), 100000, nil, nil, false, false) + require.Nil(t, err) + outputs, err := denomMetadata.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 1, len(outputs)) + parsedMetadata := outputs[0].(struct { + Description string `json:"description"` + DenomUnits []struct { + Denom string `json:"denom"` + Exponent uint32 `json:"exponent"` + Aliases []string `json:"aliases"` + } `json:"denomUnits"` + Base string `json:"base"` + Display string `json:"display"` + Name string `json:"name"` + Symbol string `json:"symbol"` + }) + require.Equal(t, metadata.Description, parsedMetadata.Description) + require.Equal(t, len(metadata.DenomUnits), len(parsedMetadata.DenomUnits)) + for i, du := range metadata.DenomUnits { + require.Equal(t, du.Denom, parsedMetadata.DenomUnits[i].Denom) + require.Equal(t, du.Exponent, parsedMetadata.DenomUnits[i].Exponent) + require.Equal(t, du.Aliases, parsedMetadata.DenomUnits[i].Aliases) + } + require.Equal(t, metadata.Base, parsedMetadata.Base) + require.Equal(t, metadata.Display, parsedMetadata.Display) + require.Equal(t, metadata.Name, parsedMetadata.Name) + require.Equal(t, metadata.Symbol, parsedMetadata.Symbol) + + // querying a denom without metadata should error + args, err = denomMetadata.Inputs.Pack("unosuchdenommeta") + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(p.GetExecutor().(*bank.PrecompileExecutor).DenomMetadataID, args...), 100000, nil, nil, false, false) + require.NotNil(t, err) +} + +func TestDenomsMetadataQuery(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + metadata := banktypes.Metadata{ + Description: "Test denoms metadata", + DenomUnits: []*banktypes.DenomUnit{ + {Denom: "udenomsmetaquery", Exponent: 0, Aliases: []string{"microdenomsmetaquery"}}, + }, + Base: "udenomsmetaquery", + Display: "udenomsmetaquery", + Name: "Denoms Meta Query", + Symbol: "DSMQ", + } + k.BankKeeper().SetDenomMetaData(ctx, metadata) + + p, err := bank.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + denomsMetadata, err := p.ABI.MethodById(p.GetExecutor().(*bank.PrecompileExecutor).DenomsMetadataID) + require.Nil(t, err) + args, err := denomsMetadata.Inputs.Pack([]byte{}) + require.Nil(t, err) + res, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(p.GetExecutor().(*bank.PrecompileExecutor).DenomsMetadataID, args...), 100000, nil, nil, false, false) + require.Nil(t, err) + outputs, err := denomsMetadata.Outputs.Unpack(res) + require.Nil(t, err) + require.Equal(t, 2, len(outputs)) + parsedMetadatas := outputs[0].([]struct { + Description string `json:"description"` + DenomUnits []struct { + Denom string `json:"denom"` + Exponent uint32 `json:"exponent"` + Aliases []string `json:"aliases"` + } `json:"denomUnits"` + Base string `json:"base"` + Display string `json:"display"` + Name string `json:"name"` + Symbol string `json:"symbol"` + }) + found := false + for _, parsedMetadata := range parsedMetadatas { + if parsedMetadata.Base == "udenomsmetaquery" { + found = true + require.Equal(t, metadata.Description, parsedMetadata.Description) + require.Equal(t, metadata.Display, parsedMetadata.Display) + require.Equal(t, metadata.Name, parsedMetadata.Name) + require.Equal(t, metadata.Symbol, parsedMetadata.Symbol) + require.Equal(t, 1, len(parsedMetadata.DenomUnits)) + require.Equal(t, "udenomsmetaquery", parsedMetadata.DenomUnits[0].Denom) + } + } + require.True(t, found) + // all metadata records fit in one page, so the next page key should be empty + require.Empty(t, outputs[1].([]byte)) +} diff --git a/precompiles/distribution/Distribution.sol b/precompiles/distribution/Distribution.sol index 7c7ee8ff85..d9004a1db4 100644 --- a/precompiles/distribution/Distribution.sol +++ b/precompiles/distribution/Distribution.sol @@ -50,6 +50,49 @@ interface IDistr { /// @return rewards Structured data containing all pending rewards function rewards(address delegatorAddress) external view returns (Rewards memory rewards); + /// @notice Gets the distribution module parameters + /// @return params The distribution module parameters + function params() external view returns (DistributionParams memory params); + + /// @notice Gets the outstanding (un-withdrawn) rewards of a validator and all its delegations + /// @param validatorAddress The validator's Sei address (e.g., "seivaloper1...") + /// @return rewards The outstanding rewards of the validator + function validatorOutstandingRewards(string memory validatorAddress) external view returns (Coin[] memory rewards); + + /// @notice Gets the accumulated commission of a validator + /// @param validatorAddress The validator's Sei address (e.g., "seivaloper1...") + /// @return commission The accumulated commission of the validator + function validatorCommission(string memory validatorAddress) external view returns (Coin[] memory commission); + + /// @notice Gets the slash events of a validator within a height range + /// @param validatorAddress The validator's Sei address (e.g., "seivaloper1...") + /// @param startingHeight The starting height to query the slashes from + /// @param endingHeight The ending height to query the slashes to + /// @param pageKey The pagination key from a previous response (empty bytes for the first page) + /// @return slashes The slash events of the validator + /// @return nextKey The pagination key for the next page (empty when exhausted) + function validatorSlashes(string memory validatorAddress, uint64 startingHeight, uint64 endingHeight, bytes memory pageKey) external view returns (Slash[] memory slashes, bytes memory nextKey); + + /// @notice Gets the pending rewards of a delegation + /// @param delegatorAddress The EVM address of the delegator + /// @param validatorAddress The validator's Sei address (e.g., "seivaloper1...") + /// @return rewards The pending rewards accrued by the delegation + function delegationRewards(address delegatorAddress, string memory validatorAddress) external view returns (Coin[] memory rewards); + + /// @notice Gets the validators a delegator is delegating to + /// @param delegatorAddress The EVM address of the delegator + /// @return validators The Sei addresses of the validators + function delegatorValidators(address delegatorAddress) external view returns (string[] memory validators); + + /// @notice Gets the withdraw address of a delegator + /// @param delegatorAddress The EVM address of the delegator + /// @return withdrawAddress The delegator's withdraw address (bech32) + function delegatorWithdrawAddress(address delegatorAddress) external view returns (string memory withdrawAddress); + + /// @notice Gets the coins held by the community pool + /// @return pool The coins in the community pool + function communityPool() external view returns (Coin[] memory pool); + /// @notice Represents a coin/token with amount, decimals, and denomination /// @dev Used to represent various tokens in the Cosmos ecosystem struct Coin { @@ -78,4 +121,24 @@ interface IDistr { /// @notice Total rewards summed across all validators Coin[] total; } + + /// @notice Parameters of the distribution module + struct DistributionParams { + /// @notice The tax rate applied to rewards for the community pool (decimal string) + string communityTax; + /// @notice The base reward rate for block proposers (decimal string) + string baseProposerReward; + /// @notice The bonus reward rate for block proposers (decimal string) + string bonusProposerReward; + /// @notice Whether delegators can set a separate withdraw address + bool withdrawAddrEnabled; + } + + /// @notice Represents a validator slash event + struct Slash { + /// @notice The validator distribution period when the slash occurred + uint64 validatorPeriod; + /// @notice The fraction of the validator's stake that was slashed (decimal string) + string fraction; + } } diff --git a/precompiles/distribution/abi.json b/precompiles/distribution/abi.json index cc5781977c..8a12f1b3a1 100755 --- a/precompiles/distribution/abi.json +++ b/precompiles/distribution/abi.json @@ -1 +1 @@ -[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"DelegationRewardsWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string[]","name":"validators","type":"string[]"},{"indexed":false,"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"MultipleDelegationRewardsWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ValidatorCommissionWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawAddr","type":"address"}],"name":"WithdrawAddressSet","type":"event"},{"inputs":[{"internalType":"address","name":"delegatorAddress","type":"address"}],"name":"rewards","outputs":[{"components":[{"components":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IDistr.Coin[]","name":"coins","type":"tuple[]"},{"internalType":"string","name":"validator_address","type":"string"}],"internalType":"struct IDistr.Reward[]","name":"rewards","type":"tuple[]"},{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IDistr.Coin[]","name":"total","type":"tuple[]"}],"internalType":"struct IDistr.Rewards","name":"rewards","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"withdrawAddr","type":"address"}],"name":"setWithdrawAddress","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"validator","type":"string"}],"name":"withdrawDelegationRewards","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string[]","name":"validators","type":"string[]"}],"name":"withdrawMultipleDelegationRewards","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawValidatorCommission","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"DelegationRewardsWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"string[]","name":"validators","type":"string[]"},{"indexed":false,"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"MultipleDelegationRewardsWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"string","name":"validator","type":"string"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ValidatorCommissionWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"delegator","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawAddr","type":"address"}],"name":"WithdrawAddressSet","type":"event"},{"inputs":[],"name":"communityPool","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IDistr.Coin[]","name":"pool","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegatorAddress","type":"address"},{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"delegationRewards","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IDistr.Coin[]","name":"rewards","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegatorAddress","type":"address"}],"name":"delegatorValidators","outputs":[{"internalType":"string[]","name":"validators","type":"string[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegatorAddress","type":"address"}],"name":"delegatorWithdrawAddress","outputs":[{"internalType":"string","name":"withdrawAddress","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"params","outputs":[{"components":[{"internalType":"string","name":"communityTax","type":"string"},{"internalType":"string","name":"baseProposerReward","type":"string"},{"internalType":"string","name":"bonusProposerReward","type":"string"},{"internalType":"bool","name":"withdrawAddrEnabled","type":"bool"}],"internalType":"struct IDistr.DistributionParams","name":"params","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegatorAddress","type":"address"}],"name":"rewards","outputs":[{"components":[{"components":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IDistr.Coin[]","name":"coins","type":"tuple[]"},{"internalType":"string","name":"validator_address","type":"string"}],"internalType":"struct IDistr.Reward[]","name":"rewards","type":"tuple[]"},{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IDistr.Coin[]","name":"total","type":"tuple[]"}],"internalType":"struct IDistr.Rewards","name":"rewards","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"withdrawAddr","type":"address"}],"name":"setWithdrawAddress","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"validatorCommission","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IDistr.Coin[]","name":"commission","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"}],"name":"validatorOutstandingRewards","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IDistr.Coin[]","name":"rewards","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"validatorAddress","type":"string"},{"internalType":"uint64","name":"startingHeight","type":"uint64"},{"internalType":"uint64","name":"endingHeight","type":"uint64"},{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"validatorSlashes","outputs":[{"components":[{"internalType":"uint64","name":"validatorPeriod","type":"uint64"},{"internalType":"string","name":"fraction","type":"string"}],"internalType":"struct IDistr.Slash[]","name":"slashes","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"validator","type":"string"}],"name":"withdrawDelegationRewards","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string[]","name":"validators","type":"string[]"}],"name":"withdrawMultipleDelegationRewards","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawValidatorCommission","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/precompiles/distribution/distribution.go b/precompiles/distribution/distribution.go index 1489832007..e027b1109a 100644 --- a/precompiles/distribution/distribution.go +++ b/precompiles/distribution/distribution.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" pcommon "github.com/sei-protocol/sei-chain/precompiles/common" "github.com/sei-protocol/sei-chain/precompiles/utils" @@ -26,6 +27,14 @@ const ( WithdrawMultipleDelegationRewardsMethod = "withdrawMultipleDelegationRewards" WithdrawValidatorCommissionMethod = "withdrawValidatorCommission" RewardsMethod = "rewards" + ParamsMethod = "params" + ValidatorOutstandingRewardsMethod = "validatorOutstandingRewards" + ValidatorCommissionMethod = "validatorCommission" + ValidatorSlashesMethod = "validatorSlashes" + DelegationRewardsMethod = "delegationRewards" + DelegatorValidatorsMethod = "delegatorValidators" + DelegatorWithdrawAddressMethod = "delegatorWithdrawAddress" + CommunityPoolMethod = "communityPool" SetWithdrawAddressEvent = "WithdrawAddressSet" DelegationRewardsEvent = "DelegationRewardsWithdrawn" @@ -50,15 +59,24 @@ var ( var f embed.FS type PrecompileExecutor struct { - distrKeeper utils.DistributionKeeper - evmKeeper utils.EVMKeeper - address common.Address + distrKeeper utils.DistributionKeeper + distributionQuerier utils.DistributionQuerier + evmKeeper utils.EVMKeeper + address common.Address SetWithdrawAddrID []byte WithdrawDelegationRewardsID []byte WithdrawMultipleDelegationRewardsID []byte WithdrawValidatorCommissionID []byte RewardsID []byte + ParamsID []byte + ValidatorOutstandingRewardsID []byte + ValidatorCommissionID []byte + ValidatorSlashesID []byte + DelegationRewardsID []byte + DelegatorValidatorsID []byte + DelegatorWithdrawAddressID []byte + CommunityPoolID []byte abi abi.ABI } @@ -67,10 +85,11 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) newAbi := pcommon.MustGetABI(f, "abi.json") p := &PrecompileExecutor{ - distrKeeper: keepers.DistributionK(), - evmKeeper: keepers.EVMK(), - address: common.HexToAddress(DistrAddress), - abi: newAbi, + distrKeeper: keepers.DistributionK(), + distributionQuerier: keepers.DistributionQ(), + evmKeeper: keepers.EVMK(), + address: common.HexToAddress(DistrAddress), + abi: newAbi, } for name, m := range newAbi.Methods { @@ -85,6 +104,22 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) p.WithdrawValidatorCommissionID = m.ID case RewardsMethod: p.RewardsID = m.ID + case ParamsMethod: + p.ParamsID = m.ID + case ValidatorOutstandingRewardsMethod: + p.ValidatorOutstandingRewardsID = m.ID + case ValidatorCommissionMethod: + p.ValidatorCommissionID = m.ID + case ValidatorSlashesMethod: + p.ValidatorSlashesID = m.ID + case DelegationRewardsMethod: + p.DelegationRewardsID = m.ID + case DelegatorValidatorsMethod: + p.DelegatorValidatorsID = m.ID + case DelegatorWithdrawAddressMethod: + p.DelegatorWithdrawAddressID = m.ID + case CommunityPoolMethod: + p.CommunityPoolID = m.ID } } @@ -95,6 +130,12 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller if ctx.EVMPrecompileCalledFromDelegateCall() { return nil, 0, errors.New("cannot delegatecall distr") } + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (the rewards queriers call IncrementValidatorPeriod), + // so run every view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } switch method.Name { case SetWithdrawAddressMethod: if readOnly { @@ -121,6 +162,22 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller return p.withdrawValidatorCommission(ctx, method, caller, evm) case RewardsMethod: return p.rewards(ctx, method, args) + case ParamsMethod: + return p.params(ctx, method, args, value) + case ValidatorOutstandingRewardsMethod: + return p.validatorOutstandingRewards(ctx, method, args, value) + case ValidatorCommissionMethod: + return p.validatorCommission(ctx, method, args, value) + case ValidatorSlashesMethod: + return p.validatorSlashes(ctx, method, args, value) + case DelegationRewardsMethod: + return p.delegationRewards(ctx, method, args, value) + case DelegatorValidatorsMethod: + return p.delegatorValidators(ctx, method, args, value) + case DelegatorWithdrawAddressMethod: + return p.delegatorWithdrawAddress(ctx, method, args, value) + case CommunityPoolMethod: + return p.communityPool(ctx, method, args, value) } return } @@ -129,6 +186,17 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { return p.evmKeeper } +// IsTransaction returns true for methods that mutate state; all other +// distribution methods are views. +func (PrecompileExecutor) IsTransaction(method string) bool { + switch method { + case SetWithdrawAddressMethod, WithdrawDelegationRewardsMethod, WithdrawMultipleDelegationRewardsMethod, WithdrawValidatorCommissionMethod: + return true + default: + return false + } +} + func (p PrecompileExecutor) setWithdrawAddress(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int, evm *vm.EVM) (ret []byte, remainingGas uint64, rerr error) { defer func() { if err := recover(); err != nil { @@ -391,6 +459,304 @@ func getResponseOutput(response *distrtypes.QueryDelegationTotalRewardsResponse) } } +type DistributionParams struct { + CommunityTax string + BaseProposerReward string + BonusProposerReward string + WithdrawAddrEnabled bool +} + +type Slash struct { + ValidatorPeriod uint64 + Fraction string +} + +func decCoinsToCoins(decCoins sdk.DecCoins) []Coin { + coins := make([]Coin, 0, len(decCoins)) + for _, coin := range decCoins { + coins = append(coins, Coin{ + Amount: coin.Amount.BigInt(), + Denom: coin.Denom, + Decimals: big.NewInt(sdk.Precision), + }) + } + return coins +} + +func (p PrecompileExecutor) params(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + + if err := p.validateInput(value, args, 0); err != nil { + rerr = err + return + } + + response, err := p.distributionQuerier.Params(sdk.WrapSDKContext(ctx), &distrtypes.QueryParamsRequest{}) + if err != nil { + rerr = err + return + } + + params := DistributionParams{ + CommunityTax: response.Params.CommunityTax.String(), + BaseProposerReward: response.Params.BaseProposerReward.String(), + BonusProposerReward: response.Params.BonusProposerReward.String(), + WithdrawAddrEnabled: response.Params.WithdrawAddrEnabled, + } + + ret, rerr = method.Outputs.Pack(params) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + +func (p PrecompileExecutor) validatorOutstandingRewards(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + + if err := p.validateInput(value, args, 1); err != nil { + rerr = err + return + } + + req := &distrtypes.QueryValidatorOutstandingRewardsRequest{ + ValidatorAddress: args[0].(string), + } + response, err := p.distributionQuerier.ValidatorOutstandingRewards(sdk.WrapSDKContext(ctx), req) + if err != nil { + rerr = err + return + } + + ret, rerr = method.Outputs.Pack(decCoinsToCoins(response.Rewards.Rewards)) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + +func (p PrecompileExecutor) validatorCommission(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + + if err := p.validateInput(value, args, 1); err != nil { + rerr = err + return + } + + req := &distrtypes.QueryValidatorCommissionRequest{ + ValidatorAddress: args[0].(string), + } + response, err := p.distributionQuerier.ValidatorCommission(sdk.WrapSDKContext(ctx), req) + if err != nil { + rerr = err + return + } + + ret, rerr = method.Outputs.Pack(decCoinsToCoins(response.Commission.Commission)) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + +func (p PrecompileExecutor) validatorSlashes(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + + if err := p.validateInput(value, args, 4); err != nil { + rerr = err + return + } + + req := &distrtypes.QueryValidatorSlashesRequest{ + ValidatorAddress: args[0].(string), + StartingHeight: args[1].(uint64), + EndingHeight: args[2].(uint64), + Pagination: &query.PageRequest{ + Key: args[3].([]byte), + }, + } + response, err := p.distributionQuerier.ValidatorSlashes(sdk.WrapSDKContext(ctx), req) + if err != nil { + rerr = err + return + } + + slashes := make([]Slash, 0, len(response.Slashes)) + for _, slash := range response.Slashes { + slashes = append(slashes, Slash{ + ValidatorPeriod: slash.ValidatorPeriod, + Fraction: slash.Fraction.String(), + }) + } + var nextKey []byte + if response.Pagination != nil { + nextKey = response.Pagination.NextKey + } + + ret, rerr = method.Outputs.Pack(slashes, nextKey) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + +func (p PrecompileExecutor) delegationRewards(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + + if err := p.validateInput(value, args, 2); err != nil { + rerr = err + return + } + + delegator, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + rerr = err + return + } + + req := &distrtypes.QueryDelegationRewardsRequest{ + DelegatorAddress: delegator.String(), + ValidatorAddress: args[1].(string), + } + response, err := p.distributionQuerier.DelegationRewards(sdk.WrapSDKContext(ctx), req) + if err != nil { + rerr = err + return + } + + ret, rerr = method.Outputs.Pack(decCoinsToCoins(response.Rewards)) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + +func (p PrecompileExecutor) delegatorValidators(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + + if err := p.validateInput(value, args, 1); err != nil { + rerr = err + return + } + + delegator, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + rerr = err + return + } + + req := &distrtypes.QueryDelegatorValidatorsRequest{ + DelegatorAddress: delegator.String(), + } + response, err := p.distributionQuerier.DelegatorValidators(sdk.WrapSDKContext(ctx), req) + if err != nil { + rerr = err + return + } + + validators := response.Validators + if validators == nil { + validators = []string{} + } + + ret, rerr = method.Outputs.Pack(validators) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + +func (p PrecompileExecutor) delegatorWithdrawAddress(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + + if err := p.validateInput(value, args, 1); err != nil { + rerr = err + return + } + + delegator, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + rerr = err + return + } + + req := &distrtypes.QueryDelegatorWithdrawAddressRequest{ + DelegatorAddress: delegator.String(), + } + response, err := p.distributionQuerier.DelegatorWithdrawAddress(sdk.WrapSDKContext(ctx), req) + if err != nil { + rerr = err + return + } + + ret, rerr = method.Outputs.Pack(response.WithdrawAddress) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + +func (p PrecompileExecutor) communityPool(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, rerr error) { + defer func() { + if err := recover(); err != nil { + ret = nil + remainingGas = 0 + rerr = fmt.Errorf("%s", err) + return + } + }() + + if err := p.validateInput(value, args, 0); err != nil { + rerr = err + return + } + + response, err := p.distributionQuerier.CommunityPool(sdk.WrapSDKContext(ctx), &distrtypes.QueryCommunityPoolRequest{}) + if err != nil { + rerr = err + return + } + + ret, rerr = method.Outputs.Pack(decCoinsToCoins(response.Pool)) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + return +} + func (p PrecompileExecutor) withdrawValidatorCommission(ctx sdk.Context, method *abi.Method, caller common.Address, evm *vm.EVM) (ret []byte, remainingGas uint64, rerr error) { defer func() { if err := recover(); err != nil { diff --git a/precompiles/distribution/distribution_test.go b/precompiles/distribution/distribution_test.go index 1c7cff7930..48edbc4a09 100644 --- a/precompiles/distribution/distribution_test.go +++ b/precompiles/distribution/distribution_test.go @@ -1314,6 +1314,210 @@ func TestWithdrawValidatorCommission_UnitTest(t *testing.T) { require.True(t, success) } +func runDistrQuery(t *testing.T, ctx sdk.Context, testApp *app.App, p *pcommon.DynamicGasPrecompile, methodID []byte, args ...interface{}) ([]byte, *abitypes.Method) { + k := &testApp.EvmKeeper + stateDb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: stateDb, + } + method, err := p.ABI.MethodById(methodID) + require.Nil(t, err) + inputs, err := method.Inputs.Pack(args...) + require.Nil(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), uint64(1000000), nil, nil, true, false) + require.Nil(t, err) + return ret, method +} + +func TestQueryParams(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + + distrParams := testApp.DistrKeeper.GetParams(ctx) + distrParams.CommunityTax = sdk.NewDecWithPrec(2, 2) + distrParams.BaseProposerReward = sdk.NewDecWithPrec(1, 2) + distrParams.BonusProposerReward = sdk.NewDecWithPrec(4, 2) + distrParams.WithdrawAddrEnabled = true + testApp.DistrKeeper.SetParams(ctx, distrParams) + + p, err := distribution.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + + ret, method := runDistrQuery(t, ctx, testApp, p, p.GetExecutor().(*distribution.PrecompileExecutor).ParamsID) + expected, err := method.Outputs.Pack(distribution.DistributionParams{ + CommunityTax: distrParams.CommunityTax.String(), + BaseProposerReward: distrParams.BaseProposerReward.String(), + BonusProposerReward: distrParams.BonusProposerReward.String(), + WithdrawAddrEnabled: true, + }) + require.Nil(t, err) + require.Equal(t, expected, ret) +} + +func TestQueryCommunityPool(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + + pool := sdk.NewDecCoins(sdk.NewDecCoin("usei", sdk.NewInt(1000))) + feePool := testApp.DistrKeeper.GetFeePool(ctx) + feePool.CommunityPool = pool + testApp.DistrKeeper.SetFeePool(ctx, feePool) + + p, err := distribution.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + + ret, method := runDistrQuery(t, ctx, testApp, p, p.GetExecutor().(*distribution.PrecompileExecutor).CommunityPoolID) + expected, err := method.Outputs.Pack([]distribution.Coin{ + { + Amount: pool[0].Amount.BigInt(), + Denom: "usei", + Decimals: big.NewInt(sdk.Precision), + }, + }) + require.Nil(t, err) + require.Equal(t, expected, ret) +} + +func TestQueryDelegatorWithdrawAddress(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + distrParams := testApp.DistrKeeper.GetParams(ctx) + distrParams.WithdrawAddrEnabled = true + testApp.DistrKeeper.SetParams(ctx, distrParams) + + delegatorSeiAddr, delegatorEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, delegatorSeiAddr, delegatorEvmAddr) + withdrawSeiAddr, _ := testkeeper.MockAddressPair() + require.Nil(t, testApp.DistrKeeper.SetWithdrawAddr(ctx, delegatorSeiAddr, withdrawSeiAddr)) + + p, err := distribution.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + + ret, method := runDistrQuery(t, ctx, testApp, p, p.GetExecutor().(*distribution.PrecompileExecutor).DelegatorWithdrawAddressID, delegatorEvmAddr) + expected, err := method.Outputs.Pack(withdrawSeiAddr.String()) + require.Nil(t, err) + require.Equal(t, expected, ret) +} + +func TestQueryDelegationRewards(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + valPub := secp256k1.GenPrivKey().PubKey() + val := setupValidator(t, ctx, testApp, stakingtypes.Bonded, valPub) + + delegatorSeiAddr, delegatorEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, delegatorSeiAddr, delegatorEvmAddr) + + bondDenom := testApp.StakingKeeper.GetParams(ctx).BondDenom + delegationAmount := sdk.NewInt(100) + coins := sdk.NewCoins(sdk.NewCoin(bondDenom, delegationAmount)) + require.Nil(t, testApp.BankKeeper.MintCoins(ctx, minttypes.ModuleName, coins)) + require.Nil(t, testApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, delegatorSeiAddr, coins)) + + validator, found := testApp.StakingKeeper.GetValidator(ctx, val) + require.True(t, found) + _, err := testApp.StakingKeeper.Delegate(ctx, delegatorSeiAddr, delegationAmount, stakingtypes.Unbonded, validator, true) + require.Nil(t, err) + + // allocate 100 tokens of rewards to the validator; the validator's self bond (100) + // and the delegation (100) each own half of the shares, and the validator takes a + // 5% commission (teststaking.DefaultCommission) + validator, found = testApp.StakingKeeper.GetValidator(ctx, val) + require.True(t, found) + testApp.DistrKeeper.AllocateTokensToValidator(ctx, validator, sdk.NewDecCoins(sdk.NewDecCoin(bondDenom, sdk.NewInt(100)))) + + // advance a block so the delegation is no longer considered to have started at + // the current height (delegations accrue no rewards within their starting block) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + p, err := distribution.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + executor := p.GetExecutor().(*distribution.PrecompileExecutor) + + // The rewards queriers call IncrementValidatorPeriod internally; the views + // must run on a branched context so the period bump is discarded. + periodBefore := testApp.DistrKeeper.GetValidatorCurrentRewards(ctx, val).Period + rewardsStateDb := state.NewDBImpl(ctx, k, true) + rewardsEvm := vm.EVM{StateDB: rewardsStateDb} + for _, q := range []struct { + methodID []byte + args []interface{} + }{ + {executor.DelegationRewardsID, []interface{}{delegatorEvmAddr, val.String()}}, + {executor.RewardsID, []interface{}{delegatorEvmAddr}}, + } { + qMethod, err := p.ABI.MethodById(q.methodID) + require.Nil(t, err) + qInputs, err := qMethod.Inputs.Pack(q.args...) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&rewardsEvm, common.Address{}, common.Address{}, append(qMethod.ID, qInputs...), uint64(1000000), nil, nil, true, false) + require.Nil(t, err) + } + require.Equal(t, periodBefore, testApp.DistrKeeper.GetValidatorCurrentRewards(rewardsStateDb.Ctx(), val).Period) + + // delegationRewards: (100 - 5% commission) / 2 = 47.5 + ret, method := runDistrQuery(t, ctx, testApp, p, executor.DelegationRewardsID, delegatorEvmAddr, val.String()) + expected, err := method.Outputs.Pack([]distribution.Coin{ + { + Amount: sdk.NewDecWithPrec(475, 1).BigInt(), + Denom: bondDenom, + Decimals: big.NewInt(sdk.Precision), + }, + }) + require.Nil(t, err) + require.Equal(t, expected, ret) + + // validatorOutstandingRewards: the full 100 tokens are outstanding + ret, method = runDistrQuery(t, ctx, testApp, p, executor.ValidatorOutstandingRewardsID, val.String()) + expected, err = method.Outputs.Pack([]distribution.Coin{ + { + Amount: sdk.NewDec(100).BigInt(), + Denom: bondDenom, + Decimals: big.NewInt(sdk.Precision), + }, + }) + require.Nil(t, err) + require.Equal(t, expected, ret) + + // validatorCommission: 5% of 100 = 5 + ret, method = runDistrQuery(t, ctx, testApp, p, executor.ValidatorCommissionID, val.String()) + expected, err = method.Outputs.Pack([]distribution.Coin{ + { + Amount: sdk.NewDec(5).BigInt(), + Denom: bondDenom, + Decimals: big.NewInt(sdk.Precision), + }, + }) + require.Nil(t, err) + require.Equal(t, expected, ret) + + // delegatorValidators: the delegator only delegates to val + ret, method = runDistrQuery(t, ctx, testApp, p, executor.DelegatorValidatorsID, delegatorEvmAddr) + expected, err = method.Outputs.Pack([]string{val.String()}) + require.Nil(t, err) + require.Equal(t, expected, ret) +} + +func TestQueryValidatorSlashes(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + + valPub := secp256k1.GenPrivKey().PubKey() + val := setupValidator(t, ctx, testApp, stakingtypes.Bonded, valPub) + + p, err := distribution.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + + ret, method := runDistrQuery(t, ctx, testApp, p, p.GetExecutor().(*distribution.PrecompileExecutor).ValidatorSlashesID, val.String(), uint64(1), uint64(10), []byte{}) + expected, err := method.Outputs.Pack([]distribution.Slash{}, []byte{}) + require.Nil(t, err) + require.Equal(t, expected, ret) +} + // TestWithdrawValidatorCommission_InputValidation tests various input validation scenarios func TestWithdrawValidatorCommission_InputValidation(t *testing.T) { testApp := testkeeper.EVMTestApp diff --git a/precompiles/evidence/Evidence.sol b/precompiles/evidence/Evidence.sol new file mode 100644 index 0000000000..0769ff6d46 --- /dev/null +++ b/precompiles/evidence/Evidence.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant EVIDENCE_PRECOMPILE_ADDRESS = 0x000000000000000000000000000000000000100f; + +IEvidence constant EVIDENCE_CONTRACT = IEvidence(EVIDENCE_PRECOMPILE_ADDRESS); + +interface IEvidence { + // Queries + // Returns the JSON encoding of the evidence with the given hash. + function evidence(bytes memory evidenceHash) external view returns (bytes memory); + + // Returns all evidence, each entry JSON-encoded, with pagination. + function allEvidence(bytes memory pageKey) external view returns (AllEvidenceResponse memory response); + + // Structs + struct AllEvidenceResponse { + bytes[] evidenceList; + bytes nextKey; + } +} diff --git a/precompiles/evidence/abi.json b/precompiles/evidence/abi.json new file mode 100644 index 0000000000..8fab85ad7b --- /dev/null +++ b/precompiles/evidence/abi.json @@ -0,0 +1,52 @@ +[ + { + "inputs": [ + { + "internalType": "bytes", + "name": "pageKey", + "type": "bytes" + } + ], + "name": "allEvidence", + "outputs": [ + { + "components": [ + { + "internalType": "bytes[]", + "name": "evidenceList", + "type": "bytes[]" + }, + { + "internalType": "bytes", + "name": "nextKey", + "type": "bytes" + } + ], + "internalType": "struct IEvidence.AllEvidenceResponse", + "name": "response", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "evidenceHash", + "type": "bytes" + } + ], + "name": "evidence", + "outputs": [ + { + "internalType": "bytes", + "name": "evidence", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/precompiles/evidence/evidence.go b/precompiles/evidence/evidence.go new file mode 100644 index 0000000000..79b1b4acbe --- /dev/null +++ b/precompiles/evidence/evidence.go @@ -0,0 +1,175 @@ +package evidence + +import ( + "embed" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" + evidencetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/evidence/types" + tmbytes "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" +) + +const ( + EvidenceMethod = "evidence" + AllEvidenceMethod = "allEvidence" +) + +const ( + EvidenceAddress = "0x000000000000000000000000000000000000100F" +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +type PrecompileExecutor struct { + evmKeeper utils.EVMKeeper + evidenceQuerier utils.EvidenceQuerier + cdc codec.Codec + + EvidenceID []byte + AllEvidenceID []byte +} + +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { + newAbi := pcommon.MustGetABI(f, "abi.json") + + p := &PrecompileExecutor{ + evmKeeper: keepers.EVMK(), + evidenceQuerier: keepers.EvidenceQ(), + cdc: keepers.Codec(), + } + + for name, m := range newAbi.Methods { + switch name { + case EvidenceMethod: + p.EvidenceID = m.ID + case AllEvidenceMethod: + p.AllEvidenceID = m.ID + } + } + + return pcommon.NewDynamicGasPrecompile(newAbi, p, common.HexToAddress(EvidenceAddress), "evidence"), nil +} + +// RequiredGas returns the required bare minimum gas to execute the precompile. +func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (e.g. auth's NextAccountNumber increments the global + // account number counter, gov's Tally deletes votes), so run every + // view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } + switch method.Name { + case EvidenceMethod: + return p.evidence(ctx, method, args, value) + case AllEvidenceMethod: + return p.allEvidence(ctx, method, args, value) + } + return +} + +func (p PrecompileExecutor) evidence(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + req := &evidencetypes.QueryEvidenceRequest{ + EvidenceHash: tmbytes.HexBytes(args[0].([]byte)), + } + + resp, err := p.evidenceQuerier.Evidence(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + evidenceJSON, err := p.cdc.MarshalAsJSON(resp.Evidence) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(evidenceJSON) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +type AllEvidenceResponse struct { + EvidenceList [][]byte + NextKey []byte +} + +func (p PrecompileExecutor) allEvidence(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + req := &evidencetypes.QueryAllEvidenceRequest{ + Pagination: &query.PageRequest{ + Key: args[0].([]byte), + }, + } + + resp, err := p.evidenceQuerier.AllEvidence(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + res := AllEvidenceResponse{ + EvidenceList: make([][]byte, len(resp.Evidence)), + } + for i, evidenceAny := range resp.Evidence { + evidenceJSON, err := p.cdc.MarshalAsJSON(evidenceAny) + if err != nil { + return nil, 0, err + } + res.EvidenceList[i] = evidenceJSON + } + if resp.Pagination != nil { + res.NextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(res) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper +} + +func (PrecompileExecutor) IsTransaction(string) bool { + return false +} diff --git a/precompiles/evidence/evidence_test.go b/precompiles/evidence/evidence_test.go new file mode 100644 index 0000000000..c0272997d6 --- /dev/null +++ b/precompiles/evidence/evidence_test.go @@ -0,0 +1,140 @@ +package evidence_test + +import ( + "math" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/evidence" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + evidencetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/evidence/types" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" + "github.com/stretchr/testify/require" +) + +func TestEvidencePrecompileAddress(t *testing.T) { + testApp := testkeeper.EVMTestApp + p, err := evidence.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + require.Equal(t, common.HexToAddress(evidence.EvidenceAddress), p.Address()) +} + +func TestEvidenceQueryNotFound(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + p, err := evidence.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + method, err := p.ABI.MethodById(p.GetExecutor().(*evidence.PrecompileExecutor).EvidenceID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack([]byte("nonexistent-evidence-hash")) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, nil, nil, true, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + require.Nil(t, ret) +} + +func TestAllEvidenceEmpty(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + p, err := evidence.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + method, err := p.ABI.MethodById(p.GetExecutor().(*evidence.PrecompileExecutor).AllEvidenceID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack([]byte{}) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, nil, nil, true, false) + require.NoError(t, err) + + expected, err := method.Outputs.Pack(evidence.AllEvidenceResponse{EvidenceList: [][]byte{}}) + require.NoError(t, err) + require.Equal(t, expected, ret) +} + +func TestEvidenceQueryAndAllEvidence(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + p, err := evidence.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + ev := &evidencetypes.Equivocation{ + Height: 1, + Power: 100, + Time: time.Unix(1234, 0).UTC(), + ConsensusAddress: sdk.ConsAddress([]byte("evidenceconsaddr1234")).String(), + } + testApp.EvidenceKeeper.SetEvidence(ctx, ev) + + // evidence(bytes evidenceHash) + method, err := p.ABI.MethodById(p.GetExecutor().(*evidence.PrecompileExecutor).EvidenceID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack([]byte(ev.Hash())) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, nil, nil, true, false) + require.NoError(t, err) + + outputs, err := method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + evidenceJSON := string(outputs[0].([]byte)) + require.Contains(t, evidenceJSON, "@type") + require.Contains(t, evidenceJSON, "Equivocation") + require.Contains(t, evidenceJSON, ev.ConsensusAddress) + + // allEvidence(bytes pageKey) + allMethod, err := p.ABI.MethodById(p.GetExecutor().(*evidence.PrecompileExecutor).AllEvidenceID) + require.NoError(t, err) + + allInputs, err := allMethod.Inputs.Pack([]byte{}) + require.NoError(t, err) + + allRet, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(allMethod.ID, allInputs...), math.MaxUint64, nil, nil, true, false) + require.NoError(t, err) + + expected, err := allMethod.Outputs.Pack(evidence.AllEvidenceResponse{EvidenceList: [][]byte{[]byte(evidenceJSON)}}) + require.NoError(t, err) + require.Equal(t, expected, allRet) +} + +func TestEvidenceQueryNonPayable(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + p, err := evidence.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + method, err := p.ABI.MethodById(p.GetExecutor().(*evidence.PrecompileExecutor).AllEvidenceID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack([]byte{}) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + require.Nil(t, ret) +} diff --git a/precompiles/evidence/setup.go b/precompiles/evidence/setup.go new file mode 100644 index 0000000000..86d68be0d1 --- /dev/null +++ b/precompiles/evidence/setup.go @@ -0,0 +1,21 @@ +// Code generated by scripts/bump_version; DO NOT EDIT. + +package evidence + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/utils" +) + +func GetVersioned(latestUpgrade string, keepers utils.Keepers) utils.VersionedPrecompiles { + return utils.VersionedPrecompiles{ + latestUpgrade: check(NewPrecompile(keepers)), + } +} + +func check(p vm.PrecompiledContract, err error) vm.PrecompiledContract { + if err != nil { + panic(err) + } + return p +} diff --git a/precompiles/evidence/versions b/precompiles/evidence/versions new file mode 100644 index 0000000000..5ff3284364 --- /dev/null +++ b/precompiles/evidence/versions @@ -0,0 +1 @@ +v6.6 diff --git a/precompiles/feegrant/Feegrant.sol b/precompiles/feegrant/Feegrant.sol new file mode 100644 index 0000000000..8db29633e6 --- /dev/null +++ b/precompiles/feegrant/Feegrant.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant FEEGRANT_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001010; + +IFeegrant constant FEEGRANT_CONTRACT = IFeegrant(FEEGRANT_PRECOMPILE_ADDRESS); + +interface IFeegrant { + // Queries + // Returns the fee allowance granted to the grantee by the granter. The + // allowance field of the returned grant is JSON-encoded. + function allowance(address granter, address grantee) external view returns (Grant memory grant); + + // Returns all the fee allowances granted to the given grantee, with pagination. + function allowances(address grantee, bytes memory pageKey) external view returns (AllowancesResponse memory response); + + // Returns all the fee allowances issued by the given granter, with pagination. + function allowancesByGranter(address granter, bytes memory pageKey) external view returns (AllowancesResponse memory response); + + // Structs + struct Grant { + string granter; + string grantee; + bytes allowance; + } + + struct AllowancesResponse { + Grant[] allowances; + bytes nextKey; + } +} diff --git a/precompiles/feegrant/abi.json b/precompiles/feegrant/abi.json new file mode 100644 index 0000000000..c79c89ee50 --- /dev/null +++ b/precompiles/feegrant/abi.json @@ -0,0 +1,149 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "granter", + "type": "address" + }, + { + "internalType": "address", + "name": "grantee", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "granter", + "type": "string" + }, + { + "internalType": "string", + "name": "grantee", + "type": "string" + }, + { + "internalType": "bytes", + "name": "allowance", + "type": "bytes" + } + ], + "internalType": "struct IFeegrant.Grant", + "name": "grant", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "grantee", + "type": "address" + }, + { + "internalType": "bytes", + "name": "pageKey", + "type": "bytes" + } + ], + "name": "allowances", + "outputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "string", + "name": "granter", + "type": "string" + }, + { + "internalType": "string", + "name": "grantee", + "type": "string" + }, + { + "internalType": "bytes", + "name": "allowance", + "type": "bytes" + } + ], + "internalType": "struct IFeegrant.Grant[]", + "name": "allowances", + "type": "tuple[]" + }, + { + "internalType": "bytes", + "name": "nextKey", + "type": "bytes" + } + ], + "internalType": "struct IFeegrant.AllowancesResponse", + "name": "response", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "granter", + "type": "address" + }, + { + "internalType": "bytes", + "name": "pageKey", + "type": "bytes" + } + ], + "name": "allowancesByGranter", + "outputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "string", + "name": "granter", + "type": "string" + }, + { + "internalType": "string", + "name": "grantee", + "type": "string" + }, + { + "internalType": "bytes", + "name": "allowance", + "type": "bytes" + } + ], + "internalType": "struct IFeegrant.Grant[]", + "name": "allowances", + "type": "tuple[]" + }, + { + "internalType": "bytes", + "name": "nextKey", + "type": "bytes" + } + ], + "internalType": "struct IFeegrant.AllowancesResponse", + "name": "response", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/precompiles/feegrant/feegrant.go b/precompiles/feegrant/feegrant.go new file mode 100644 index 0000000000..34a5d6094d --- /dev/null +++ b/precompiles/feegrant/feegrant.go @@ -0,0 +1,265 @@ +package feegrant + +import ( + "embed" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" + feegranttypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/feegrant" +) + +const ( + AllowanceMethod = "allowance" + AllowancesMethod = "allowances" + AllowancesByGranterMethod = "allowancesByGranter" +) + +const ( + FeegrantAddress = "0x0000000000000000000000000000000000001010" +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +type PrecompileExecutor struct { + evmKeeper utils.EVMKeeper + feegrantQuerier utils.FeegrantQuerier + cdc codec.Codec + + AllowanceID []byte + AllowancesID []byte + AllowancesByGranterID []byte +} + +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { + newAbi := pcommon.MustGetABI(f, "abi.json") + + p := &PrecompileExecutor{ + evmKeeper: keepers.EVMK(), + feegrantQuerier: keepers.FeegrantQ(), + cdc: keepers.Codec(), + } + + for name, m := range newAbi.Methods { + switch name { + case AllowanceMethod: + p.AllowanceID = m.ID + case AllowancesMethod: + p.AllowancesID = m.ID + case AllowancesByGranterMethod: + p.AllowancesByGranterID = m.ID + } + } + + return pcommon.NewDynamicGasPrecompile(newAbi, p, common.HexToAddress(FeegrantAddress), "feegrant"), nil +} + +// RequiredGas returns the required bare minimum gas to execute the precompile. +func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (e.g. auth's NextAccountNumber increments the global + // account number counter, gov's Tally deletes votes), so run every + // view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } + switch method.Name { + case AllowanceMethod: + return p.allowance(ctx, method, args, value) + case AllowancesMethod: + return p.allowances(ctx, method, args, value) + case AllowancesByGranterMethod: + return p.allowancesByGranter(ctx, method, args, value) + } + return +} + +type Grant struct { + Granter string + Grantee string + Allowance []byte +} + +type AllowancesResponse struct { + Allowances []Grant + NextKey []byte +} + +func (p PrecompileExecutor) allowance(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + granter, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[1], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + req := &feegranttypes.QueryAllowanceRequest{ + Granter: granter.String(), + Grantee: grantee.String(), + } + + resp, err := p.feegrantQuerier.Allowance(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + grant, err := p.toGrant(resp.Allowance) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(grant) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) allowances(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + req := &feegranttypes.QueryAllowancesRequest{ + Grantee: grantee.String(), + Pagination: &query.PageRequest{ + Key: args[1].([]byte), + }, + } + + resp, err := p.feegrantQuerier.Allowances(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + res, err := p.toAllowancesResponse(resp.Allowances, resp.Pagination) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(res) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) allowancesByGranter(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + granter, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + req := &feegranttypes.QueryAllowancesByGranterRequest{ + Granter: granter.String(), + Pagination: &query.PageRequest{ + Key: args[1].([]byte), + }, + } + + resp, err := p.feegrantQuerier.AllowancesByGranter(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + res, err := p.toAllowancesResponse(resp.Allowances, resp.Pagination) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(res) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) toGrant(grant *feegranttypes.Grant) (Grant, error) { + if grant == nil { + return Grant{}, errors.New("no allowance found") + } + allowanceJSON, err := p.cdc.MarshalAsJSON(grant.Allowance) + if err != nil { + return Grant{}, err + } + return Grant{ + Granter: grant.Granter, + Grantee: grant.Grantee, + Allowance: allowanceJSON, + }, nil +} + +func (p PrecompileExecutor) toAllowancesResponse(grants []*feegranttypes.Grant, pagination *query.PageResponse) (AllowancesResponse, error) { + res := AllowancesResponse{ + Allowances: make([]Grant, len(grants)), + } + for i, grant := range grants { + g, err := p.toGrant(grant) + if err != nil { + return AllowancesResponse{}, err + } + res.Allowances[i] = g + } + if pagination != nil { + res.NextKey = pagination.NextKey + } + return res, nil +} + +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper +} + +func (PrecompileExecutor) IsTransaction(string) bool { + return false +} diff --git a/precompiles/feegrant/feegrant_test.go b/precompiles/feegrant/feegrant_test.go new file mode 100644 index 0000000000..719eb5da3e --- /dev/null +++ b/precompiles/feegrant/feegrant_test.go @@ -0,0 +1,161 @@ +package feegrant_test + +import ( + "math" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/feegrant" + codectypes "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + feegranttypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/feegrant" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" + "github.com/stretchr/testify/require" +) + +func TestFeegrantPrecompileAddress(t *testing.T) { + testApp := testkeeper.EVMTestApp + p, err := feegrant.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + require.Equal(t, "0x0000000000000000000000000000000000001010", p.Address().Hex()) +} + +func TestFeegrantQueries(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + p, err := feegrant.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + + granterSei, granterEvm := testkeeper.MockAddressPair() + granteeSei, granteeEvm := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, granterSei, granterEvm) + k.SetAddressMapping(ctx, granteeSei, granteeEvm) + + allowance := &feegranttypes.BasicAllowance{ + SpendLimit: sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))), + } + require.NoError(t, testApp.FeeGrantKeeper.GrantAllowance(ctx, granterSei, granteeSei, allowance)) + + // The precompile JSON-encodes the grant's allowance Any; build the same + // encoding here for exact-output comparisons. + allowanceAny, err := codectypes.NewAnyWithValue(allowance) + require.NoError(t, err) + allowanceJSON, err := testApp.GetPrecompileKeepers().Codec().MarshalAsJSON(allowanceAny) + require.NoError(t, err) + require.Contains(t, string(allowanceJSON), "@type") + require.Contains(t, string(allowanceJSON), "BasicAllowance") + + executor := p.GetExecutor().(*feegrant.PrecompileExecutor) + + t.Run("allowance", func(t *testing.T) { + method, err := p.ABI.MethodById(executor.AllowanceID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack(granterEvm, granteeEvm) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, nil, nil, true, false) + require.NoError(t, err) + + outputs, err := method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + grant := *abi.ConvertType(outputs[0], new(feegrant.Grant)).(*feegrant.Grant) + require.Equal(t, granterSei.String(), grant.Granter) + require.Equal(t, granteeSei.String(), grant.Grantee) + require.Contains(t, string(grant.Allowance), "@type") + require.Equal(t, string(allowanceJSON), string(grant.Allowance)) + }) + + expectedListOutput := feegrant.AllowancesResponse{ + Allowances: []feegrant.Grant{ + { + Granter: granterSei.String(), + Grantee: granteeSei.String(), + Allowance: allowanceJSON, + }, + }, + } + + t.Run("allowances", func(t *testing.T) { + method, err := p.ABI.MethodById(executor.AllowancesID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack(granteeEvm, []byte{}) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, nil, nil, true, false) + require.NoError(t, err) + + expected, err := method.Outputs.Pack(expectedListOutput) + require.NoError(t, err) + require.Equal(t, expected, ret) + }) + + t.Run("allowancesByGranter", func(t *testing.T) { + method, err := p.ABI.MethodById(executor.AllowancesByGranterID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack(granterEvm, []byte{}) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, nil, nil, true, false) + require.NoError(t, err) + + expected, err := method.Outputs.Pack(expectedListOutput) + require.NoError(t, err) + require.Equal(t, expected, ret) + }) + + t.Run("allowance not found", func(t *testing.T) { + otherSei, otherEvm := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, otherSei, otherEvm) + + method, err := p.ABI.MethodById(executor.AllowanceID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack(otherEvm, granteeEvm) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, nil, nil, true, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + require.Nil(t, ret) + }) + + t.Run("fails if value passed", func(t *testing.T) { + method, err := p.ABI.MethodById(executor.AllowanceID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack(granterEvm, granteeEvm) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + require.Nil(t, ret) + }) + + t.Run("fails for unassociated address", func(t *testing.T) { + _, unassociatedEvm := testkeeper.MockAddressPair() + + method, err := p.ABI.MethodById(executor.AllowanceID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack(unassociatedEvm, granteeEvm) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), math.MaxUint64, nil, nil, true, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + require.Nil(t, ret) + }) +} diff --git a/precompiles/feegrant/setup.go b/precompiles/feegrant/setup.go new file mode 100644 index 0000000000..cad0a69095 --- /dev/null +++ b/precompiles/feegrant/setup.go @@ -0,0 +1,21 @@ +// Code generated by scripts/bump_version; DO NOT EDIT. + +package feegrant + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/utils" +) + +func GetVersioned(latestUpgrade string, keepers utils.Keepers) utils.VersionedPrecompiles { + return utils.VersionedPrecompiles{ + latestUpgrade: check(NewPrecompile(keepers)), + } +} + +func check(p vm.PrecompiledContract, err error) vm.PrecompiledContract { + if err != nil { + panic(err) + } + return p +} diff --git a/precompiles/feegrant/versions b/precompiles/feegrant/versions new file mode 100644 index 0000000000..5ff3284364 --- /dev/null +++ b/precompiles/feegrant/versions @@ -0,0 +1 @@ +v6.6 diff --git a/precompiles/gov/Gov.sol b/precompiles/gov/Gov.sol index e190579f32..760f9f7ec7 100644 --- a/precompiles/gov/Gov.sol +++ b/precompiles/gov/Gov.sol @@ -13,6 +13,61 @@ struct WeightedVoteOption { } interface IGov { + struct Coin { + uint256 amount; + string denom; + } + + struct TallyResultData { + string yes; + string abstain; + string no; + string noWithVeto; + } + + struct WeightedVoteOptionData { + int32 option; + string weight; // Weight as decimal string (e.g., "0.7") + } + + struct ProposalData { + uint64 id; + int32 status; // ProposalStatus enum value + TallyResultData finalTallyResult; + int64 submitTime; // Unix seconds + int64 depositEndTime; // Unix seconds + Coin[] totalDeposit; + int64 votingStartTime; // Unix seconds + int64 votingEndTime; // Unix seconds + bool isExpedited; + bytes content; // proposal content as JSON + } + + struct VoteData { + uint64 proposalId; + string voter; // bech32 address + WeightedVoteOptionData[] options; + } + + struct DepositData { + uint64 proposalId; + string depositor; // bech32 address + Coin[] amount; + } + + struct GovParams { + uint64 votingPeriod; // seconds + uint64 expeditedVotingPeriod; // seconds + Coin[] minDeposit; + uint64 maxDepositPeriod; // seconds + Coin[] minExpeditedDeposit; + string quorum; + string threshold; + string vetoThreshold; + string expeditedQuorum; + string expeditedThreshold; + } + /** * @dev Cast a simple vote on a governance proposal * @param proposalID The ID of the proposal to vote on @@ -65,4 +120,90 @@ interface IGov { function submitProposal( string calldata proposalJSON ) payable external returns (uint64 proposalID); + + /** + * @dev Query a proposal by ID + * @param proposalID The ID of the proposal + * @return proposal The proposal details + */ + function proposal( + uint64 proposalID + ) external view returns (ProposalData memory proposal); + + /** + * @dev Query proposals with optional filters + * @param proposalStatus Filter by proposal status (0 = all) + * @param voter Filter by voter address (zero address = no filter) + * @param depositor Filter by depositor address (zero address = no filter) + * @param pageKey Pagination key (empty for first page) + * @return proposals The matching proposals + * @return nextKey Pagination key for the next page + */ + function proposals( + int32 proposalStatus, + address voter, + address depositor, + bytes memory pageKey + ) external view returns (ProposalData[] memory proposals, bytes memory nextKey); + + /** + * @dev Query a vote cast on a proposal + * @param proposalID The ID of the proposal + * @param voter The voter address + * @return vote The vote details + */ + function getVote( + uint64 proposalID, + address voter + ) external view returns (VoteData memory vote); + + /** + * @dev Query all votes cast on a proposal + * @param proposalID The ID of the proposal + * @param pageKey Pagination key (empty for first page) + * @return votes The votes cast on the proposal + * @return nextKey Pagination key for the next page + */ + function votes( + uint64 proposalID, + bytes memory pageKey + ) external view returns (VoteData[] memory votes, bytes memory nextKey); + + /** + * @dev Query the governance module parameters + * @return params The voting, deposit and tally parameters + */ + function params() external view returns (GovParams memory params); + + /** + * @dev Query a deposit made to a proposal + * @param proposalID The ID of the proposal + * @param depositor The depositor address + * @return deposit The deposit details + */ + function getDeposit( + uint64 proposalID, + address depositor + ) external view returns (DepositData memory deposit); + + /** + * @dev Query all deposits made to a proposal + * @param proposalID The ID of the proposal + * @param pageKey Pagination key (empty for first page) + * @return deposits The deposits made to the proposal + * @return nextKey Pagination key for the next page + */ + function deposits( + uint64 proposalID, + bytes memory pageKey + ) external view returns (DepositData[] memory deposits, bytes memory nextKey); + + /** + * @dev Query the current tally of votes on a proposal + * @param proposalID The ID of the proposal + * @return tallyResult The tally result + */ + function tallyResult( + uint64 proposalID + ) external view returns (TallyResultData memory tallyResult); } diff --git a/precompiles/gov/abi.json b/precompiles/gov/abi.json index 1c0a870ccf..d406ce2617 100755 --- a/precompiles/gov/abi.json +++ b/precompiles/gov/abi.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"string","name":"proposalJSON","type":"string"}],"name":"submitProposal","outputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"}],"name":"deposit","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"},{"internalType":"int32","name":"option","type":"int32"}],"name":"vote","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"},{"components":[{"internalType":"int32","name":"option","type":"int32"},{"internalType":"string","name":"weight","type":"string"}],"internalType":"struct WeightedVoteOption[]","name":"options","type":"tuple[]"}],"name":"voteWeighted","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] +[{"inputs":[{"internalType":"string","name":"proposalJSON","type":"string"}],"name":"submitProposal","outputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"}],"name":"deposit","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"},{"internalType":"int32","name":"option","type":"int32"}],"name":"vote","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"},{"components":[{"internalType":"int32","name":"option","type":"int32"},{"internalType":"string","name":"weight","type":"string"}],"internalType":"struct WeightedVoteOption[]","name":"options","type":"tuple[]"}],"name":"voteWeighted","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"}],"name":"proposal","outputs":[{"components":[{"internalType":"uint64","name":"id","type":"uint64"},{"internalType":"int32","name":"status","type":"int32"},{"components":[{"internalType":"string","name":"yes","type":"string"},{"internalType":"string","name":"abstain","type":"string"},{"internalType":"string","name":"no","type":"string"},{"internalType":"string","name":"noWithVeto","type":"string"}],"internalType":"struct IGov.TallyResultData","name":"finalTallyResult","type":"tuple"},{"internalType":"int64","name":"submitTime","type":"int64"},{"internalType":"int64","name":"depositEndTime","type":"int64"},{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IGov.Coin[]","name":"totalDeposit","type":"tuple[]"},{"internalType":"int64","name":"votingStartTime","type":"int64"},{"internalType":"int64","name":"votingEndTime","type":"int64"},{"internalType":"bool","name":"isExpedited","type":"bool"},{"internalType":"bytes","name":"content","type":"bytes"}],"internalType":"struct IGov.ProposalData","name":"proposal","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int32","name":"proposalStatus","type":"int32"},{"internalType":"address","name":"voter","type":"address"},{"internalType":"address","name":"depositor","type":"address"},{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"proposals","outputs":[{"components":[{"internalType":"uint64","name":"id","type":"uint64"},{"internalType":"int32","name":"status","type":"int32"},{"components":[{"internalType":"string","name":"yes","type":"string"},{"internalType":"string","name":"abstain","type":"string"},{"internalType":"string","name":"no","type":"string"},{"internalType":"string","name":"noWithVeto","type":"string"}],"internalType":"struct IGov.TallyResultData","name":"finalTallyResult","type":"tuple"},{"internalType":"int64","name":"submitTime","type":"int64"},{"internalType":"int64","name":"depositEndTime","type":"int64"},{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IGov.Coin[]","name":"totalDeposit","type":"tuple[]"},{"internalType":"int64","name":"votingStartTime","type":"int64"},{"internalType":"int64","name":"votingEndTime","type":"int64"},{"internalType":"bool","name":"isExpedited","type":"bool"},{"internalType":"bytes","name":"content","type":"bytes"}],"internalType":"struct IGov.ProposalData[]","name":"proposals","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"},{"internalType":"address","name":"voter","type":"address"}],"name":"getVote","outputs":[{"components":[{"internalType":"uint64","name":"proposalId","type":"uint64"},{"internalType":"string","name":"voter","type":"string"},{"components":[{"internalType":"int32","name":"option","type":"int32"},{"internalType":"string","name":"weight","type":"string"}],"internalType":"struct IGov.WeightedVoteOptionData[]","name":"options","type":"tuple[]"}],"internalType":"struct IGov.VoteData","name":"vote","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"},{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"votes","outputs":[{"components":[{"internalType":"uint64","name":"proposalId","type":"uint64"},{"internalType":"string","name":"voter","type":"string"},{"components":[{"internalType":"int32","name":"option","type":"int32"},{"internalType":"string","name":"weight","type":"string"}],"internalType":"struct IGov.WeightedVoteOptionData[]","name":"options","type":"tuple[]"}],"internalType":"struct IGov.VoteData[]","name":"votes","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"params","outputs":[{"components":[{"internalType":"uint64","name":"votingPeriod","type":"uint64"},{"internalType":"uint64","name":"expeditedVotingPeriod","type":"uint64"},{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IGov.Coin[]","name":"minDeposit","type":"tuple[]"},{"internalType":"uint64","name":"maxDepositPeriod","type":"uint64"},{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IGov.Coin[]","name":"minExpeditedDeposit","type":"tuple[]"},{"internalType":"string","name":"quorum","type":"string"},{"internalType":"string","name":"threshold","type":"string"},{"internalType":"string","name":"vetoThreshold","type":"string"},{"internalType":"string","name":"expeditedQuorum","type":"string"},{"internalType":"string","name":"expeditedThreshold","type":"string"}],"internalType":"struct IGov.GovParams","name":"params","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"},{"internalType":"address","name":"depositor","type":"address"}],"name":"getDeposit","outputs":[{"components":[{"internalType":"uint64","name":"proposalId","type":"uint64"},{"internalType":"string","name":"depositor","type":"string"},{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IGov.Coin[]","name":"amount","type":"tuple[]"}],"internalType":"struct IGov.DepositData","name":"deposit","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"},{"internalType":"bytes","name":"pageKey","type":"bytes"}],"name":"deposits","outputs":[{"components":[{"internalType":"uint64","name":"proposalId","type":"uint64"},{"internalType":"string","name":"depositor","type":"string"},{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"string","name":"denom","type":"string"}],"internalType":"struct IGov.Coin[]","name":"amount","type":"tuple[]"}],"internalType":"struct IGov.DepositData[]","name":"deposits","type":"tuple[]"},{"internalType":"bytes","name":"nextKey","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"proposalID","type":"uint64"}],"name":"tallyResult","outputs":[{"components":[{"internalType":"string","name":"yes","type":"string"},{"internalType":"string","name":"abstain","type":"string"},{"internalType":"string","name":"no","type":"string"},{"internalType":"string","name":"noWithVeto","type":"string"}],"internalType":"struct IGov.TallyResultData","name":"tallyResult","type":"tuple"}],"stateMutability":"view","type":"function"}] diff --git a/precompiles/gov/gov.go b/precompiles/gov/gov.go index 3112f3b268..5a08215c64 100644 --- a/precompiles/gov/gov.go +++ b/precompiles/gov/gov.go @@ -1,7 +1,6 @@ package gov import ( - "bytes" "embed" "encoding/json" "errors" @@ -14,7 +13,9 @@ import ( "github.com/ethereum/go-ethereum/core/vm" pcommon "github.com/sei-protocol/sei-chain/precompiles/common" "github.com/sei-protocol/sei-chain/precompiles/utils" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" "github.com/sei-protocol/sei-chain/x/evm/types" ) @@ -26,6 +27,20 @@ const ( SubmitProposalMethod = "submitProposal" ) +// Query method names. The vote/deposit queries are named getVote/getDeposit +// rather than overloading the vote/deposit transaction methods: overloads +// break common tooling (e.g. ethers.js refuses ambiguous function names). +const ( + ProposalQueryMethod = "proposal" + ProposalsQueryMethod = "proposals" + VoteQueryMethod = "getVote" + VotesQueryMethod = "votes" + ParamsQueryMethod = "params" + DepositQueryMethod = "getDeposit" + DepositsQueryMethod = "deposits" + TallyResultQueryMethod = "tallyResult" +) + const ( GovAddress = "0x0000000000000000000000000000000000001006" ) @@ -37,8 +52,10 @@ var f embed.FS type PrecompileExecutor struct { govMsgServer utils.GovMsgServer + govQuerier utils.GovQuerier evmKeeper utils.EVMKeeper bankKeeper utils.BankKeeper + cdc codec.Codec address common.Address proposalHandlers map[string]ProposalHandler @@ -48,13 +65,15 @@ type PrecompileExecutor struct { SubmitProposalID []byte } -func NewPrecompile(keepers utils.Keepers) (*pcommon.Precompile, error) { +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { newAbi := pcommon.MustGetABI(f, "abi.json") p := &PrecompileExecutor{ govMsgServer: keepers.GovMS(), + govQuerier: keepers.GovQ(), evmKeeper: keepers.EVMK(), bankKeeper: keepers.BankK(), + cdc: keepers.Codec(), address: common.HexToAddress(GovAddress), } @@ -76,29 +95,69 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.Precompile, error) { } // Create the precompile - return pcommon.NewPrecompile(newAbi, p, p.address, "gov"), nil + return pcommon.NewDynamicGasPrecompile(newAbi, p, p.address, "gov"), nil } // RequiredGas returns the required bare minimum gas to execute the precompile. func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { - if bytes.Equal(method.ID, p.VoteID) || bytes.Equal(method.ID, p.VoteWeightedID) { - return 30000 - } else if bytes.Equal(method.ID, p.DepositID) { - return 30000 - } else if bytes.Equal(method.ID, p.SubmitProposalID) { - return 50000 - } + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} - // This should never happen since this is going to fail during Run - return pcommon.UnknownMethodCallGas +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper } -func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, hooks *tracing.Hooks) (bz []byte, err error) { - if readOnly { - return nil, errors.New("cannot call gov precompile from staticcall") +// IsTransaction returns true for methods that mutate state. All gov query +// methods are views. +func (p PrecompileExecutor) IsTransaction(method string) bool { + switch method { + case VoteMethod, VoteWeightedMethod, DepositMethod, SubmitProposalMethod: + return true + default: + return false } +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() if ctx.EVMPrecompileCalledFromDelegateCall() { - return nil, errors.New("cannot delegatecall gov") + return nil, 0, errors.New("cannot delegatecall gov") + } + + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (gov's TallyResult calls Tally, which deletes votes as + // it counts), so run every view on a branched context and discard the + // writes. + ctx, _ = ctx.CacheContext() + } + + switch method.Name { + case ProposalQueryMethod: + return p.proposalQuery(ctx, method, args, value) + case ProposalsQueryMethod: + return p.proposalsQuery(ctx, method, args, value) + case VoteQueryMethod: + return p.voteQuery(ctx, method, args, value) + case VotesQueryMethod: + return p.votesQuery(ctx, method, args, value) + case ParamsQueryMethod: + return p.paramsQuery(ctx, method, args, value) + case DepositQueryMethod: + return p.depositQuery(ctx, method, args, value) + case DepositsQueryMethod: + return p.depositsQuery(ctx, method, args, value) + case TallyResultQueryMethod: + return p.tallyResultQuery(ctx, method, args, value) + } + + if readOnly { + return nil, 0, errors.New("cannot call gov precompile from staticcall") } switch method.Name { @@ -114,17 +173,17 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller return } -func (p PrecompileExecutor) vote(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int) ([]byte, error) { +func (p PrecompileExecutor) vote(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int) ([]byte, uint64, error) { if err := pcommon.ValidateNonPayable(value); err != nil { - return nil, err + return nil, 0, err } if err := pcommon.ValidateArgsLength(args, 2); err != nil { - return nil, err + return nil, 0, err } voter, found := p.evmKeeper.GetSeiAddress(ctx, caller) if !found { - return nil, types.NewAssociationMissingErr(caller.Hex()) + return nil, 0, types.NewAssociationMissingErr(caller.Hex()) } proposalID := args[0].(uint64) voteOption := args[1].(int32) @@ -132,28 +191,32 @@ func (p PrecompileExecutor) vote(ctx sdk.Context, method *abi.Method, caller com msg := govtypes.NewMsgVote(voter, proposalID, govtypes.VoteOption(voteOption)) err := msg.ValidateBasic() if err != nil { - return nil, err + return nil, 0, err } goCtx := sdk.WrapSDKContext(ctx) _, err = p.govMsgServer.Vote(goCtx, msg) if err != nil { - return nil, err + return nil, 0, err + } + bz, err := method.Outputs.Pack(true) + if err != nil { + return nil, 0, err } - return method.Outputs.Pack(true) + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil } -func (p PrecompileExecutor) voteWeighted(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int) ([]byte, error) { +func (p PrecompileExecutor) voteWeighted(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int) ([]byte, uint64, error) { if err := pcommon.ValidateNonPayable(value); err != nil { - return nil, err + return nil, 0, err } if err := pcommon.ValidateArgsLength(args, 2); err != nil { - return nil, err + return nil, 0, err } voter, found := p.evmKeeper.GetSeiAddress(ctx, caller) if !found { - return nil, types.NewAssociationMissingErr(caller.Hex()) + return nil, 0, types.NewAssociationMissingErr(caller.Hex()) } proposalID := args[0].(uint64) @@ -166,7 +229,7 @@ func (p PrecompileExecutor) voteWeighted(ctx sdk.Context, method *abi.Method, ca maxOptions := 4 if len(weightedOptionsStruct) > maxOptions { - return nil, fmt.Errorf("too many vote options provided: maximum allowed is %d", maxOptions) + return nil, 0, fmt.Errorf("too many vote options provided: maximum allowed is %d", maxOptions) } // Convert to WeightedVoteOptions @@ -175,7 +238,7 @@ func (p PrecompileExecutor) voteWeighted(ctx sdk.Context, method *abi.Method, ca // Parse weight as decimal weight, err := sdk.NewDecFromStr(optionStruct.Weight) if err != nil { - return nil, fmt.Errorf("invalid weight format: %w", err) + return nil, 0, fmt.Errorf("invalid weight format: %w", err) } voteOptions[i] = govtypes.WeightedVoteOption{ @@ -187,64 +250,72 @@ func (p PrecompileExecutor) voteWeighted(ctx sdk.Context, method *abi.Method, ca msg := govtypes.NewMsgVoteWeighted(voter, proposalID, voteOptions) err := msg.ValidateBasic() if err != nil { - return nil, err + return nil, 0, err } goCtx := sdk.WrapSDKContext(ctx) _, err = p.govMsgServer.VoteWeighted(goCtx, msg) if err != nil { - return nil, err + return nil, 0, err + } + bz, err := method.Outputs.Pack(true) + if err != nil { + return nil, 0, err } - return method.Outputs.Pack(true) + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil } -func (p PrecompileExecutor) deposit(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int, hooks *tracing.Hooks, evm *vm.EVM) ([]byte, error) { +func (p PrecompileExecutor) deposit(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int, hooks *tracing.Hooks, evm *vm.EVM) ([]byte, uint64, error) { if err := pcommon.ValidateArgsLength(args, 1); err != nil { - return nil, err + return nil, 0, err } depositor, found := p.evmKeeper.GetSeiAddress(ctx, caller) if !found { - return nil, types.NewAssociationMissingErr(caller.Hex()) + return nil, 0, types.NewAssociationMissingErr(caller.Hex()) } proposalID := args[0].(uint64) if value == nil || value.Sign() == 0 { - return nil, errors.New("set `value` field to non-zero to deposit fund") + return nil, 0, errors.New("set `value` field to non-zero to deposit fund") } coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), depositor, value, p.bankKeeper, p.evmKeeper, hooks, evm.GetDepth()) if err != nil { - return nil, err + return nil, 0, err } msg := govtypes.NewMsgDeposit(depositor, proposalID, sdk.NewCoins(coin)) err = msg.ValidateBasic() if err != nil { - return nil, err + return nil, 0, err } goCtx := sdk.WrapSDKContext(ctx) _, err = p.govMsgServer.Deposit(goCtx, msg) if err != nil { - return nil, err + return nil, 0, err } - return method.Outputs.Pack(true) + bz, err := method.Outputs.Pack(true) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil } -func (p PrecompileExecutor) submitProposal(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int, hooks *tracing.Hooks, evm *vm.EVM) ([]byte, error) { +func (p PrecompileExecutor) submitProposal(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int, hooks *tracing.Hooks, evm *vm.EVM) ([]byte, uint64, error) { if err := pcommon.ValidateArgsLength(args, 1); err != nil { - return nil, err + return nil, 0, err } proposer, found := p.evmKeeper.GetSeiAddress(ctx, caller) if !found { - return nil, types.NewAssociationMissingErr(caller.Hex()) + return nil, 0, types.NewAssociationMissingErr(caller.Hex()) } // Parse the proposal JSON proposalJSON := args[0].(string) var proposal Proposal if err := json.Unmarshal([]byte(proposalJSON), &proposal); err != nil { - return nil, fmt.Errorf("failed to parse proposal JSON: %w", err) + return nil, 0, fmt.Errorf("failed to parse proposal JSON: %w", err) } initialDeposit, err := pcommon.HandlePaymentUsei( @@ -258,26 +329,26 @@ func (p PrecompileExecutor) submitProposal(ctx sdk.Context, method *abi.Method, evm.GetDepth()) if err != nil { - return nil, err + return nil, 0, err } // Create the proposal content using the handler system content, err := p.createProposalContent(ctx, proposal) if err != nil { - return nil, err + return nil, 0, err } // Create the MsgSubmitProposal msg, err := govtypes.NewMsgSubmitProposalWithExpedite(content, sdk.NewCoins(initialDeposit), proposer, proposal.IsExpedited) if err != nil { - return nil, err + return nil, 0, err } // Validate the Msg err = msg.ValidateBasic() if err != nil { - return nil, err + return nil, 0, err } // Create a MsgServer context @@ -286,9 +357,401 @@ func (p PrecompileExecutor) submitProposal(ctx sdk.Context, method *abi.Method, // Submit the proposal using the MsgServer res, err := p.govMsgServer.SubmitProposal(goCtx, msg) if err != nil { - return nil, err + return nil, 0, err } // Return the proposal ID - return method.Outputs.Pack(res.ProposalId) + bz, err := method.Outputs.Pack(res.ProposalId) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +// Coin mirrors the abi.json Coin tuple. Field order must match the tuple +// component order exactly. +type Coin struct { + Amount *big.Int + Denom string +} + +// TallyResultData mirrors the abi.json TallyResultData tuple. +type TallyResultData struct { + Yes string + Abstain string + No string + NoWithVeto string +} + +// WeightedVoteOptionData mirrors the abi.json WeightedVoteOptionData tuple. +type WeightedVoteOptionData struct { + Option int32 + Weight string +} + +// ProposalData mirrors the abi.json ProposalData tuple. Content contains the +// proposal content (a protobuf Any) marshaled to JSON. +type ProposalData struct { + Id uint64 //nolint:revive,stylecheck // must match abi component name "id" + Status int32 + FinalTallyResult TallyResultData + SubmitTime int64 + DepositEndTime int64 + TotalDeposit []Coin + VotingStartTime int64 + VotingEndTime int64 + IsExpedited bool + Content []byte +} + +// VoteData mirrors the abi.json VoteData tuple. +type VoteData struct { + ProposalId uint64 //nolint:revive,stylecheck // must match abi component name "proposalId" + Voter string + Options []WeightedVoteOptionData +} + +// DepositData mirrors the abi.json DepositData tuple. +type DepositData struct { + ProposalId uint64 //nolint:revive,stylecheck // must match abi component name "proposalId" + Depositor string + Amount []Coin +} + +// GovParams mirrors the abi.json GovParams tuple. Durations are expressed in +// seconds. +type GovParams struct { + VotingPeriod uint64 + ExpeditedVotingPeriod uint64 + MinDeposit []Coin + MaxDepositPeriod uint64 + MinExpeditedDeposit []Coin + Quorum string + Threshold string + VetoThreshold string + ExpeditedQuorum string + ExpeditedThreshold string +} + +func (p PrecompileExecutor) proposalQuery(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + req := &govtypes.QueryProposalRequest{ProposalId: args[0].(uint64)} + resp, err := p.govQuerier.Proposal(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + proposal, err := p.convertProposal(resp.Proposal) + if err != nil { + return nil, 0, err + } + bz, err := method.Outputs.Pack(proposal) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) proposalsQuery(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + if err := pcommon.ValidateArgsLength(args, 4); err != nil { + return nil, 0, err + } + voter, err := p.optionalBech32FromArg(ctx, args[1]) + if err != nil { + return nil, 0, err + } + depositor, err := p.optionalBech32FromArg(ctx, args[2]) + if err != nil { + return nil, 0, err + } + req := &govtypes.QueryProposalsRequest{ + ProposalStatus: govtypes.ProposalStatus(args[0].(int32)), + Voter: voter, + Depositor: depositor, + Pagination: &query.PageRequest{ + Key: args[3].([]byte), + }, + } + resp, err := p.govQuerier.Proposals(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + proposals := make([]ProposalData, len(resp.Proposals)) + for i, proposal := range resp.Proposals { + converted, err := p.convertProposal(proposal) + if err != nil { + return nil, 0, err + } + proposals[i] = converted + } + var nextKey []byte + if resp.Pagination != nil { + nextKey = resp.Pagination.NextKey + } + bz, err := method.Outputs.Pack(proposals, nextKey) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) voteQuery(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + voter, err := pcommon.GetSeiAddressFromArg(ctx, args[1], p.evmKeeper) + if err != nil { + return nil, 0, err + } + req := &govtypes.QueryVoteRequest{ + ProposalId: args[0].(uint64), + Voter: voter.String(), + } + resp, err := p.govQuerier.Vote(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + bz, err := method.Outputs.Pack(convertVote(resp.Vote)) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) votesQuery(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + req := &govtypes.QueryVotesRequest{ + ProposalId: args[0].(uint64), + Pagination: &query.PageRequest{ + Key: args[1].([]byte), + }, + } + resp, err := p.govQuerier.Votes(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + votes := make([]VoteData, len(resp.Votes)) + for i, vote := range resp.Votes { + votes[i] = convertVote(vote) + } + var nextKey []byte + if resp.Pagination != nil { + nextKey = resp.Pagination.NextKey + } + bz, err := method.Outputs.Pack(votes, nextKey) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) paramsQuery(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + goCtx := sdk.WrapSDKContext(ctx) + votingResp, err := p.govQuerier.Params(goCtx, &govtypes.QueryParamsRequest{ParamsType: govtypes.ParamVoting}) + if err != nil { + return nil, 0, err + } + depositResp, err := p.govQuerier.Params(goCtx, &govtypes.QueryParamsRequest{ParamsType: govtypes.ParamDeposit}) + if err != nil { + return nil, 0, err + } + tallyResp, err := p.govQuerier.Params(goCtx, &govtypes.QueryParamsRequest{ParamsType: govtypes.ParamTallying}) + if err != nil { + return nil, 0, err + } + params := GovParams{ + VotingPeriod: uint64(votingResp.VotingParams.VotingPeriod.Seconds()), + ExpeditedVotingPeriod: uint64(votingResp.VotingParams.ExpeditedVotingPeriod.Seconds()), + MinDeposit: convertCoins(depositResp.DepositParams.MinDeposit), + MaxDepositPeriod: uint64(depositResp.DepositParams.MaxDepositPeriod.Seconds()), + MinExpeditedDeposit: convertCoins(depositResp.DepositParams.MinExpeditedDeposit), + Quorum: tallyResp.TallyParams.Quorum.String(), + Threshold: tallyResp.TallyParams.Threshold.String(), + VetoThreshold: tallyResp.TallyParams.VetoThreshold.String(), + ExpeditedQuorum: tallyResp.TallyParams.ExpeditedQuorum.String(), + ExpeditedThreshold: tallyResp.TallyParams.ExpeditedThreshold.String(), + } + bz, err := method.Outputs.Pack(params) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) depositQuery(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + depositor, err := pcommon.GetSeiAddressFromArg(ctx, args[1], p.evmKeeper) + if err != nil { + return nil, 0, err + } + req := &govtypes.QueryDepositRequest{ + ProposalId: args[0].(uint64), + Depositor: depositor.String(), + } + resp, err := p.govQuerier.Deposit(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + bz, err := method.Outputs.Pack(convertDeposit(resp.Deposit)) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) depositsQuery(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + req := &govtypes.QueryDepositsRequest{ + ProposalId: args[0].(uint64), + Pagination: &query.PageRequest{ + Key: args[1].([]byte), + }, + } + resp, err := p.govQuerier.Deposits(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + deposits := make([]DepositData, len(resp.Deposits)) + for i, deposit := range resp.Deposits { + deposits[i] = convertDeposit(deposit) + } + var nextKey []byte + if resp.Pagination != nil { + nextKey = resp.Pagination.NextKey + } + bz, err := method.Outputs.Pack(deposits, nextKey) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) tallyResultQuery(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + req := &govtypes.QueryTallyResultRequest{ProposalId: args[0].(uint64)} + resp, err := p.govQuerier.TallyResult(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + bz, err := method.Outputs.Pack(convertTallyResult(resp.Tally)) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +// optionalBech32FromArg resolves an EVM address argument to its associated Sei +// address in bech32 form. The zero address means "no filter" and resolves to +// an empty string. +func (p PrecompileExecutor) optionalBech32FromArg(ctx sdk.Context, arg interface{}) (string, error) { + if arg.(common.Address) == (common.Address{}) { + return "", nil + } + seiAddr, err := pcommon.GetSeiAddressFromArg(ctx, arg, p.evmKeeper) + if err != nil { + return "", err + } + return seiAddr.String(), nil +} + +func (p PrecompileExecutor) convertProposal(proposal govtypes.Proposal) (ProposalData, error) { + content := []byte{} + if proposal.Content != nil { + bz, err := p.cdc.MarshalAsJSON(proposal.Content) + if err != nil { + return ProposalData{}, err + } + content = bz + } + return ProposalData{ + Id: proposal.ProposalId, + Status: int32(proposal.Status), + FinalTallyResult: convertTallyResult(proposal.FinalTallyResult), + SubmitTime: proposal.SubmitTime.Unix(), + DepositEndTime: proposal.DepositEndTime.Unix(), + TotalDeposit: convertCoins(proposal.TotalDeposit), + VotingStartTime: proposal.VotingStartTime.Unix(), + VotingEndTime: proposal.VotingEndTime.Unix(), + IsExpedited: proposal.IsExpedited, + Content: content, + }, nil +} + +func convertTallyResult(tally govtypes.TallyResult) TallyResultData { + return TallyResultData{ + Yes: tally.Yes.String(), + Abstain: tally.Abstain.String(), + No: tally.No.String(), + NoWithVeto: tally.NoWithVeto.String(), + } +} + +func convertVote(vote govtypes.Vote) VoteData { + options := make([]WeightedVoteOptionData, len(vote.Options)) + for i, option := range vote.Options { + options[i] = WeightedVoteOptionData{ + Option: int32(option.Option), + Weight: option.Weight.String(), + } + } + return VoteData{ + ProposalId: vote.ProposalId, + Voter: vote.Voter, + Options: options, + } +} + +func convertDeposit(deposit govtypes.Deposit) DepositData { + return DepositData{ + ProposalId: deposit.ProposalId, + Depositor: deposit.Depositor, + Amount: convertCoins(deposit.Amount), + } +} + +func convertCoins(coins sdk.Coins) []Coin { + converted := make([]Coin, len(coins)) + for i, coin := range coins { + converted[i] = Coin{ + Amount: coin.Amount.BigInt(), + Denom: coin.Denom, + } + } + return converted } diff --git a/precompiles/gov/gov_test.go b/precompiles/gov/gov_test.go index 0bca502975..5949fae93c 100644 --- a/precompiles/gov/gov_test.go +++ b/precompiles/gov/gov_test.go @@ -6,8 +6,10 @@ import ( "math/big" "testing" + ethabi "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" @@ -19,6 +21,7 @@ import ( testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" "github.com/sei-protocol/sei-chain/x/evm/ante" "github.com/sei-protocol/sei-chain/x/evm/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" "github.com/sei-protocol/sei-chain/x/evm/types/ethtx" ) @@ -760,3 +763,190 @@ func TestPrecompileExecutor_submitProposal(t *testing.T) { }) } } + +func TestGovQueryPrecompile(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + baseDenom := k.GetBaseDenom(ctx) + + // Seed a proposal in voting period. + content := govtypes.ContentFromProposalType("query title", "query description", govtypes.ProposalTypeText, false) + proposal, err := testApp.GovKeeper.SubmitProposal(ctx, content) + require.Nil(t, err) + testApp.GovKeeper.ActivateVotingPeriod(ctx, proposal) + proposalID := proposal.ProposalId + + // Seed a vote. + voterSeiAddr, voterEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, voterSeiAddr, voterEvmAddr) + require.Nil(t, testApp.GovKeeper.AddVote(ctx, proposalID, voterSeiAddr, govtypes.NewNonSplitVoteOption(govtypes.OptionYes))) + + // Seed a deposit. + depositorSeiAddr, depositorEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, depositorSeiAddr, depositorEvmAddr) + depositCoins := sdk.NewCoins(sdk.NewCoin(baseDenom, sdk.NewInt(1000000))) + require.Nil(t, k.BankKeeper().MintCoins(ctx, evmtypes.ModuleName, depositCoins)) + require.Nil(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, depositorSeiAddr, depositCoins)) + _, err = testApp.GovKeeper.AddDeposit(ctx, proposalID, depositorSeiAddr, depositCoins) + require.Nil(t, err) + + storedProposal, found := testApp.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, found) + + p, err := gov.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + govAbi := pcommon.MustGetABI(f, "abi.json") + + query := func(t *testing.T, methodName string, args ...interface{}) []interface{} { + method, ok := govAbi.Methods[methodName] + require.True(t, ok, "method %s not found in abi", methodName) + inputs, err := method.Inputs.Pack(args...) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 1000000, nil, nil, true, false) + require.Nil(t, err) + outputs, err := method.Outputs.Unpack(ret) + require.Nil(t, err) + return outputs + } + + t.Run("proposal", func(t *testing.T) { + outputs := query(t, gov.ProposalQueryMethod, proposalID) + require.Len(t, outputs, 1) + got := *ethabi.ConvertType(outputs[0], new(gov.ProposalData)).(*gov.ProposalData) + require.Equal(t, proposalID, got.Id) + require.Equal(t, int32(govtypes.StatusVotingPeriod), got.Status) + require.Equal(t, storedProposal.SubmitTime.Unix(), got.SubmitTime) + require.Equal(t, storedProposal.DepositEndTime.Unix(), got.DepositEndTime) + require.Equal(t, storedProposal.VotingStartTime.Unix(), got.VotingStartTime) + require.Equal(t, storedProposal.VotingEndTime.Unix(), got.VotingEndTime) + require.False(t, got.IsExpedited) + require.Equal(t, []gov.Coin{{Amount: big.NewInt(1000000), Denom: baseDenom}}, got.TotalDeposit) + require.Contains(t, string(got.Content), "\"@type\"") + require.Contains(t, string(got.Content), "TextProposal") + require.Contains(t, string(got.Content), "query title") + }) + + t.Run("proposals filtered by depositor", func(t *testing.T) { + outputs := query(t, gov.ProposalsQueryMethod, int32(0), common.Address{}, depositorEvmAddr, []byte{}) + require.Len(t, outputs, 2) + gotProposals := *ethabi.ConvertType(outputs[0], new([]gov.ProposalData)).(*[]gov.ProposalData) + require.Len(t, gotProposals, 1) + require.Equal(t, proposalID, gotProposals[0].Id) + require.Contains(t, string(gotProposals[0].Content), "\"@type\"") + require.Empty(t, outputs[1].([]byte)) + }) + + t.Run("proposals filtered by voter and status", func(t *testing.T) { + outputs := query(t, gov.ProposalsQueryMethod, int32(govtypes.StatusVotingPeriod), voterEvmAddr, common.Address{}, []byte{}) + gotProposals := *ethabi.ConvertType(outputs[0], new([]gov.ProposalData)).(*[]gov.ProposalData) + require.Len(t, gotProposals, 1) + require.Equal(t, proposalID, gotProposals[0].Id) + }) + + t.Run("vote", func(t *testing.T) { + outputs := query(t, gov.VoteQueryMethod, proposalID, voterEvmAddr) + require.Len(t, outputs, 1) + got := *ethabi.ConvertType(outputs[0], new(gov.VoteData)).(*gov.VoteData) + require.Equal(t, proposalID, got.ProposalId) + require.Equal(t, voterSeiAddr.String(), got.Voter) + require.Equal(t, []gov.WeightedVoteOptionData{{Option: int32(govtypes.OptionYes), Weight: sdk.OneDec().String()}}, got.Options) + }) + + t.Run("votes", func(t *testing.T) { + outputs := query(t, gov.VotesQueryMethod, proposalID, []byte{}) + require.Len(t, outputs, 2) + gotVotes := *ethabi.ConvertType(outputs[0], new([]gov.VoteData)).(*[]gov.VoteData) + require.Len(t, gotVotes, 1) + require.Equal(t, voterSeiAddr.String(), gotVotes[0].Voter) + require.Empty(t, outputs[1].([]byte)) + }) + + t.Run("params", func(t *testing.T) { + outputs := query(t, gov.ParamsQueryMethod) + require.Len(t, outputs, 1) + got := *ethabi.ConvertType(outputs[0], new(gov.GovParams)).(*gov.GovParams) + votingParams := testApp.GovKeeper.GetVotingParams(ctx) + depositParams := testApp.GovKeeper.GetDepositParams(ctx) + tallyParams := testApp.GovKeeper.GetTallyParams(ctx) + require.Equal(t, uint64(votingParams.VotingPeriod.Seconds()), got.VotingPeriod) + require.Equal(t, uint64(votingParams.ExpeditedVotingPeriod.Seconds()), got.ExpeditedVotingPeriod) + require.Equal(t, uint64(depositParams.MaxDepositPeriod.Seconds()), got.MaxDepositPeriod) + require.Len(t, got.MinDeposit, len(depositParams.MinDeposit)) + for i, coin := range depositParams.MinDeposit { + require.Equal(t, coin.Denom, got.MinDeposit[i].Denom) + require.Equal(t, coin.Amount.BigInt(), got.MinDeposit[i].Amount) + } + require.Len(t, got.MinExpeditedDeposit, len(depositParams.MinExpeditedDeposit)) + require.Equal(t, tallyParams.Quorum.String(), got.Quorum) + require.Equal(t, tallyParams.Threshold.String(), got.Threshold) + require.Equal(t, tallyParams.VetoThreshold.String(), got.VetoThreshold) + require.Equal(t, tallyParams.ExpeditedQuorum.String(), got.ExpeditedQuorum) + require.Equal(t, tallyParams.ExpeditedThreshold.String(), got.ExpeditedThreshold) + }) + + t.Run("deposit", func(t *testing.T) { + outputs := query(t, gov.DepositQueryMethod, proposalID, depositorEvmAddr) + require.Len(t, outputs, 1) + got := *ethabi.ConvertType(outputs[0], new(gov.DepositData)).(*gov.DepositData) + require.Equal(t, proposalID, got.ProposalId) + require.Equal(t, depositorSeiAddr.String(), got.Depositor) + require.Equal(t, []gov.Coin{{Amount: big.NewInt(1000000), Denom: baseDenom}}, got.Amount) + }) + + t.Run("deposits", func(t *testing.T) { + outputs := query(t, gov.DepositsQueryMethod, proposalID, []byte{}) + require.Len(t, outputs, 2) + gotDeposits := *ethabi.ConvertType(outputs[0], new([]gov.DepositData)).(*[]gov.DepositData) + require.Len(t, gotDeposits, 1) + require.Equal(t, depositorSeiAddr.String(), gotDeposits[0].Depositor) + require.Equal(t, []gov.Coin{{Amount: big.NewInt(1000000), Denom: baseDenom}}, gotDeposits[0].Amount) + require.Empty(t, outputs[1].([]byte)) + }) + + t.Run("tallyResult", func(t *testing.T) { + outputs := query(t, gov.TallyResultQueryMethod, proposalID) + require.Len(t, outputs, 1) + got := *ethabi.ConvertType(outputs[0], new(gov.TallyResultData)).(*gov.TallyResultData) + // The voter has no staked tokens, so the live tally is all zeros. + require.Equal(t, gov.TallyResultData{Yes: "0", Abstain: "0", No: "0", NoWithVeto: "0"}, got) + }) + + t.Run("vote query with zero voter address fails", func(t *testing.T) { + method := govAbi.Methods[gov.VoteQueryMethod] + inputs, err := method.Inputs.Pack(proposalID, common.Address{}) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 1000000, nil, nil, true, false) + require.NotNil(t, err) + }) + + t.Run("tallyResult does not delete votes", func(t *testing.T) { + method := govAbi.Methods[gov.TallyResultQueryMethod] + inputs, err := method.Inputs.Pack(proposalID) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 1000000, nil, nil, true, false) + require.Nil(t, err) + // Tally deletes votes as it counts; the view must run on a branched + // context so the deletion is discarded. + _, found := testApp.GovKeeper.GetVote(statedb.Ctx(), proposalID, voterSeiAddr) + require.True(t, found) + }) + + t.Run("out of gas returns revert error instead of panicking", func(t *testing.T) { + method := govAbi.Methods[gov.ProposalQueryMethod] + inputs, err := method.Inputs.Pack(proposalID) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb} + require.NotPanics(t, func() { + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 1, nil, nil, true, false) + }) + require.NotNil(t, err) + }) +} diff --git a/precompiles/mint/Mint.sol b/precompiles/mint/Mint.sol new file mode 100644 index 0000000000..e5abbdb45b --- /dev/null +++ b/precompiles/mint/Mint.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant MINT_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001012; + +IMint constant MINT_CONTRACT = IMint(MINT_PRECOMPILE_ADDRESS); + +interface IMint { + // Queries + function params() external view returns (MintParams memory); + function minter() external view returns (Minter memory); + + // Structs + struct ScheduledTokenRelease { + string startDate; + string endDate; + uint64 tokenReleaseAmount; + } + + struct MintParams { + string mintDenom; + ScheduledTokenRelease[] tokenReleaseSchedule; + } + + struct Minter { + string startDate; + string endDate; + string denom; + uint64 totalMintAmount; + uint64 remainingMintAmount; + uint64 lastMintAmount; + string lastMintDate; + uint64 lastMintHeight; + } +} diff --git a/precompiles/mint/abi.json b/precompiles/mint/abi.json new file mode 100644 index 0000000000..ae09431233 --- /dev/null +++ b/precompiles/mint/abi.json @@ -0,0 +1 @@ +[{"inputs":[],"name":"minter","outputs":[{"components":[{"internalType":"string","name":"startDate","type":"string"},{"internalType":"string","name":"endDate","type":"string"},{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint64","name":"totalMintAmount","type":"uint64"},{"internalType":"uint64","name":"remainingMintAmount","type":"uint64"},{"internalType":"uint64","name":"lastMintAmount","type":"uint64"},{"internalType":"string","name":"lastMintDate","type":"string"},{"internalType":"uint64","name":"lastMintHeight","type":"uint64"}],"internalType":"struct IMint.Minter","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"params","outputs":[{"components":[{"internalType":"string","name":"mintDenom","type":"string"},{"components":[{"internalType":"string","name":"startDate","type":"string"},{"internalType":"string","name":"endDate","type":"string"},{"internalType":"uint64","name":"tokenReleaseAmount","type":"uint64"}],"internalType":"struct IMint.ScheduledTokenRelease[]","name":"tokenReleaseSchedule","type":"tuple[]"}],"internalType":"struct IMint.MintParams","name":"","type":"tuple"}],"stateMutability":"view","type":"function"}] diff --git a/precompiles/mint/mint.go b/precompiles/mint/mint.go new file mode 100644 index 0000000000..f744fca77f --- /dev/null +++ b/precompiles/mint/mint.go @@ -0,0 +1,185 @@ +package mint + +import ( + "embed" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" +) + +const ( + ParamsMethod = "params" + MinterMethod = "minter" +) + +const ( + MintAddress = "0x0000000000000000000000000000000000001012" +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +type PrecompileExecutor struct { + evmKeeper utils.EVMKeeper + mintQuerier utils.MintQuerier + + ParamsID []byte + MinterID []byte +} + +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { + newAbi := pcommon.MustGetABI(f, "abi.json") + + p := &PrecompileExecutor{ + evmKeeper: keepers.EVMK(), + mintQuerier: keepers.MintQ(), + } + + for name, m := range newAbi.Methods { + switch name { + case ParamsMethod: + p.ParamsID = m.ID + case MinterMethod: + p.MinterID = m.ID + } + } + + return pcommon.NewDynamicGasPrecompile(newAbi, p, common.HexToAddress(MintAddress), "mint"), nil +} + +// RequiredGas returns the required bare minimum gas to execute the precompile. +func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (e.g. auth's NextAccountNumber increments the global + // account number counter, gov's Tally deletes votes), so run every + // view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } + switch method.Name { + case ParamsMethod: + return p.params(ctx, method, args, value) + case MinterMethod: + return p.minter(ctx, method, args, value) + } + return +} + +type ScheduledTokenRelease struct { + StartDate string + EndDate string + TokenReleaseAmount uint64 +} + +type MintParams struct { + MintDenom string + TokenReleaseSchedule []ScheduledTokenRelease +} + +type Minter struct { + StartDate string + EndDate string + Denom string + TotalMintAmount uint64 + RemainingMintAmount uint64 + LastMintAmount uint64 + LastMintDate string + LastMintHeight uint64 +} + +func (p PrecompileExecutor) params(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + + request := &minttypes.QueryParamsRequest{} + response, err := p.mintQuerier.Params(sdk.WrapSDKContext(ctx), request) + if err != nil { + return nil, 0, err + } + + tokenReleaseSchedule := make([]ScheduledTokenRelease, 0, len(response.Params.TokenReleaseSchedule)) + for _, release := range response.Params.TokenReleaseSchedule { + tokenReleaseSchedule = append(tokenReleaseSchedule, ScheduledTokenRelease{ + StartDate: release.StartDate, + EndDate: release.EndDate, + TokenReleaseAmount: release.TokenReleaseAmount, + }) + } + + params := MintParams{ + MintDenom: response.Params.MintDenom, + TokenReleaseSchedule: tokenReleaseSchedule, + } + + bz, err := method.Outputs.Pack(params) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) minter(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + + request := &minttypes.QueryMinterRequest{} + response, err := p.mintQuerier.Minter(sdk.WrapSDKContext(ctx), request) + if err != nil { + return nil, 0, err + } + + minter := Minter{ + StartDate: response.StartDate, + EndDate: response.EndDate, + Denom: response.Denom, + TotalMintAmount: response.TotalMintAmount, + RemainingMintAmount: response.RemainingMintAmount, + LastMintAmount: response.LastMintAmount, + LastMintDate: response.LastMintDate, + LastMintHeight: response.LastMintHeight, + } + + bz, err := method.Outputs.Pack(minter) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper +} + +func (PrecompileExecutor) IsTransaction(string) bool { + return false +} diff --git a/precompiles/mint/mint_test.go b/precompiles/mint/mint_test.go new file mode 100644 index 0000000000..72e24424a1 --- /dev/null +++ b/precompiles/mint/mint_test.go @@ -0,0 +1,113 @@ +package mint_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/mint" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" + "github.com/stretchr/testify/require" +) + +func TestPrecompile_Run_Params(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + // seed a token release schedule through the keeper + mintParams := testApp.MintKeeper.GetParams(ctx) + mintParams.TokenReleaseSchedule = []minttypes.ScheduledTokenRelease{ + { + StartDate: "2024-01-01", + EndDate: "2024-02-01", + TokenReleaseAmount: 1000, + }, + } + testApp.MintKeeper.SetParams(ctx, mintParams) + + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := mint.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*mint.PrecompileExecutor).ParamsID) + require.NoError(t, err) + + expected := mint.MintParams{ + MintDenom: mintParams.MintDenom, + TokenReleaseSchedule: []mint.ScheduledTokenRelease{ + { + StartDate: "2024-01-01", + EndDate: "2024-02-01", + TokenReleaseAmount: 1000, + }, + }, + } + expectedBz, err := method.Outputs.Pack(expected) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, nil, nil, true, false) + require.NoError(t, err) + require.Equal(t, expectedBz, ret) + + // default genesis mint denom is the bond denom + require.Equal(t, "usei", mintParams.MintDenom) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} + +func TestPrecompile_Run_Minter(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + // seed a minter through the keeper + minter := minttypes.NewMinter("2024-01-01", "2024-02-01", "usei", 5000) + testApp.MintKeeper.SetMinter(ctx, minter) + + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := mint.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*mint.PrecompileExecutor).MinterID) + require.NoError(t, err) + + expected := mint.Minter{ + StartDate: minter.StartDate, + EndDate: minter.EndDate, + Denom: minter.Denom, + TotalMintAmount: minter.TotalMintAmount, + RemainingMintAmount: minter.RemainingMintAmount, + LastMintAmount: minter.LastMintAmount, + LastMintDate: minter.LastMintDate, + LastMintHeight: minter.LastMintHeight, + } + expectedBz, err := method.Outputs.Pack(expected) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, nil, nil, true, false) + require.NoError(t, err) + require.Equal(t, expectedBz, ret) + + require.Equal(t, "usei", expected.Denom) + require.Equal(t, uint64(5000), expected.TotalMintAmount) + require.Equal(t, uint64(5000), expected.RemainingMintAmount) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} diff --git a/precompiles/mint/setup.go b/precompiles/mint/setup.go new file mode 100644 index 0000000000..7bc180ff30 --- /dev/null +++ b/precompiles/mint/setup.go @@ -0,0 +1,21 @@ +// Code generated by scripts/bump_version; DO NOT EDIT. + +package mint + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/utils" +) + +func GetVersioned(latestUpgrade string, keepers utils.Keepers) utils.VersionedPrecompiles { + return utils.VersionedPrecompiles{ + latestUpgrade: check(NewPrecompile(keepers)), + } +} + +func check(p vm.PrecompiledContract, err error) vm.PrecompiledContract { + if err != nil { + panic(err) + } + return p +} diff --git a/precompiles/mint/versions b/precompiles/mint/versions new file mode 100644 index 0000000000..5ff3284364 --- /dev/null +++ b/precompiles/mint/versions @@ -0,0 +1 @@ +v6.6 diff --git a/precompiles/params/Params.sol b/precompiles/params/Params.sol new file mode 100644 index 0000000000..f57720f8df --- /dev/null +++ b/precompiles/params/Params.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant PARAMS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001013; + +IParams constant PARAMS_CONTRACT = IParams(PARAMS_PRECOMPILE_ADDRESS); + +interface IParams { + // Queries + function params(string memory subspace, string memory key) external view returns (string memory value); +} diff --git a/precompiles/params/abi.json b/precompiles/params/abi.json new file mode 100644 index 0000000000..0db21dfe38 --- /dev/null +++ b/precompiles/params/abi.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"string","name":"subspace","type":"string"},{"internalType":"string","name":"key","type":"string"}],"name":"params","outputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"view","type":"function"}] diff --git a/precompiles/params/params.go b/precompiles/params/params.go new file mode 100644 index 0000000000..afd26ce5a0 --- /dev/null +++ b/precompiles/params/params.go @@ -0,0 +1,116 @@ +package params + +import ( + "embed" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/params/types/proposal" +) + +const ( + ParamsMethod = "params" +) + +const ( + ParamsAddress = "0x0000000000000000000000000000000000001013" +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +type PrecompileExecutor struct { + evmKeeper utils.EVMKeeper + paramsQuerier utils.ParamsQuerier + + ParamsID []byte +} + +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { + newAbi := pcommon.MustGetABI(f, "abi.json") + + p := &PrecompileExecutor{ + evmKeeper: keepers.EVMK(), + paramsQuerier: keepers.ParamsQ(), + } + + for name, m := range newAbi.Methods { + switch name { + case ParamsMethod: + p.ParamsID = m.ID + } + } + + return pcommon.NewDynamicGasPrecompile(newAbi, p, common.HexToAddress(ParamsAddress), "params"), nil +} + +// RequiredGas returns the required bare minimum gas to execute the precompile. +func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (e.g. auth's NextAccountNumber increments the global + // account number counter, gov's Tally deletes votes), so run every + // view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } + switch method.Name { + case ParamsMethod: + return p.params(ctx, method, args, value) + } + return +} + +func (p PrecompileExecutor) params(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + subspace := args[0].(string) + key := args[1].(string) + + request := &proposal.QueryParamsRequest{ + Subspace: subspace, + Key: key, + } + response, err := p.paramsQuerier.Params(sdk.WrapSDKContext(ctx), request) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(response.Param.Value) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper +} + +func (PrecompileExecutor) IsTransaction(string) bool { + return false +} diff --git a/precompiles/params/params_test.go b/precompiles/params/params_test.go new file mode 100644 index 0000000000..dad8a4e964 --- /dev/null +++ b/precompiles/params/params_test.go @@ -0,0 +1,102 @@ +package params_test + +import ( + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/params" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" + "github.com/stretchr/testify/require" +) + +func TestPrecompile_Run_Params(t *testing.T) { + tests := []struct { + name string + subspace string + key string + wantErr bool + }{ + { + name: "returns staking MaxValidators param", + subspace: "staking", + key: "MaxValidators", + wantErr: false, + }, + { + name: "fails for unknown subspace", + subspace: "notasubspace", + key: "NotAKey", + wantErr: true, + }, + { + name: "fails for empty subspace", + subspace: "", + key: "MaxValidators", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := params.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*params.PrecompileExecutor).ParamsID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack(tt.subspace, tt.key) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, nil, nil, true, false) + if tt.wantErr { + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + return + } + require.NoError(t, err) + + outputs, err := method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + value := outputs[0].(string) + require.NotEmpty(t, value) + // MaxValidators is a JSON-encoded uint32 matching the staking keeper's value + expected := fmt.Sprintf("%d", testApp.StakingKeeper.MaxValidators(ctx)) + require.Equal(t, expected, value) + }) + } +} + +func TestPrecompile_Run_Params_NonPayable(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := params.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*params.PrecompileExecutor).ParamsID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack("staking", "MaxValidators") + require.NoError(t, err) + + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} diff --git a/precompiles/params/setup.go b/precompiles/params/setup.go new file mode 100644 index 0000000000..86cb0edbca --- /dev/null +++ b/precompiles/params/setup.go @@ -0,0 +1,21 @@ +// Code generated by scripts/bump_version; DO NOT EDIT. + +package params + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/utils" +) + +func GetVersioned(latestUpgrade string, keepers utils.Keepers) utils.VersionedPrecompiles { + return utils.VersionedPrecompiles{ + latestUpgrade: check(NewPrecompile(keepers)), + } +} + +func check(p vm.PrecompiledContract, err error) vm.PrecompiledContract { + if err != nil { + panic(err) + } + return p +} diff --git a/precompiles/params/versions b/precompiles/params/versions new file mode 100644 index 0000000000..5ff3284364 --- /dev/null +++ b/precompiles/params/versions @@ -0,0 +1 @@ +v6.6 diff --git a/precompiles/setup.go b/precompiles/setup.go index 9224ec908a..7a800be0c3 100644 --- a/precompiles/setup.go +++ b/precompiles/setup.go @@ -7,17 +7,25 @@ import ( ecommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "github.com/sei-protocol/sei-chain/precompiles/addr" + "github.com/sei-protocol/sei-chain/precompiles/auth" + "github.com/sei-protocol/sei-chain/precompiles/authz" "github.com/sei-protocol/sei-chain/precompiles/bank" "github.com/sei-protocol/sei-chain/precompiles/distribution" + "github.com/sei-protocol/sei-chain/precompiles/evidence" + "github.com/sei-protocol/sei-chain/precompiles/feegrant" "github.com/sei-protocol/sei-chain/precompiles/gov" "github.com/sei-protocol/sei-chain/precompiles/ibc" "github.com/sei-protocol/sei-chain/precompiles/json" + "github.com/sei-protocol/sei-chain/precompiles/mint" "github.com/sei-protocol/sei-chain/precompiles/oracle" "github.com/sei-protocol/sei-chain/precompiles/p256" + "github.com/sei-protocol/sei-chain/precompiles/params" "github.com/sei-protocol/sei-chain/precompiles/pointer" "github.com/sei-protocol/sei-chain/precompiles/pointerview" + "github.com/sei-protocol/sei-chain/precompiles/slashing" "github.com/sei-protocol/sei-chain/precompiles/solo" "github.com/sei-protocol/sei-chain/precompiles/staking" + "github.com/sei-protocol/sei-chain/precompiles/upgrade" "github.com/sei-protocol/sei-chain/precompiles/utils" "github.com/sei-protocol/sei-chain/precompiles/wasmd" ) @@ -58,6 +66,14 @@ func GetCustomPrecompiles( ecommon.HexToAddress(pointerview.PointerViewAddress): pointerview.GetVersioned(latestUpgrade, keepers), ecommon.HexToAddress(p256.P256VerifyAddress): p256.GetVersioned(latestUpgrade, keepers), ecommon.HexToAddress(solo.SoloAddress): solo.GetVersioned(latestUpgrade, keepers), + ecommon.HexToAddress(auth.AuthAddress): auth.GetVersioned(latestUpgrade, keepers), + ecommon.HexToAddress(authz.AuthzAddress): authz.GetVersioned(latestUpgrade, keepers), + ecommon.HexToAddress(evidence.EvidenceAddress): evidence.GetVersioned(latestUpgrade, keepers), + ecommon.HexToAddress(feegrant.FeegrantAddress): feegrant.GetVersioned(latestUpgrade, keepers), + ecommon.HexToAddress(mint.MintAddress): mint.GetVersioned(latestUpgrade, keepers), + ecommon.HexToAddress(params.ParamsAddress): params.GetVersioned(latestUpgrade, keepers), + ecommon.HexToAddress(slashing.SlashingAddress): slashing.GetVersioned(latestUpgrade, keepers), + ecommon.HexToAddress(upgrade.UpgradeAddress): upgrade.GetVersioned(latestUpgrade, keepers), } } diff --git a/precompiles/setup_test.go b/precompiles/setup_test.go new file mode 100644 index 0000000000..869cb8ad18 --- /dev/null +++ b/precompiles/setup_test.go @@ -0,0 +1,55 @@ +package precompiles_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/sei-protocol/sei-chain/precompiles" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + "github.com/stretchr/testify/require" +) + +// TestIsTransactionMatchesABIStateMutability verifies that every precompile +// executor exposing IsTransaction agrees with its ABI: exactly the non-view +// methods are transactions. Several Execute implementations rely on this +// classification to run views on a branched, discarded context, so a method +// misclassified as a view would have its state changes silently dropped (and +// a query misclassified as a transaction would leak querier side effects). +func TestIsTransactionMatchesABIStateMutability(t *testing.T) { + const latest = "latest" + for addr, versioned := range precompiles.GetCustomPrecompiles(latest, &utils.EmptyKeepers{}) { + var ( + name string + executor interface{} + methods map[string]bool // method name -> is view per abi + ) + switch p := versioned[latest].(type) { + case *pcommon.DynamicGasPrecompile: + name, executor = p.GetName(), p.GetExecutor() + methods = viewMethods(p.GetABI().Methods) + case *pcommon.Precompile: + name, executor = p.GetName(), p.GetExecutor() + methods = viewMethods(p.GetABI().Methods) + default: + t.Fatalf("unexpected precompile wrapper type %T at %s", versioned[latest], addr.Hex()) + } + classifier, ok := executor.(interface{ IsTransaction(string) bool }) + if !ok { + // fully dynamic precompiles don't classify their methods + continue + } + for method, isView := range methods { + require.Equalf(t, !isView, classifier.IsTransaction(method), + "%s: IsTransaction(%q) disagrees with the abi stateMutability", name, method) + } + } +} + +func viewMethods(methods map[string]abi.Method) map[string]bool { + out := make(map[string]bool, len(methods)) + for name, m := range methods { + out[name] = m.StateMutability == "view" || m.StateMutability == "pure" + } + return out +} diff --git a/precompiles/slashing/Slashing.sol b/precompiles/slashing/Slashing.sol new file mode 100644 index 0000000000..33619a9a9e --- /dev/null +++ b/precompiles/slashing/Slashing.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant SLASHING_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001014; + +ISlashing constant SLASHING_CONTRACT = ISlashing(SLASHING_PRECOMPILE_ADDRESS); + +interface ISlashing { + // Queries + function params() external view returns (SlashingParams memory params); + + function signingInfo( + string memory consAddress + ) external view returns (SigningInfo memory signingInfo); + + function signingInfos( + bytes memory pageKey + ) external view returns (SigningInfosResponse memory response); + + // Structs + struct SlashingParams { + int64 signedBlocksWindow; + string minSignedPerWindow; + uint64 downtimeJailDuration; + string slashFractionDoubleSign; + string slashFractionDowntime; + } + + struct SigningInfo { + string validatorAddress; + int64 startHeight; + int64 indexOffset; + int64 jailedUntil; + bool tombstoned; + int64 missedBlocksCounter; + } + + struct SigningInfosResponse { + SigningInfo[] signingInfos; + bytes nextKey; + } +} diff --git a/precompiles/slashing/abi.json b/precompiles/slashing/abi.json new file mode 100644 index 0000000000..4f91c7f29b --- /dev/null +++ b/precompiles/slashing/abi.json @@ -0,0 +1,156 @@ +[ + { + "inputs": [], + "name": "params", + "outputs": [ + { + "components": [ + { + "internalType": "int64", + "name": "signedBlocksWindow", + "type": "int64" + }, + { + "internalType": "string", + "name": "minSignedPerWindow", + "type": "string" + }, + { + "internalType": "uint64", + "name": "downtimeJailDuration", + "type": "uint64" + }, + { + "internalType": "string", + "name": "slashFractionDoubleSign", + "type": "string" + }, + { + "internalType": "string", + "name": "slashFractionDowntime", + "type": "string" + } + ], + "internalType": "struct ISlashing.SlashingParams", + "name": "params", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "consAddress", + "type": "string" + } + ], + "name": "signingInfo", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "int64", + "name": "startHeight", + "type": "int64" + }, + { + "internalType": "int64", + "name": "indexOffset", + "type": "int64" + }, + { + "internalType": "int64", + "name": "jailedUntil", + "type": "int64" + }, + { + "internalType": "bool", + "name": "tombstoned", + "type": "bool" + }, + { + "internalType": "int64", + "name": "missedBlocksCounter", + "type": "int64" + } + ], + "internalType": "struct ISlashing.SigningInfo", + "name": "signingInfo", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "pageKey", + "type": "bytes" + } + ], + "name": "signingInfos", + "outputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "int64", + "name": "startHeight", + "type": "int64" + }, + { + "internalType": "int64", + "name": "indexOffset", + "type": "int64" + }, + { + "internalType": "int64", + "name": "jailedUntil", + "type": "int64" + }, + { + "internalType": "bool", + "name": "tombstoned", + "type": "bool" + }, + { + "internalType": "int64", + "name": "missedBlocksCounter", + "type": "int64" + } + ], + "internalType": "struct ISlashing.SigningInfo[]", + "name": "signingInfos", + "type": "tuple[]" + }, + { + "internalType": "bytes", + "name": "nextKey", + "type": "bytes" + } + ], + "internalType": "struct ISlashing.SigningInfosResponse", + "name": "response", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/precompiles/slashing/setup.go b/precompiles/slashing/setup.go new file mode 100644 index 0000000000..da008e8935 --- /dev/null +++ b/precompiles/slashing/setup.go @@ -0,0 +1,21 @@ +// Code generated by scripts/bump_version; DO NOT EDIT. + +package slashing + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/utils" +) + +func GetVersioned(latestUpgrade string, keepers utils.Keepers) utils.VersionedPrecompiles { + return utils.VersionedPrecompiles{ + latestUpgrade: check(NewPrecompile(keepers)), + } +} + +func check(p vm.PrecompiledContract, err error) vm.PrecompiledContract { + if err != nil { + panic(err) + } + return p +} diff --git a/precompiles/slashing/slashing.go b/precompiles/slashing/slashing.go new file mode 100644 index 0000000000..461981bfad --- /dev/null +++ b/precompiles/slashing/slashing.go @@ -0,0 +1,228 @@ +package slashing + +import ( + "embed" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" + slashingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/types" +) + +const ( + ParamsMethod = "params" + SigningInfoMethod = "signingInfo" + SigningInfosMethod = "signingInfos" +) + +const ( + SlashingAddress = "0x0000000000000000000000000000000000001014" +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +type PrecompileExecutor struct { + evmKeeper utils.EVMKeeper + slashingQuerier utils.SlashingQuerier + + ParamsID []byte + SigningInfoID []byte + SigningInfosID []byte +} + +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { + newAbi := pcommon.MustGetABI(f, "abi.json") + + p := &PrecompileExecutor{ + evmKeeper: keepers.EVMK(), + slashingQuerier: keepers.SlashingQ(), + } + + for name, m := range newAbi.Methods { + switch name { + case ParamsMethod: + p.ParamsID = m.ID + case SigningInfoMethod: + p.SigningInfoID = m.ID + case SigningInfosMethod: + p.SigningInfosID = m.ID + } + } + + return pcommon.NewDynamicGasPrecompile(newAbi, p, common.HexToAddress(SlashingAddress), "slashing"), nil +} + +// RequiredGas returns the required bare minimum gas to execute the precompile. +func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (e.g. auth's NextAccountNumber increments the global + // account number counter, gov's Tally deletes votes), so run every + // view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } + switch method.Name { + case ParamsMethod: + return p.params(ctx, method, args, value) + case SigningInfoMethod: + return p.signingInfo(ctx, method, args, value) + case SigningInfosMethod: + return p.signingInfos(ctx, method, args, value) + } + return +} + +type SlashingParams struct { + SignedBlocksWindow int64 + MinSignedPerWindow string + DowntimeJailDuration uint64 + SlashFractionDoubleSign string + SlashFractionDowntime string +} + +type SigningInfo struct { + ValidatorAddress string + StartHeight int64 + IndexOffset int64 + JailedUntil int64 + Tombstoned bool + MissedBlocksCounter int64 +} + +type SigningInfosResponse struct { + SigningInfos []SigningInfo + NextKey []byte +} + +func (p PrecompileExecutor) params(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + + request := &slashingtypes.QueryParamsRequest{} + response, err := p.slashingQuerier.Params(sdk.WrapSDKContext(ctx), request) + if err != nil { + return nil, 0, err + } + + params := SlashingParams{ + SignedBlocksWindow: response.Params.SignedBlocksWindow, + MinSignedPerWindow: response.Params.MinSignedPerWindow.String(), + DowntimeJailDuration: uint64(response.Params.DowntimeJailDuration.Seconds()), + SlashFractionDoubleSign: response.Params.SlashFractionDoubleSign.String(), + SlashFractionDowntime: response.Params.SlashFractionDowntime.String(), + } + + bz, err := method.Outputs.Pack(params) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) signingInfo(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + req := &slashingtypes.QuerySigningInfoRequest{ + ConsAddress: args[0].(string), + } + + resp, err := p.slashingQuerier.SigningInfo(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + signingInfo := convertSigningInfoToPrecompileType(resp.ValSigningInfo) + + bz, err := method.Outputs.Pack(signingInfo) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) signingInfos(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + req := &slashingtypes.QuerySigningInfosRequest{ + Pagination: &query.PageRequest{ + Key: args[0].([]byte), + }, + } + + resp, err := p.slashingQuerier.SigningInfos(sdk.WrapSDKContext(ctx), req) + if err != nil { + return nil, 0, err + } + + res := SigningInfosResponse{ + SigningInfos: make([]SigningInfo, len(resp.Info)), + } + for i, info := range resp.Info { + res.SigningInfos[i] = convertSigningInfoToPrecompileType(info) + } + if resp.Pagination != nil { + res.NextKey = resp.Pagination.NextKey + } + + bz, err := method.Outputs.Pack(res) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func convertSigningInfoToPrecompileType(info slashingtypes.ValidatorSigningInfo) SigningInfo { + return SigningInfo{ + ValidatorAddress: info.Address, + StartHeight: info.StartHeight, + IndexOffset: info.IndexOffset, + JailedUntil: info.JailedUntil.Unix(), + Tombstoned: info.Tombstoned, + MissedBlocksCounter: info.MissedBlocksCounter, + } +} + +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper +} + +func (PrecompileExecutor) IsTransaction(string) bool { + return false +} diff --git a/precompiles/slashing/slashing_test.go b/precompiles/slashing/slashing_test.go new file mode 100644 index 0000000000..71f9ed6d2a --- /dev/null +++ b/precompiles/slashing/slashing_test.go @@ -0,0 +1,193 @@ +package slashing_test + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/slashing" + "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/ed25519" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + slashingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/types" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" + "github.com/stretchr/testify/require" +) + +func mockConsAddress() sdk.ConsAddress { + privKey := ed25519.GenPrivKey() + return sdk.ConsAddress(privKey.PubKey().Address()) +} + +func TestPrecompile_Run_Params(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := slashing.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*slashing.PrecompileExecutor).ParamsID) + require.NoError(t, err) + + slashingParams := testApp.SlashingKeeper.GetParams(ctx) + expected := slashing.SlashingParams{ + SignedBlocksWindow: slashingParams.SignedBlocksWindow, + MinSignedPerWindow: slashingParams.MinSignedPerWindow.String(), + DowntimeJailDuration: uint64(slashingParams.DowntimeJailDuration.Seconds()), + SlashFractionDoubleSign: slashingParams.SlashFractionDoubleSign.String(), + SlashFractionDowntime: slashingParams.SlashFractionDowntime.String(), + } + expectedBz, err := method.Outputs.Pack(expected) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, nil, nil, true, false) + require.NoError(t, err) + require.Equal(t, expectedBz, ret) + + // sanity check against default genesis + require.Greater(t, expected.SignedBlocksWindow, int64(0)) + require.NotEmpty(t, expected.MinSignedPerWindow) + require.NotEmpty(t, expected.SlashFractionDoubleSign) + require.NotEmpty(t, expected.SlashFractionDowntime) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} + +func TestPrecompile_Run_SigningInfo(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + // seed a signing info through the keeper + consAddr := mockConsAddress() + info := slashingtypes.NewValidatorSigningInfo(consAddr, 1, 5, time.Unix(1000, 0).UTC(), true, 7) + testApp.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, info) + + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := slashing.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*slashing.PrecompileExecutor).SigningInfoID) + require.NoError(t, err) + + expected := slashing.SigningInfo{ + ValidatorAddress: consAddr.String(), + StartHeight: 1, + IndexOffset: 5, + JailedUntil: 1000, + Tombstoned: true, + MissedBlocksCounter: 7, + } + expectedBz, err := method.Outputs.Pack(expected) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack(consAddr.String()) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, nil, nil, true, false) + require.NoError(t, err) + require.Equal(t, expectedBz, ret) + + // unknown validator reverts + unknownInputs, err := method.Inputs.Pack(mockConsAddress().String()) + require.NoError(t, err) + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, unknownInputs...), 100000, nil, nil, true, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + + // invalid bech32 address reverts + invalidInputs, err := method.Inputs.Pack("notanaddress") + require.NoError(t, err) + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, invalidInputs...), 100000, nil, nil, true, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} + +func TestPrecompile_Run_SigningInfos(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + // seed two signing infos through the keeper + consAddr1 := mockConsAddress() + consAddr2 := mockConsAddress() + info1 := slashingtypes.NewValidatorSigningInfo(consAddr1, 1, 2, time.Unix(100, 0).UTC(), false, 3) + info2 := slashingtypes.NewValidatorSigningInfo(consAddr2, 4, 5, time.Unix(200, 0).UTC(), true, 6) + testApp.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr1, info1) + testApp.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr2, info2) + + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := slashing.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*slashing.PrecompileExecutor).SigningInfosID) + require.NoError(t, err) + + // build the expected list from the keeper's own iteration (same store + // order as the paginated query); state may contain infos seeded by + // other tests sharing EVMTestApp + expected := slashing.SigningInfosResponse{} + testApp.SlashingKeeper.IterateValidatorSigningInfos(ctx, func(_ sdk.ConsAddress, info slashingtypes.ValidatorSigningInfo) bool { + expected.SigningInfos = append(expected.SigningInfos, slashing.SigningInfo{ + ValidatorAddress: info.Address, + StartHeight: info.StartHeight, + IndexOffset: info.IndexOffset, + JailedUntil: info.JailedUntil.Unix(), + Tombstoned: info.Tombstoned, + MissedBlocksCounter: info.MissedBlocksCounter, + }) + return false + }) + require.GreaterOrEqual(t, len(expected.SigningInfos), 2) + require.Contains(t, expected.SigningInfos, slashing.SigningInfo{ + ValidatorAddress: consAddr1.String(), + StartHeight: 1, + IndexOffset: 2, + JailedUntil: 100, + Tombstoned: false, + MissedBlocksCounter: 3, + }) + require.Contains(t, expected.SigningInfos, slashing.SigningInfo{ + ValidatorAddress: consAddr2.String(), + StartHeight: 4, + IndexOffset: 5, + JailedUntil: 200, + Tombstoned: true, + MissedBlocksCounter: 6, + }) + expectedBz, err := method.Outputs.Pack(expected) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack([]byte{}) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, nil, nil, true, false) + require.NoError(t, err) + require.Equal(t, expectedBz, ret) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} diff --git a/precompiles/slashing/versions b/precompiles/slashing/versions new file mode 100644 index 0000000000..5ff3284364 --- /dev/null +++ b/precompiles/slashing/versions @@ -0,0 +1 @@ +v6.6 diff --git a/precompiles/upgrade/Upgrade.sol b/precompiles/upgrade/Upgrade.sol new file mode 100644 index 0000000000..df64a24bdc --- /dev/null +++ b/precompiles/upgrade/Upgrade.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant UPGRADE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001015; + +IUpgrade constant UPGRADE_CONTRACT = IUpgrade(UPGRADE_PRECOMPILE_ADDRESS); + +interface IUpgrade { + // Queries + // Returns the currently scheduled upgrade plan, or a zero-valued plan if + // no upgrade is scheduled. + function currentPlan() external view returns (Plan memory plan); + + // Returns the block height at which the named upgrade was applied, or 0 + // if it has not been applied. + function appliedPlan( + string memory name + ) external view returns (int64 height); + + // Returns the upgraded consensus state stored for the given last height, + // or empty bytes if none is stored. + function upgradedConsensusState( + int64 lastHeight + ) external view returns (bytes memory consensusState); + + // Returns the consensus versions of app modules. An empty moduleName + // returns all modules; a specific moduleName returns just that module. + function moduleVersions( + string memory moduleName + ) external view returns (ModuleVersion[] memory versions); + + // Structs + struct Plan { + string name; + int64 height; + string info; + } + + struct ModuleVersion { + string name; + uint64 version; + } +} diff --git a/precompiles/upgrade/abi.json b/precompiles/upgrade/abi.json new file mode 100644 index 0000000000..ae2f3855b0 --- /dev/null +++ b/precompiles/upgrade/abi.json @@ -0,0 +1,101 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "name": "appliedPlan", + "outputs": [ + { + "internalType": "int64", + "name": "height", + "type": "int64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "currentPlan", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "int64", + "name": "height", + "type": "int64" + }, + { + "internalType": "string", + "name": "info", + "type": "string" + } + ], + "internalType": "struct IUpgrade.Plan", + "name": "plan", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "moduleName", + "type": "string" + } + ], + "name": "moduleVersions", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "internalType": "struct IUpgrade.ModuleVersion[]", + "name": "versions", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int64", + "name": "lastHeight", + "type": "int64" + } + ], + "name": "upgradedConsensusState", + "outputs": [ + { + "internalType": "bytes", + "name": "consensusState", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/precompiles/upgrade/setup.go b/precompiles/upgrade/setup.go new file mode 100644 index 0000000000..096ccea9ec --- /dev/null +++ b/precompiles/upgrade/setup.go @@ -0,0 +1,21 @@ +// Code generated by scripts/bump_version; DO NOT EDIT. + +package upgrade + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/utils" +) + +func GetVersioned(latestUpgrade string, keepers utils.Keepers) utils.VersionedPrecompiles { + return utils.VersionedPrecompiles{ + latestUpgrade: check(NewPrecompile(keepers)), + } +} + +func check(p vm.PrecompiledContract, err error) vm.PrecompiledContract { + if err != nil { + panic(err) + } + return p +} diff --git a/precompiles/upgrade/upgrade.go b/precompiles/upgrade/upgrade.go new file mode 100644 index 0000000000..5d32169ef7 --- /dev/null +++ b/precompiles/upgrade/upgrade.go @@ -0,0 +1,234 @@ +package upgrade + +import ( + "embed" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + pcommon "github.com/sei-protocol/sei-chain/precompiles/common" + "github.com/sei-protocol/sei-chain/precompiles/utils" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + upgradetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/upgrade/types" +) + +const ( + CurrentPlanMethod = "currentPlan" + AppliedPlanMethod = "appliedPlan" + UpgradedConsensusStateMethod = "upgradedConsensusState" + ModuleVersionsMethod = "moduleVersions" +) + +const ( + UpgradeAddress = "0x0000000000000000000000000000000000001015" +) + +// Embed abi json file to the executable binary. Needed when importing as dependency. +// +//go:embed abi.json +var f embed.FS + +type PrecompileExecutor struct { + evmKeeper utils.EVMKeeper + upgradeQuerier utils.UpgradeQuerier + + CurrentPlanID []byte + AppliedPlanID []byte + UpgradedConsensusStateID []byte + ModuleVersionsID []byte +} + +func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) { + newAbi := pcommon.MustGetABI(f, "abi.json") + + p := &PrecompileExecutor{ + evmKeeper: keepers.EVMK(), + upgradeQuerier: keepers.UpgradeQ(), + } + + for name, m := range newAbi.Methods { + switch name { + case CurrentPlanMethod: + p.CurrentPlanID = m.ID + case AppliedPlanMethod: + p.AppliedPlanID = m.ID + case UpgradedConsensusStateMethod: + p.UpgradedConsensusStateID = m.ID + case ModuleVersionsMethod: + p.ModuleVersionsID = m.ID + } + } + + return pcommon.NewDynamicGasPrecompile(newAbi, p, common.HexToAddress(UpgradeAddress), "upgrade"), nil +} + +// RequiredGas returns the required bare minimum gas to execute the precompile. +func (p PrecompileExecutor) RequiredGas(input []byte, method *abi.Method) uint64 { + return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) +} + +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, callingContract common.Address, args []interface{}, value *big.Int, readOnly bool, evm *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (bz []byte, remainingGas uint64, err error) { + // Needed to catch gas meter panics + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("execution reverted: %v", r) + } + }() + if !p.IsTransaction(method.Name) { + // Queries must not mutate state even when the underlying querier has + // side effects (e.g. auth's NextAccountNumber increments the global + // account number counter, gov's Tally deletes votes), so run every + // view on a branched context and discard the writes. + ctx, _ = ctx.CacheContext() + } + switch method.Name { + case CurrentPlanMethod: + return p.currentPlan(ctx, method, args, value) + case AppliedPlanMethod: + return p.appliedPlan(ctx, method, args, value) + case UpgradedConsensusStateMethod: + return p.upgradedConsensusState(ctx, method, args, value) + case ModuleVersionsMethod: + return p.moduleVersions(ctx, method, args, value) + } + return +} + +type Plan struct { + Name string + Height int64 + Info string +} + +type ModuleVersion struct { + Name string + Version uint64 +} + +func (p PrecompileExecutor) currentPlan(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + + request := &upgradetypes.QueryCurrentPlanRequest{} + response, err := p.upgradeQuerier.CurrentPlan(sdk.WrapSDKContext(ctx), request) + if err != nil { + return nil, 0, err + } + + // no plan scheduled returns a zero-valued plan + plan := Plan{} + if response.Plan != nil { + plan = Plan{ + Name: response.Plan.Name, + Height: response.Plan.Height, + Info: response.Plan.Info, + } + } + + bz, err := method.Outputs.Pack(plan) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) appliedPlan(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + request := &upgradetypes.QueryAppliedPlanRequest{ + Name: args[0].(string), + } + response, err := p.upgradeQuerier.AppliedPlan(sdk.WrapSDKContext(ctx), request) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(response.Height) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) upgradedConsensusState(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + // The request type is marked deprecated upstream, but it is still the + // wire type of the Query/UpgradedConsensusState rpc this method mirrors. + request := &upgradetypes.QueryUpgradedConsensusStateRequest{ //nolint:staticcheck + LastHeight: args[0].(int64), + } + response, err := p.upgradeQuerier.UpgradedConsensusState(sdk.WrapSDKContext(ctx), request) + if err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(response.UpgradedConsensusState) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) moduleVersions(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, uint64, error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + request := &upgradetypes.QueryModuleVersionsRequest{ + ModuleName: args[0].(string), + } + response, err := p.upgradeQuerier.ModuleVersions(sdk.WrapSDKContext(ctx), request) + if err != nil { + return nil, 0, err + } + + versions := make([]ModuleVersion, 0, len(response.ModuleVersions)) + for _, mv := range response.ModuleVersions { + if mv == nil { + continue + } + versions = append(versions, ModuleVersion{ + Name: mv.Name, + Version: mv.Version, + }) + } + + bz, err := method.Outputs.Pack(versions) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { + return p.evmKeeper +} + +func (PrecompileExecutor) IsTransaction(string) bool { + return false +} diff --git a/precompiles/upgrade/upgrade_test.go b/precompiles/upgrade/upgrade_test.go new file mode 100644 index 0000000000..2ddd2153c8 --- /dev/null +++ b/precompiles/upgrade/upgrade_test.go @@ -0,0 +1,204 @@ +package upgrade_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/precompiles/upgrade" + upgradetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/upgrade/types" + tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + "github.com/sei-protocol/sei-chain/x/evm/state" + "github.com/stretchr/testify/require" +) + +func TestPrecompile_Run_CurrentPlan(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := upgrade.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*upgrade.PrecompileExecutor).CurrentPlanID) + require.NoError(t, err) + + // no plan scheduled returns a zero-valued plan + expectedEmptyBz, err := method.Outputs.Pack(upgrade.Plan{}) + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, nil, nil, true, false) + require.NoError(t, err) + require.Equal(t, expectedEmptyBz, ret) + + // schedule a plan and expect it to round-trip + plan := upgradetypes.Plan{ + Name: "test-upgrade", + Height: ctx.BlockHeight() + 100, + Info: "test-info", + } + require.NoError(t, testApp.UpgradeKeeper.ScheduleUpgrade(ctx, plan)) + + expectedBz, err := method.Outputs.Pack(upgrade.Plan{ + Name: plan.Name, + Height: plan.Height, + Info: plan.Info, + }) + require.NoError(t, err) + + ret, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, nil, nil, true, false) + require.NoError(t, err) + require.Equal(t, expectedBz, ret) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, method.ID, 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} + +func TestPrecompile_Run_AppliedPlan(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + // seed an applied upgrade through the keeper + testApp.UpgradeKeeper.SetDone(ctx, "applied-upgrade") + + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := upgrade.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*upgrade.PrecompileExecutor).AppliedPlanID) + require.NoError(t, err) + + inputs, err := method.Inputs.Pack("applied-upgrade") + require.NoError(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, nil, nil, true, false) + require.NoError(t, err) + outputs, err := method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + require.Equal(t, ctx.BlockHeight(), outputs[0].(int64)) + + // unknown upgrade name returns 0 without error + unknownInputs, err := method.Inputs.Pack("never-applied") + require.NoError(t, err) + ret, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, unknownInputs...), 100000, nil, nil, true, false) + require.NoError(t, err) + outputs, err = method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + require.Equal(t, int64(0), outputs[0].(int64)) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} + +func TestPrecompile_Run_UpgradedConsensusState(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := upgrade.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*upgrade.PrecompileExecutor).UpgradedConsensusStateID) + require.NoError(t, err) + + // nothing stored returns empty bytes without error + inputs, err := method.Inputs.Pack(int64(123)) + require.NoError(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, nil, nil, true, false) + require.NoError(t, err) + outputs, err := method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + require.Empty(t, outputs[0].([]byte)) + + // seed a consensus state through the keeper and expect it to round-trip + consState := []byte("test-consensus-state") + require.NoError(t, testApp.UpgradeKeeper.SetUpgradedConsensusState(ctx, 123, consState)) + + ret, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, nil, nil, true, false) + require.NoError(t, err) + outputs, err = method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + require.Equal(t, consState, outputs[0].([]byte)) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 100000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} + +func TestPrecompile_Run_ModuleVersions(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + } + + p, err := upgrade.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + method, err := p.ABI.MethodById(p.GetExecutor().(*upgrade.PrecompileExecutor).ModuleVersionsID) + require.NoError(t, err) + + // empty module name returns all modules + inputs, err := method.Inputs.Pack("") + require.NoError(t, err) + ret, _, err := p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, inputs...), 200000, nil, nil, true, false) + require.NoError(t, err) + outputs, err := method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + allVersions := outputs[0].([]struct { + Name string `json:"name"` + Version uint64 `json:"version"` + }) + require.NotEmpty(t, allVersions) + + // a specific module name returns exactly one entry + bankInputs, err := method.Inputs.Pack("bank") + require.NoError(t, err) + ret, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, bankInputs...), 200000, nil, nil, true, false) + require.NoError(t, err) + outputs, err = method.Outputs.Unpack(ret) + require.NoError(t, err) + require.Len(t, outputs, 1) + bankVersions := outputs[0].([]struct { + Name string `json:"name"` + Version uint64 `json:"version"` + }) + require.Len(t, bankVersions, 1) + require.Equal(t, "bank", bankVersions[0].Name) + require.Greater(t, bankVersions[0].Version, uint64(0)) + + // unknown module name reverts + unknownInputs, err := method.Inputs.Pack("notamodule") + require.NoError(t, err) + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, unknownInputs...), 200000, nil, nil, true, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) + + // sending value to a view method reverts + _, _, err = p.RunAndCalculateGas(&evm, common.Address{}, common.Address{}, append(method.ID, bankInputs...), 200000, big.NewInt(1), nil, false, false) + require.Error(t, err) + require.Equal(t, vm.ErrExecutionReverted, err) +} diff --git a/precompiles/upgrade/versions b/precompiles/upgrade/versions new file mode 100644 index 0000000000..5ff3284364 --- /dev/null +++ b/precompiles/upgrade/versions @@ -0,0 +1 @@ +v6.6 diff --git a/precompiles/utils/expected_keepers.go b/precompiles/utils/expected_keepers.go index 905f5ec053..4760584cce 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -11,23 +11,34 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "github.com/sei-protocol/sei-chain/sei-cosmos/client" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/authz" banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" distrtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/distribution/types" + evidencetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/evidence/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/feegrant" govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" + paramsproposal "github.com/sei-protocol/sei-chain/sei-cosmos/x/params/types/proposal" + slashingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/types" stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" + upgradetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/upgrade/types" ibctypes "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/apps/transfer/types" clienttypes "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/02-client/types" "github.com/sei-protocol/sei-chain/utils" + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" oracletypes "github.com/sei-protocol/sei-chain/x/oracle/types" ) type Keepers interface { BankK() BankKeeper BankMS() BankMsgServer + BankQ() BankQuerier EVMK() EVMKeeper AccountK() AccountKeeper + AuthQ() AuthQuerier + AuthzQ() AuthzQuerier OracleK() OracleKeeper WasmdK() WasmdKeeper WasmdVK() WasmdViewKeeper @@ -35,20 +46,32 @@ type Keepers interface { StakingQ() StakingQuerier GovK() GovKeeper GovMS() GovMsgServer + GovQ() GovQuerier DistributionK() DistributionKeeper + DistributionQ() DistributionQuerier + EvidenceQ() EvidenceQuerier + FeegrantQ() FeegrantQuerier + MintQ() MintQuerier + ParamsQ() ParamsQuerier + SlashingQ() SlashingQuerier + UpgradeQ() UpgradeQuerier TransferK() TransferKeeper ClientK() ClientKeeper ConnectionK() ConnectionKeeper ChannelK() ChannelKeeper TxConfig() client.TxConfig + Codec() codec.Codec } type EmptyKeepers struct{} func (ek *EmptyKeepers) BankK() BankKeeper { return nil } func (ek *EmptyKeepers) BankMS() BankMsgServer { return nil } +func (ek *EmptyKeepers) BankQ() BankQuerier { return nil } func (ek *EmptyKeepers) EVMK() EVMKeeper { return nil } func (ek *EmptyKeepers) AccountK() AccountKeeper { return nil } +func (ek *EmptyKeepers) AuthQ() AuthQuerier { return nil } +func (ek *EmptyKeepers) AuthzQ() AuthzQuerier { return nil } func (ek *EmptyKeepers) OracleK() OracleKeeper { return nil } func (ek *EmptyKeepers) WasmdK() WasmdKeeper { return nil } func (ek *EmptyKeepers) WasmdVK() WasmdViewKeeper { return nil } @@ -56,12 +79,23 @@ func (ek *EmptyKeepers) StakingK() StakingKeeper { return nil } func (ek *EmptyKeepers) StakingQ() StakingQuerier { return nil } func (ek *EmptyKeepers) GovK() GovKeeper { return nil } func (ek *EmptyKeepers) GovMS() GovMsgServer { return nil } +func (ek *EmptyKeepers) GovQ() GovQuerier { return nil } func (ek *EmptyKeepers) DistributionK() DistributionKeeper { return nil } -func (ek *EmptyKeepers) TransferK() TransferKeeper { return nil } -func (ek *EmptyKeepers) ClientK() ClientKeeper { return nil } -func (ek *EmptyKeepers) ConnectionK() ConnectionKeeper { return nil } -func (ek *EmptyKeepers) ChannelK() ChannelKeeper { return nil } -func (ek *EmptyKeepers) TxConfig() client.TxConfig { return nil } +func (ek *EmptyKeepers) DistributionQ() DistributionQuerier { + return nil +} +func (ek *EmptyKeepers) EvidenceQ() EvidenceQuerier { return nil } +func (ek *EmptyKeepers) FeegrantQ() FeegrantQuerier { return nil } +func (ek *EmptyKeepers) MintQ() MintQuerier { return nil } +func (ek *EmptyKeepers) ParamsQ() ParamsQuerier { return nil } +func (ek *EmptyKeepers) SlashingQ() SlashingQuerier { return nil } +func (ek *EmptyKeepers) UpgradeQ() UpgradeQuerier { return nil } +func (ek *EmptyKeepers) TransferK() TransferKeeper { return nil } +func (ek *EmptyKeepers) ClientK() ClientKeeper { return nil } +func (ek *EmptyKeepers) ConnectionK() ConnectionKeeper { return nil } +func (ek *EmptyKeepers) ChannelK() ChannelKeeper { return nil } +func (ek *EmptyKeepers) TxConfig() client.TxConfig { return nil } +func (ek *EmptyKeepers) Codec() codec.Codec { return nil } type BankKeeper interface { SendCoins(sdk.Context, sdk.AccAddress, sdk.AccAddress, sdk.Coins) error @@ -210,3 +244,81 @@ type ConnectionKeeper interface { type ChannelKeeper interface { GetChannel(ctx sdk.Context, portID, channelID string) (types.Channel, bool) } + +type BankQuerier interface { + SpendableBalances(ctx context.Context, req *banktypes.QuerySpendableBalancesRequest) (*banktypes.QuerySpendableBalancesResponse, error) + TotalSupply(ctx context.Context, req *banktypes.QueryTotalSupplyRequest) (*banktypes.QueryTotalSupplyResponse, error) + Params(ctx context.Context, req *banktypes.QueryParamsRequest) (*banktypes.QueryParamsResponse, error) + DenomMetadata(c context.Context, req *banktypes.QueryDenomMetadataRequest) (*banktypes.QueryDenomMetadataResponse, error) + DenomsMetadata(c context.Context, req *banktypes.QueryDenomsMetadataRequest) (*banktypes.QueryDenomsMetadataResponse, error) +} + +type AuthQuerier interface { + Accounts(c context.Context, req *authtypes.QueryAccountsRequest) (*authtypes.QueryAccountsResponse, error) + Account(c context.Context, req *authtypes.QueryAccountRequest) (*authtypes.QueryAccountResponse, error) + Params(c context.Context, req *authtypes.QueryParamsRequest) (*authtypes.QueryParamsResponse, error) + NextAccountNumber(c context.Context, req *authtypes.QueryNextAccountNumberRequest) (*authtypes.QueryNextAccountNumberResponse, error) +} + +type AuthzQuerier interface { + Grants(c context.Context, req *authz.QueryGrantsRequest) (*authz.QueryGrantsResponse, error) + GranterGrants(c context.Context, req *authz.QueryGranterGrantsRequest) (*authz.QueryGranterGrantsResponse, error) + GranteeGrants(c context.Context, req *authz.QueryGranteeGrantsRequest) (*authz.QueryGranteeGrantsResponse, error) +} + +type GovQuerier interface { + Proposal(c context.Context, req *govtypes.QueryProposalRequest) (*govtypes.QueryProposalResponse, error) + Proposals(c context.Context, req *govtypes.QueryProposalsRequest) (*govtypes.QueryProposalsResponse, error) + Vote(c context.Context, req *govtypes.QueryVoteRequest) (*govtypes.QueryVoteResponse, error) + Votes(c context.Context, req *govtypes.QueryVotesRequest) (*govtypes.QueryVotesResponse, error) + Params(c context.Context, req *govtypes.QueryParamsRequest) (*govtypes.QueryParamsResponse, error) + Deposit(c context.Context, req *govtypes.QueryDepositRequest) (*govtypes.QueryDepositResponse, error) + Deposits(c context.Context, req *govtypes.QueryDepositsRequest) (*govtypes.QueryDepositsResponse, error) + TallyResult(c context.Context, req *govtypes.QueryTallyResultRequest) (*govtypes.QueryTallyResultResponse, error) +} + +type DistributionQuerier interface { + Params(c context.Context, req *distrtypes.QueryParamsRequest) (*distrtypes.QueryParamsResponse, error) + ValidatorOutstandingRewards(c context.Context, req *distrtypes.QueryValidatorOutstandingRewardsRequest) (*distrtypes.QueryValidatorOutstandingRewardsResponse, error) + ValidatorCommission(c context.Context, req *distrtypes.QueryValidatorCommissionRequest) (*distrtypes.QueryValidatorCommissionResponse, error) + ValidatorSlashes(c context.Context, req *distrtypes.QueryValidatorSlashesRequest) (*distrtypes.QueryValidatorSlashesResponse, error) + DelegationRewards(c context.Context, req *distrtypes.QueryDelegationRewardsRequest) (*distrtypes.QueryDelegationRewardsResponse, error) + DelegatorValidators(c context.Context, req *distrtypes.QueryDelegatorValidatorsRequest) (*distrtypes.QueryDelegatorValidatorsResponse, error) + DelegatorWithdrawAddress(c context.Context, req *distrtypes.QueryDelegatorWithdrawAddressRequest) (*distrtypes.QueryDelegatorWithdrawAddressResponse, error) + CommunityPool(c context.Context, req *distrtypes.QueryCommunityPoolRequest) (*distrtypes.QueryCommunityPoolResponse, error) +} + +type EvidenceQuerier interface { + Evidence(c context.Context, req *evidencetypes.QueryEvidenceRequest) (*evidencetypes.QueryEvidenceResponse, error) + AllEvidence(c context.Context, req *evidencetypes.QueryAllEvidenceRequest) (*evidencetypes.QueryAllEvidenceResponse, error) +} + +type FeegrantQuerier interface { + Allowance(c context.Context, req *feegrant.QueryAllowanceRequest) (*feegrant.QueryAllowanceResponse, error) + Allowances(c context.Context, req *feegrant.QueryAllowancesRequest) (*feegrant.QueryAllowancesResponse, error) + AllowancesByGranter(c context.Context, req *feegrant.QueryAllowancesByGranterRequest) (*feegrant.QueryAllowancesByGranterResponse, error) +} + +type MintQuerier interface { + Params(c context.Context, req *minttypes.QueryParamsRequest) (*minttypes.QueryParamsResponse, error) + Minter(c context.Context, req *minttypes.QueryMinterRequest) (*minttypes.QueryMinterResponse, error) +} + +type ParamsQuerier interface { + Params(c context.Context, req *paramsproposal.QueryParamsRequest) (*paramsproposal.QueryParamsResponse, error) +} + +type SlashingQuerier interface { + Params(c context.Context, req *slashingtypes.QueryParamsRequest) (*slashingtypes.QueryParamsResponse, error) + SigningInfo(c context.Context, req *slashingtypes.QuerySigningInfoRequest) (*slashingtypes.QuerySigningInfoResponse, error) + SigningInfos(c context.Context, req *slashingtypes.QuerySigningInfosRequest) (*slashingtypes.QuerySigningInfosResponse, error) +} + +type UpgradeQuerier interface { + CurrentPlan(c context.Context, req *upgradetypes.QueryCurrentPlanRequest) (*upgradetypes.QueryCurrentPlanResponse, error) + AppliedPlan(c context.Context, req *upgradetypes.QueryAppliedPlanRequest) (*upgradetypes.QueryAppliedPlanResponse, error) + // The request type is marked deprecated upstream but is still the wire + // type of the Query/UpgradedConsensusState rpc. + UpgradedConsensusState(c context.Context, req *upgradetypes.QueryUpgradedConsensusStateRequest) (*upgradetypes.QueryUpgradedConsensusStateResponse, error) //nolint:staticcheck + ModuleVersions(c context.Context, req *upgradetypes.QueryModuleVersionsRequest) (*upgradetypes.QueryModuleVersionsResponse, error) +}