From a41813c973bc97302fb75eaaee34cfc16c7abe8b Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 29 Jul 2026 15:19:02 +0800 Subject: [PATCH 1/8] feat(precompiles): expose module Msg rpcs as precompile methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #3767: add transaction methods for the module Msg rpcs that had no EVM precompile counterpart — bank multiSend, distribution fundCommunityPool, addr associateContractAddress, authz grant/exec/revoke, feegrant grantAllowance/revokeAllowance, and slashing unjail. evidence SubmitEvidence and vesting CreateVestingAccount are intentionally omitted (no evidence router is set, and the vesting module is deprecated); oracle and tokenfactory are out of scope. Co-Authored-By: Claude Fable 5 --- app/precompiles.go | 15 ++ precompiles/addr/Addr.sol | 5 + precompiles/addr/abi.json | 2 +- precompiles/addr/addr.go | 59 ++++- precompiles/addr/addr_test.go | 70 ++++++ precompiles/authz/Authz.sol | 39 +++ precompiles/authz/abi.json | 2 +- precompiles/authz/authz.go | 227 +++++++++++++++++- precompiles/authz/authz_test.go | 115 +++++++++ precompiles/bank/Bank.sol | 7 + precompiles/bank/abi.json | 2 +- precompiles/bank/bank.go | 75 ++++++ precompiles/bank/bank_test.go | 76 ++++++ precompiles/distribution/Distribution.sol | 7 + precompiles/distribution/abi.json | 2 +- precompiles/distribution/distribution.go | 70 +++++- precompiles/distribution/distribution_test.go | 83 +++++++ precompiles/feegrant/Feegrant.sol | 10 + precompiles/feegrant/abi.json | 43 ++++ precompiles/feegrant/feegrant.go | 134 ++++++++++- precompiles/feegrant/feegrant_test.go | 77 ++++++ precompiles/slashing/Slashing.sol | 5 + precompiles/slashing/abi.json | 13 + precompiles/slashing/slashing.go | 73 +++++- precompiles/slashing/slashing_test.go | 91 +++++++ precompiles/utils/expected_keepers.go | 30 +++ 26 files changed, 1293 insertions(+), 39 deletions(-) diff --git a/app/precompiles.go b/app/precompiles.go index bf1b9d751e..19ab50325d 100644 --- a/app/precompiles.go +++ b/app/precompiles.go @@ -5,9 +5,12 @@ import ( "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" + feegrantkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/feegrant/keeper" govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" + slashingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/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" + evmkeeper "github.com/sei-protocol/sei-chain/x/evm/keeper" mintkeeper "github.com/sei-protocol/sei-chain/x/mint/keeper" ) @@ -16,8 +19,10 @@ type PrecompileKeepers struct { putils.BankMsgServer putils.BankQuerier putils.EVMKeeper + putils.EvmMsgServer putils.AccountKeeper putils.AuthQuerier + putils.AuthzMsgServer putils.AuthzQuerier putils.OracleKeeper putils.WasmdKeeper @@ -30,9 +35,11 @@ type PrecompileKeepers struct { putils.DistributionKeeper putils.DistributionQuerier putils.EvidenceQuerier + putils.FeegrantMsgServer putils.FeegrantQuerier putils.MintQuerier putils.ParamsQuerier + putils.SlashingMsgServer putils.SlashingQuerier putils.UpgradeQuerier putils.TransferKeeper @@ -49,8 +56,10 @@ func NewPrecompileKeepers(a *App) *PrecompileKeepers { BankMsgServer: bankkeeper.NewMsgServerImpl(a.BankKeeper), BankQuerier: a.BankKeeper, EVMKeeper: &a.EvmKeeper, + EvmMsgServer: evmkeeper.NewMsgServerImpl(&a.EvmKeeper), AccountKeeper: a.AccountKeeper, AuthQuerier: a.AccountKeeper, + AuthzMsgServer: a.AuthzKeeper, AuthzQuerier: a.AuthzKeeper, OracleKeeper: a.OracleKeeper, WasmdKeeper: wasmkeeper.NewDefaultPermissionKeeper(a.WasmKeeper), @@ -63,9 +72,11 @@ func NewPrecompileKeepers(a *App) *PrecompileKeepers { DistributionKeeper: a.DistrKeeper, DistributionQuerier: a.DistrKeeper, EvidenceQuerier: a.EvidenceKeeper, + FeegrantMsgServer: feegrantkeeper.NewMsgServerImpl(a.FeeGrantKeeper), FeegrantQuerier: a.FeeGrantKeeper, MintQuerier: mintkeeper.NewQuerier(a.MintKeeper), ParamsQuerier: a.ParamsKeeper, + SlashingMsgServer: slashingkeeper.NewMsgServerImpl(a.SlashingKeeper), SlashingQuerier: a.SlashingKeeper, UpgradeQuerier: a.UpgradeKeeper, TransferKeeper: a.TransferKeeper, @@ -81,8 +92,10 @@ func (pk *PrecompileKeepers) BankK() putils.BankKeeper { return 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) EvmMS() putils.EvmMsgServer { return pk.EvmMsgServer } func (pk *PrecompileKeepers) AccountK() putils.AccountKeeper { return pk.AccountKeeper } func (pk *PrecompileKeepers) AuthQ() putils.AuthQuerier { return pk.AuthQuerier } +func (pk *PrecompileKeepers) AuthzMS() putils.AuthzMsgServer { return pk.AuthzMsgServer } 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 } @@ -97,9 +110,11 @@ func (pk *PrecompileKeepers) DistributionQ() putils.DistributionQuerier { return pk.DistributionQuerier } func (pk *PrecompileKeepers) EvidenceQ() putils.EvidenceQuerier { return pk.EvidenceQuerier } +func (pk *PrecompileKeepers) FeegrantMS() putils.FeegrantMsgServer { return pk.FeegrantMsgServer } 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) SlashingMS() putils.SlashingMsgServer { return pk.SlashingMsgServer } 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 } diff --git a/precompiles/addr/Addr.sol b/precompiles/addr/Addr.sol index e83f62da77..b8521a08e5 100644 --- a/precompiles/addr/Addr.sol +++ b/precompiles/addr/Addr.sol @@ -21,6 +21,11 @@ interface IAddr { string memory pubKeyHex ) external returns (string memory seiAddr, address evmAddr); + // Associates a CosmWasm contract address with its EVM counterpart + function associateContractAddress( + string memory cwAddr + ) external returns (string memory seiAddr, address evmAddr); + // Queries function getSeiAddr(address addr) external view returns (string memory response); function getEvmAddr(string memory addr) external view returns (address response); diff --git a/precompiles/addr/abi.json b/precompiles/addr/abi.json index f0425eaf83..356ebb0563 100755 --- a/precompiles/addr/abi.json +++ b/precompiles/addr/abi.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"string","name":"v","type":"string"},{"internalType":"string","name":"r","type":"string"},{"internalType":"string","name":"s","type":"string"},{"internalType":"string","name":"customMessage","type":"string"}],"name":"associate","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"pubKeyHex","type":"string"}],"name":"associatePubKey","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getSeiAddr","outputs":[{"internalType":"string","name":"response","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"addr","type":"string"}],"name":"getEvmAddr","outputs":[{"internalType":"address","name":"response","type":"address"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"string","name":"v","type":"string"},{"internalType":"string","name":"r","type":"string"},{"internalType":"string","name":"s","type":"string"},{"internalType":"string","name":"customMessage","type":"string"}],"name":"associate","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"pubKeyHex","type":"string"}],"name":"associatePubKey","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"cwAddr","type":"string"}],"name":"associateContractAddress","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getSeiAddr","outputs":[{"internalType":"string","name":"response","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"addr","type":"string"}],"name":"getEvmAddr","outputs":[{"internalType":"address","name":"response","type":"address"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/precompiles/addr/addr.go b/precompiles/addr/addr.go index ec8f435654..c21ab04430 100644 --- a/precompiles/addr/addr.go +++ b/precompiles/addr/addr.go @@ -30,10 +30,11 @@ import ( ) const ( - GetSeiAddressMethod = "getSeiAddr" - GetEvmAddressMethod = "getEvmAddr" - Associate = "associate" - AssociatePubKey = "associatePubKey" + GetSeiAddressMethod = "getSeiAddr" + GetEvmAddressMethod = "getEvmAddr" + Associate = "associate" + AssociatePubKey = "associatePubKey" + AssociateContractAddress = "associateContractAddress" ) const ( @@ -47,13 +48,15 @@ var f embed.FS type PrecompileExecutor struct { evmKeeper putils.EVMKeeper + evmMsgServer putils.EvmMsgServer bankKeeper putils.BankKeeper accountKeeper putils.AccountKeeper - GetSeiAddressID []byte - GetEvmAddressID []byte - AssociateID []byte - AssociatePubKeyID []byte + GetSeiAddressID []byte + GetEvmAddressID []byte + AssociateID []byte + AssociatePubKeyID []byte + AssociateContractAddressID []byte } func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error) { @@ -62,6 +65,7 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error p := &PrecompileExecutor{ evmKeeper: keepers.EVMK(), + evmMsgServer: keepers.EvmMS(), bankKeeper: keepers.BankK(), accountKeeper: keepers.AccountK(), } @@ -76,6 +80,8 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error p.AssociateID = m.ID case AssociatePubKey: p.AssociatePubKeyID = m.ID + case AssociateContractAddress: + p.AssociateContractAddressID = m.ID } } @@ -84,13 +90,13 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error // 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.AssociateID) || bytes.Equal(method.ID, p.AssociatePubKeyID) { + if bytes.Equal(method.ID, p.AssociateID) || bytes.Equal(method.ID, p.AssociatePubKeyID) || bytes.Equal(method.ID, p.AssociateContractAddressID) { return 50000 } return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) } -func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, _ common.Address, _ common.Address, args []interface{}, value *big.Int, readOnly bool, _ *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (ret []byte, remainingGas uint64, err error) { +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, _ common.Address, args []interface{}, value *big.Int, readOnly bool, _ *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (ret []byte, remainingGas uint64, err error) { // Needed to catch gas meter panics defer func() { if r := recover(); r != nil { @@ -112,6 +118,11 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, _ commo return nil, 0, errors.New("cannot call associate pub key precompile from staticcall") } return p.associatePublicKey(ctx, method, args, value) + case AssociateContractAddress: + if readOnly { + return nil, 0, errors.New("cannot call associate contract address precompile from staticcall") + } + return p.associateContractAddress(ctx, method, caller, args, value) } return } @@ -254,9 +265,35 @@ func (p PrecompileExecutor) associateAddresses(ctx sdk.Context, method *abi.Meth return ret, pcommon.GetRemainingGas(ctx, p.evmKeeper), err } +func (p PrecompileExecutor) associateContractAddress(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, err error) { + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + msg := &types.MsgAssociateContractAddress{ + Sender: p.evmKeeper.GetSeiAddressOrDefault(ctx, caller).String(), + Address: args[0].(string), + } + if err := msg.ValidateBasic(); err != nil { + return nil, 0, err + } + + if _, err := p.evmMsgServer.AssociateContractAddress(sdk.WrapSDKContext(ctx), msg); err != nil { + return nil, 0, err + } + + seiAddr := sdk.MustAccAddressFromBech32(msg.Address) + ret, err = method.Outputs.Pack(seiAddr.String(), common.BytesToAddress(seiAddr)) + return ret, pcommon.GetRemainingGas(ctx, p.evmKeeper), err +} + func (PrecompileExecutor) IsTransaction(method string) bool { switch method { - case Associate, AssociatePubKey: + case Associate, AssociatePubKey, AssociateContractAddress: return true default: return false diff --git a/precompiles/addr/addr_test.go b/precompiles/addr/addr_test.go index 68ad339939..281416636c 100644 --- a/precompiles/addr/addr_test.go +++ b/precompiles/addr/addr_test.go @@ -5,14 +5,18 @@ import ( "fmt" "math/big" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/sei-protocol/sei-chain/precompiles/addr" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" + evmkeeper "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/stretchr/testify/require" ) @@ -404,3 +408,69 @@ func TestGetAddr(t *testing.T) { require.Equal(t, 1, len(unpacked)) require.Equal(t, evmAddr, unpacked[0].(common.Address)) } + +func TestAssociateContractAddress(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2).WithBlockTime(time.Now()) + k := &testApp.EvmKeeper + + // a CW->ERC20 pointer is a real wasm contract, which is what + // associateContractAddress expects to be pointed at + dummySeiAddr, dummyEvmAddr := testkeeper.MockAddressPair() + res, err := evmkeeper.NewMsgServerImpl(k).RegisterPointer(sdk.WrapSDKContext(ctx), &evmtypes.MsgRegisterPointer{ + Sender: dummySeiAddr.String(), + PointerType: evmtypes.PointerType_ERC20, + ErcAddress: dummyEvmAddr.Hex(), + }) + require.Nil(t, err) + cwAddr := res.PointerAddress + + callerPrivKey := testkeeper.MockPrivateKey() + callerSeiAddress, callerEvmAddress := testkeeper.PrivateKeyToAddresses(callerPrivKey) + k.SetAddressMapping(ctx, callerSeiAddress, callerEvmAddress) + + pre, err := addr.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + executor := pre.GetExecutor().(*addr.PrecompileExecutor) + method, err := pre.ABI.MethodById(executor.AssociateContractAddressID) + require.Nil(t, err) + + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: callerEvmAddress}} + + args, err := method.Inputs.Pack(cwAddr) + require.Nil(t, err) + + // should error because of read only call + _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, args...), 100000, nil, nil, true, false) + require.NotNil(t, err) + // should error because it's not payable + _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, args...), 100000, big.NewInt(1), nil, false, false) + require.NotNil(t, err) + // should error because the address is not a wasm contract + nonContractArgs, err := method.Inputs.Pack(dummySeiAddr.String()) + require.Nil(t, err) + _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, nonContractArgs...), 100000, nil, nil, false, false) + require.NotNil(t, err) + // should error because the address is not valid bech32 + invalidArgs, err := method.Inputs.Pack("not-bech32") + require.Nil(t, err) + _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, invalidArgs...), 100000, nil, nil, false, false) + require.NotNil(t, err) + + // happy path + ret, _, err := pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, args...), 100000, nil, nil, false, false) + require.Nil(t, err) + outputs, err := method.Outputs.Unpack(ret) + require.Nil(t, err) + require.Equal(t, cwAddr, outputs[0].(string)) + cwSeiAddr := sdk.MustAccAddressFromBech32(cwAddr) + require.Equal(t, common.BytesToAddress(cwSeiAddr), outputs[1].(common.Address)) + associatedEvmAddr, found := k.GetEVMAddress(statedb.Ctx(), cwSeiAddr) + require.True(t, found) + require.Equal(t, common.BytesToAddress(cwSeiAddr), associatedEvmAddr) + + // re-associating the same contract should fail + _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, args...), 100000, nil, nil, false, false) + require.NotNil(t, err) +} diff --git a/precompiles/authz/Authz.sol b/precompiles/authz/Authz.sol index e475869aad..d82cd9710a 100644 --- a/precompiles/authz/Authz.sol +++ b/precompiles/authz/Authz.sol @@ -6,6 +6,45 @@ address constant AUTHZ_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000 IAuthz constant AUTHZ_CONTRACT = IAuthz(AUTHZ_PRECOMPILE_ADDRESS); interface IAuthz { + // Transactions + + /** + * @notice Grant an authorization from the caller (granter) to a grantee + * @param grantee The grantee's EVM address (must be associated with a Sei address) + * @param authorization The authorization as protobuf-JSON bytes, e.g. + * {"@type":"/cosmos.bank.v1beta1.SendAuthorization","spend_limit":[{"denom":"usei","amount":"100"}]} + * @param expiration The expiration time of the grant as a Unix timestamp in seconds + * @return success True if the grant was stored successfully + */ + function grant( + address grantee, + bytes memory authorization, + int64 expiration + ) external returns (bool success); + + /** + * @notice Execute messages with the caller as the authz grantee. Messages + * whose signer is not the caller require a matching grant. EVM + * messages are not allowed. + * @param msgs The messages to execute, each as protobuf-JSON bytes, e.g. + * {"@type":"/cosmos.bank.v1beta1.MsgSend","from_address":"sei1...","to_address":"sei1...","amount":[...]} + * @return responses The protobuf-encoded response of each message + */ + function exec( + bytes[] memory msgs + ) external returns (bytes[] memory responses); + + /** + * @notice Revoke the caller's grant to a grantee for the given message type URL + * @param grantee The grantee's EVM address (must be associated with a Sei address) + * @param msgTypeUrl The message type URL of the authorization to revoke, e.g. "/cosmos.bank.v1beta1.MsgSend" + * @return success True if the grant was revoked successfully + */ + function revoke( + address grantee, + string memory msgTypeUrl + ) external returns (bool success); + // Queries /** diff --git a/precompiles/authz/abi.json b/precompiles/authz/abi.json index 17e9770acf..a4b19c3790 100644 --- a/precompiles/authz/abi.json +++ b/precompiles/authz/abi.json @@ -1 +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"}] +[{"inputs":[{"internalType":"bytes[]","name":"msgs","type":"bytes[]"}],"name":"exec","outputs":[{"internalType":"bytes[]","name":"responses","type":"bytes[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"grantee","type":"address"},{"internalType":"bytes","name":"authorization","type":"bytes"},{"internalType":"int64","name":"expiration","type":"int64"}],"name":"grant","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"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"},{"inputs":[{"internalType":"address","name":"grantee","type":"address"},{"internalType":"string","name":"msgTypeUrl","type":"string"}],"name":"revoke","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] diff --git a/precompiles/authz/authz.go b/precompiles/authz/authz.go index 30afd111bc..1b45fb60d7 100644 --- a/precompiles/authz/authz.go +++ b/precompiles/authz/authz.go @@ -2,8 +2,10 @@ package authz import ( "embed" + "errors" "fmt" "math/big" + "time" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -15,14 +17,23 @@ import ( 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" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) const ( + GrantMethod = "grant" + ExecMethod = "exec" + RevokeMethod = "revoke" GrantsMethod = "grants" GranterGrantsMethod = "granterGrants" GranteeGrantsMethod = "granteeGrants" ) +// maxNestedMsgs caps MsgExec nesting when scanning exec messages, mirroring +// app/antedecorators.AuthzNestedMessageDecorator (which precompile calls +// bypass since they don't go through the cosmos ante handlers). +const maxNestedMsgs = 5 + const ( AuthzAddress = "0x000000000000000000000000000000000000100E" ) @@ -33,10 +44,14 @@ const ( var f embed.FS type PrecompileExecutor struct { - evmKeeper utils.EVMKeeper - authzQuerier utils.AuthzQuerier - cdc codec.Codec - + evmKeeper utils.EVMKeeper + authzMsgServer utils.AuthzMsgServer + authzQuerier utils.AuthzQuerier + cdc codec.Codec + + GrantID []byte + ExecID []byte + RevokeID []byte GrantsID []byte GranterGrantsID []byte GranteeGrantsID []byte @@ -46,13 +61,20 @@ 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(), + evmKeeper: keepers.EVMK(), + authzMsgServer: keepers.AuthzMS(), + authzQuerier: keepers.AuthzQ(), + cdc: keepers.Codec(), } for name, m := range newAbi.Methods { switch name { + case GrantMethod: + p.GrantID = m.ID + case ExecMethod: + p.ExecID = m.ID + case RevokeMethod: + p.RevokeID = m.ID case GrantsMethod: p.GrantsID = m.ID case GranterGrantsMethod: @@ -92,9 +114,189 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller case GranteeGrantsMethod: return p.granteeGrants(ctx, method, args, value) } + + // Transaction methods act on behalf of the caller, so they must not be + // reachable through delegatecall (which would let a contract act on + // behalf of its own caller) or staticcall. + if ctx.EVMPrecompileCalledFromDelegateCall() { + return nil, 0, errors.New("cannot delegatecall authz") + } + if readOnly { + return nil, 0, errors.New("cannot call authz precompile from staticcall") + } + switch method.Name { + case GrantMethod: + return p.grant(ctx, method, caller, args, value) + case ExecMethod: + return p.exec(ctx, method, caller, args, value) + case RevokeMethod: + return p.revoke(ctx, method, caller, args, value) + } return } +// grant stores an authorization from the caller (granter) to the grantee. The +// authorization is a protobuf-JSON encoded Authorization (the same encoding +// the query methods return), e.g. +// {"@type":"/cosmos.bank.v1beta1.SendAuthorization","spend_limit":[...]}. +func (p PrecompileExecutor) grant(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, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 3); err != nil { + return nil, 0, err + } + + granter, found := p.evmKeeper.GetSeiAddress(ctx, caller) + if !found { + return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) + } + + grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + var authorization authztypes.Authorization + if err := p.cdc.UnmarshalInterfaceJSON(args[1].([]byte), &authorization); err != nil { + return nil, 0, fmt.Errorf("failed to parse authorization JSON: %w", err) + } + + expiration := time.Unix(args[2].(int64), 0).UTC() + msg, err := authztypes.NewMsgGrant(granter, grantee, authorization, expiration) + if err != nil { + return nil, 0, err + } + + if err := msg.ValidateBasic(); err != nil { + return nil, 0, err + } + + if _, err := p.authzMsgServer.Grant(sdk.WrapSDKContext(ctx), msg); err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(true) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +// exec executes messages (each protobuf-JSON encoded) with the caller as the +// authz grantee. Messages whose signer is not the caller require a matching +// grant. EVM messages are rejected, mirroring the cosmos ante handler's +// restriction on nested EVM messages in MsgExec. +func (p PrecompileExecutor) exec(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, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + grantee, found := p.evmKeeper.GetSeiAddress(ctx, caller) + if !found { + return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) + } + + msgBzs := args[0].([][]byte) + msgs := make([]sdk.Msg, len(msgBzs)) + for i, msgBz := range msgBzs { + var m sdk.Msg + if err := p.cdc.UnmarshalInterfaceJSON(msgBz, &m); err != nil { + return nil, 0, fmt.Errorf("failed to parse message JSON: %w", err) + } + msgs[i] = m + } + + if err := checkNoNestedEVMMessages(msgs, 0); err != nil { + return nil, 0, err + } + + msg := authztypes.NewMsgExec(grantee, msgs) + if err := msg.ValidateBasic(); err != nil { + return nil, 0, err + } + + resp, err := p.authzMsgServer.Exec(sdk.WrapSDKContext(ctx), &msg) + if err != nil { + return nil, 0, err + } + + results := resp.Results + if results == nil { + results = [][]byte{} + } + bz, err := method.Outputs.Pack(results) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +// revoke deletes the caller's grant to the grantee for the given msg type url. +func (p PrecompileExecutor) revoke(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, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + granter, found := p.evmKeeper.GetSeiAddress(ctx, caller) + if !found { + return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) + } + + grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + msg := authztypes.NewMsgRevoke(granter, grantee, args[1].(string)) + if err := msg.ValidateBasic(); err != nil { + return nil, 0, err + } + + if _, err := p.authzMsgServer.Revoke(sdk.WrapSDKContext(ctx), &msg); err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(true) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +// checkNoNestedEVMMessages rejects EVM messages anywhere in the message tree, +// recursing through nested MsgExecs, mirroring +// app/antedecorators.AuthzNestedMessageDecorator. +func checkNoNestedEVMMessages(msgs []sdk.Msg, nestedLvl int) error { + if nestedLvl >= maxNestedMsgs { + return errors.New("permission denied, more nested msgs than permitted") + } + for _, m := range msgs { + switch m := m.(type) { + case *evmtypes.MsgEVMTransaction: + return errors.New("permission denied, authz exec contains evm message") + case *authztypes.MsgExec: + nested, err := m.GetMessages() + if err != nil { + return err + } + if err := checkNoNestedEVMMessages(nested, nestedLvl+1); err != nil { + return err + } + } + } + return nil +} + type Grant struct { Authorization []byte Expiration int64 @@ -286,6 +488,13 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { return p.evmKeeper } -func (PrecompileExecutor) IsTransaction(string) bool { - return false +// IsTransaction returns true for methods that mutate state; all other authz +// methods are views. +func (PrecompileExecutor) IsTransaction(method string) bool { + switch method { + case GrantMethod, ExecMethod, RevokeMethod: + return true + default: + return false + } } diff --git a/precompiles/authz/authz_test.go b/precompiles/authz/authz_test.go index bc0707fc0c..d53b2bccda 100644 --- a/precompiles/authz/authz_test.go +++ b/precompiles/authz/authz_test.go @@ -8,10 +8,12 @@ import ( "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" + authztypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/authz" 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" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" "github.com/stretchr/testify/require" ) @@ -145,3 +147,116 @@ func TestGranteeGrants(t *testing.T) { require.Contains(t, authorizationJSON, "SendAuthorization") require.Equal(t, expiration.Unix(), grant.FieldByName("Expiration").Int()) } + +func TestGrantExecRevoke(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + cdc := testApp.AppCodec() + + granterSeiAddr, granterEvmAddr := testkeeper.MockAddressPair() + granteeSeiAddr, granteeEvmAddr := testkeeper.MockAddressPair() + thirdSeiAddr, thirdEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, granterSeiAddr, granterEvmAddr) + k.SetAddressMapping(ctx, granteeSeiAddr, granteeEvmAddr) + k.SetAddressMapping(ctx, thirdSeiAddr, thirdEvmAddr) + + amt := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))) + require.Nil(t, testApp.BankKeeper.MintCoins(ctx, evmtypes.ModuleName, amt)) + require.Nil(t, testApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, granterSeiAddr, amt)) + + 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) + sendMsgTypeURL := sdk.MsgTypeURL(&banktypes.MsgSend{}) + + // grant a send authorization to the grantee + authorizationJSON, err := cdc.MarshalInterfaceJSON(banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(500))))) + require.Nil(t, err) + expiration := time.Unix(1893456000, 0).UTC() + + grantMethod, err := p.ABI.MethodById(executor.GrantID) + require.Nil(t, err) + grantArgs, err := grantMethod.Inputs.Pack(granteeEvmAddr, authorizationJSON, expiration.Unix()) + require.Nil(t, err) + + // should error because of read only call + _, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantID, grantArgs...), 1000000, nil, nil, true, false) + require.NotNil(t, err) + // should error because of delegatecall + _, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantID, grantArgs...), 1000000, nil, nil, false, true) + require.NotNil(t, err) + // should error because caller is not associated + _, unassociatedEvmAddr := testkeeper.MockAddressPair() + _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvmAddr, unassociatedEvmAddr, append(executor.GrantID, grantArgs...), 1000000, nil, nil, false, false) + require.NotNil(t, err) + // should error because the authorization JSON is invalid + badAuthorizationArgs, err := grantMethod.Inputs.Pack(granteeEvmAddr, []byte("{"), expiration.Unix()) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantID, badAuthorizationArgs...), 1000000, nil, nil, false, false) + require.NotNil(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantID, grantArgs...), 1000000, nil, nil, false, false) + require.Nil(t, err) + outputs, err := grantMethod.Outputs.Unpack(ret) + require.Nil(t, err) + require.True(t, outputs[0].(bool)) + authorization, _ := testApp.AuthzKeeper.GetCleanAuthorization(statedb.Ctx(), granteeSeiAddr, granterSeiAddr, sendMsgTypeURL) + require.NotNil(t, authorization) + + // exec a send on behalf of the granter as the grantee + sendJSON, err := cdc.MarshalInterfaceJSON(&banktypes.MsgSend{ + FromAddress: granterSeiAddr.String(), + ToAddress: granteeSeiAddr.String(), + Amount: sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(300))), + }) + require.Nil(t, err) + execMethod, err := p.ABI.MethodById(executor.ExecID) + require.Nil(t, err) + execArgs, err := execMethod.Inputs.Pack([][]byte{sendJSON}) + require.Nil(t, err) + + // should error because a third party has no grant for the granter's send + _, _, err = p.RunAndCalculateGas(&evm, thirdEvmAddr, thirdEvmAddr, append(executor.ExecID, execArgs...), 1000000, nil, nil, false, false) + require.NotNil(t, err) + + ret, _, err = p.RunAndCalculateGas(&evm, granteeEvmAddr, granteeEvmAddr, append(executor.ExecID, execArgs...), 1000000, nil, nil, false, false) + require.Nil(t, err) + execOutputs, err := execMethod.Outputs.Unpack(ret) + require.Nil(t, err) + require.Len(t, execOutputs, 1) + require.Equal(t, int64(300), testApp.BankKeeper.GetBalance(statedb.Ctx(), granteeSeiAddr, "usei").Amount.Int64()) + require.Equal(t, int64(700), testApp.BankKeeper.GetBalance(statedb.Ctx(), granterSeiAddr, "usei").Amount.Int64()) + + // exec of an EVM message is rejected + evmMsgJSON, err := cdc.MarshalInterfaceJSON(&evmtypes.MsgEVMTransaction{}) + require.Nil(t, err) + evmExecArgs, err := execMethod.Inputs.Pack([][]byte{evmMsgJSON}) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, granteeEvmAddr, granteeEvmAddr, append(executor.ExecID, evmExecArgs...), 1000000, nil, nil, false, false) + require.NotNil(t, err) + + // exec of a nested MsgExec containing an EVM message is rejected + nestedExec := authztypes.NewMsgExec(granteeSeiAddr, []sdk.Msg{&evmtypes.MsgEVMTransaction{}}) + nestedExecJSON, err := cdc.MarshalInterfaceJSON(&nestedExec) + require.Nil(t, err) + nestedExecArgs, err := execMethod.Inputs.Pack([][]byte{nestedExecJSON}) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, granteeEvmAddr, granteeEvmAddr, append(executor.ExecID, nestedExecArgs...), 1000000, nil, nil, false, false) + require.NotNil(t, err) + + // revoke the grant + revokeMethod, err := p.ABI.MethodById(executor.RevokeID) + require.Nil(t, err) + revokeArgs, err := revokeMethod.Inputs.Pack(granteeEvmAddr, sendMsgTypeURL) + require.Nil(t, err) + ret, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.RevokeID, revokeArgs...), 1000000, nil, nil, false, false) + require.Nil(t, err) + outputs, err = revokeMethod.Outputs.Unpack(ret) + require.Nil(t, err) + require.True(t, outputs[0].(bool)) + authorization, _ = testApp.AuthzKeeper.GetCleanAuthorization(statedb.Ctx(), granteeSeiAddr, granterSeiAddr, sendMsgTypeURL) + require.Nil(t, authorization) +} diff --git a/precompiles/bank/Bank.sol b/precompiles/bank/Bank.sol index 3436ef854d..2e6f85ae71 100644 --- a/precompiles/bank/Bank.sol +++ b/precompiles/bank/Bank.sol @@ -16,6 +16,13 @@ interface IBank { uint256 amount ) external returns (bool success); + function multiSend( + address fromAddress, + address[] memory toAddresses, + string memory denom, + uint256[] memory amounts + ) external returns (bool success); + function sendNative( string memory toNativeAddress ) payable external returns (bool success); diff --git a/precompiles/bank/abi.json b/precompiles/bank/abi.json index 9c408cdbee..2df3247b25 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":"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 +[{"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":"address","name":"fromAddress","type":"address"},{"internalType":"address[]","name":"toAddresses","type":"address[]"},{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"multiSend","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","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 7478d3284e..7db1944092 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -21,6 +21,7 @@ import ( const ( SendMethod = "send" + MultiSendMethod = "multiSend" SendNativeMethod = "sendNative" BalanceMethod = "balance" AllBalancesMethod = "all_balances" @@ -53,6 +54,7 @@ type PrecompileExecutor struct { address common.Address SendID []byte + MultiSendID []byte SendNativeID []byte BalanceID []byte AllBalancesID []byte @@ -116,6 +118,8 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error switch name { case SendMethod: p.SendID = m.ID + case MultiSendMethod: + p.MultiSendID = m.ID case SendNativeMethod: p.SendNativeID = m.ID case BalanceMethod: @@ -167,6 +171,8 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller switch method.Name { case SendMethod: return p.send(ctx, caller, method, args, value, readOnly) + case MultiSendMethod: + return p.multiSend(ctx, caller, method, args, value, readOnly) case SendNativeMethod: return p.sendNative(ctx, method, args, caller, callingContract, value, readOnly, hooks, evm) case BalanceMethod: @@ -248,6 +254,73 @@ func (p PrecompileExecutor) send(ctx sdk.Context, caller common.Address, method return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err } +func (p PrecompileExecutor) multiSend(ctx sdk.Context, caller common.Address, method *abi.Method, args []interface{}, value *big.Int, readOnly bool) ([]byte, uint64, error) { + if readOnly { + return nil, 0, errors.New("cannot call multiSend from staticcall") + } + if err := pcommon.ValidateNonPayable(value); err != nil { + return nil, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 4); err != nil { + return nil, 0, err + } + denom := args[2].(string) + if denom == "" { + return nil, 0, errors.New("invalid denom") + } + pointer, _, exists := p.evmKeeper.GetERC20NativePointer(ctx, denom) + if !exists || pointer.Cmp(caller) != 0 { + return nil, 0, fmt.Errorf("only pointer %s can send %s but got %s", pointer.Hex(), denom, caller.Hex()) + } + toAddresses := args[1].([]common.Address) + amounts := args[3].([]*big.Int) + if len(toAddresses) == 0 || len(toAddresses) != len(amounts) { + return nil, 0, errors.New("toAddresses and amounts must be non-empty and of equal length") + } + senderSeiAddr, err := p.accAddressFromArg(ctx, args[0]) + if err != nil { + return nil, 0, err + } + + total := sdk.ZeroInt() + outputs := make([]banktypes.Output, 0, len(toAddresses)) + for i, toAddress := range toAddresses { + if amounts[i].Cmp(utils.Big0) == 0 { + // zero-amount entries are no-ops, mirroring send's zero short circuit + continue + } + receiverSeiAddr, err := p.accAddressFromArg(ctx, toAddress) + if err != nil { + return nil, 0, err + } + amount := sdk.NewIntFromBigInt(amounts[i]) + total = total.Add(amount) + outputs = append(outputs, banktypes.NewOutput(receiverSeiAddr, sdk.NewCoins(sdk.NewCoin(denom, amount)))) + } + if len(outputs) == 0 { + // short circuit + bz, err := method.Outputs.Pack(true) + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err + } + + msg := banktypes.NewMsgMultiSend( + []banktypes.Input{banktypes.NewInput(senderSeiAddr, sdk.NewCoins(sdk.NewCoin(denom, total)))}, + outputs, + ) + + if err := msg.ValidateBasic(); err != nil { + return nil, 0, err + } + + if _, err := p.bankMsgServer.MultiSend(sdk.WrapSDKContext(ctx), msg); err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(true) + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err +} + func (p PrecompileExecutor) sendNative(ctx sdk.Context, method *abi.Method, args []interface{}, caller common.Address, callingContract common.Address, value *big.Int, readOnly bool, hooks *tracing.Hooks, evm *vm.EVM) ([]byte, uint64, error) { if readOnly { return nil, 0, errors.New("cannot call sendNative from staticcall") @@ -645,6 +718,8 @@ func (PrecompileExecutor) IsTransaction(method string) bool { switch method { case SendMethod: return true + case MultiSendMethod: + return true case SendNativeMethod: return true default: diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go index 37e5630947..884329cf22 100644 --- a/precompiles/bank/bank_test.go +++ b/precompiles/bank/bank_test.go @@ -351,6 +351,82 @@ func TestSendForUnlinkedReceiver(t *testing.T) { require.NotNil(t, err) } +func TestMultiSend(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + // Setup sender addresses and environment + privKey := testkeeper.MockPrivateKey() + senderAddr, senderEVMAddr := testkeeper.PrivateKeyToAddresses(privKey) + k.SetAddressMapping(ctx, senderAddr, senderEVMAddr) + require.Nil(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin("ufoo", sdk.NewInt(10000000))))) + require.Nil(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, senderAddr, sdk.NewCoins(sdk.NewCoin("ufoo", sdk.NewInt(10000000))))) + + _, pointerAddr := testkeeper.MockAddressPair() + k.SetERC20NativePointer(ctx, "ufoo", pointerAddr) + + // One linked receiver, one unlinked receiver + receiver1SeiAddr, receiver1EvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, receiver1SeiAddr, receiver1EvmAddr) + _, receiver2EvmAddr := testkeeper.MockAddressPair() + + p, err := bank.NewPrecompile(testApp.GetPrecompileKeepers()) + require.Nil(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{ + StateDB: statedb, + TxContext: vm.TxContext{Origin: senderEVMAddr}, + } + executor := p.GetExecutor().(*bank.PrecompileExecutor) + + multiSend, err := p.ABI.MethodById(executor.MultiSendID) + require.Nil(t, err) + args, err := multiSend.Inputs.Pack(senderEVMAddr, []common.Address{receiver1EvmAddr, receiver2EvmAddr}, "ufoo", []*big.Int{big.NewInt(100), big.NewInt(200)}) + require.Nil(t, err) + + // should error because the caller is not the denom's pointer + _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, false) + require.NotNil(t, err) + // should error because of read only call + _, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, args...), 200000, nil, nil, true, false) + require.NotNil(t, err) + // should error because it's not payable + _, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, args...), 200000, big.NewInt(1), nil, false, false) + require.NotNil(t, err) + // should error because denom is empty + invalidDenomArgs, err := multiSend.Inputs.Pack(senderEVMAddr, []common.Address{receiver1EvmAddr}, "", []*big.Int{big.NewInt(100)}) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, invalidDenomArgs...), 200000, nil, nil, false, false) + require.NotNil(t, err) + // should error because toAddresses and amounts lengths differ + mismatchedArgs, err := multiSend.Inputs.Pack(senderEVMAddr, []common.Address{receiver1EvmAddr}, "ufoo", []*big.Int{big.NewInt(1), big.NewInt(2)}) + require.Nil(t, err) + _, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, mismatchedArgs...), 200000, nil, nil, false, false) + require.NotNil(t, err) + + // success case sends to both linked and unlinked receivers; balances are + // checked on the statedb's context since that's where precompile writes land + ret, _, err := p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, false) + require.Nil(t, err) + outputs, err := multiSend.Outputs.Unpack(ret) + require.Nil(t, err) + require.True(t, outputs[0].(bool)) + require.Equal(t, int64(100), k.BankKeeper().GetBalance(statedb.Ctx(), receiver1SeiAddr, "ufoo").Amount.Int64()) + require.Equal(t, int64(200), k.BankKeeper().GetBalance(statedb.Ctx(), sdk.AccAddress(receiver2EvmAddr[:]), "ufoo").Amount.Int64()) + require.Equal(t, int64(9999700), k.BankKeeper().GetBalance(statedb.Ctx(), senderAddr, "ufoo").Amount.Int64()) + + // zero amounts short circuit without moving funds + zeroArgs, err := multiSend.Inputs.Pack(senderEVMAddr, []common.Address{receiver1EvmAddr}, "ufoo", []*big.Int{big.NewInt(0)}) + require.Nil(t, err) + ret, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, zeroArgs...), 200000, nil, nil, false, false) + require.Nil(t, err) + outputs, err = multiSend.Outputs.Unpack(ret) + require.Nil(t, err) + require.True(t, outputs[0].(bool)) + require.Equal(t, int64(100), k.BankKeeper().GetBalance(statedb.Ctx(), receiver1SeiAddr, "ufoo").Amount.Int64()) +} + func TestMetadata(t *testing.T) { k := &testkeeper.EVMTestApp.EvmKeeper ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) diff --git a/precompiles/distribution/Distribution.sol b/precompiles/distribution/Distribution.sol index d9004a1db4..f82bd784d2 100644 --- a/precompiles/distribution/Distribution.sol +++ b/precompiles/distribution/Distribution.sol @@ -16,6 +16,7 @@ interface IDistr { event DelegationRewardsWithdrawn(address indexed delegator, string validator, uint256 amount); event MultipleDelegationRewardsWithdrawn(address indexed delegator, string[] validators, uint256[] amounts); event ValidatorCommissionWithdrawn(string indexed validator, uint256 amount); + event CommunityPoolFunded(address indexed depositor, uint256 amount); // Transactions @@ -42,6 +43,12 @@ interface IDistr { /// @return success True if commission was withdrawn successfully function withdrawValidatorCommission() external returns (bool success); + /// @notice Funds the community pool with the usei sent as the call's value + /// @dev The caller must have a valid associated Sei address; the value must + /// have no non-zero wei remainder (1usei = 10^12 wei) + /// @return success True if the community pool was funded successfully + function fundCommunityPool() external payable returns (bool success); + // Queries /// @notice Gets all pending rewards for a delegator diff --git a/precompiles/distribution/abi.json b/precompiles/distribution/abi.json index 8a12f1b3a1..6d01ccdc87 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":[],"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 +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"depositor","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"CommunityPoolFunded","type":"event"},{"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":"fundCommunityPool","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","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 e027b1109a..16b2c92d28 100644 --- a/precompiles/distribution/distribution.go +++ b/precompiles/distribution/distribution.go @@ -26,6 +26,7 @@ const ( WithdrawDelegationRewardsMethod = "withdrawDelegationRewards" WithdrawMultipleDelegationRewardsMethod = "withdrawMultipleDelegationRewards" WithdrawValidatorCommissionMethod = "withdrawValidatorCommission" + FundCommunityPoolMethod = "fundCommunityPool" RewardsMethod = "rewards" ParamsMethod = "params" ValidatorOutstandingRewardsMethod = "validatorOutstandingRewards" @@ -40,6 +41,7 @@ const ( DelegationRewardsEvent = "DelegationRewardsWithdrawn" MultipleDelegationRewardsEvent = "MultipleDelegationRewardsWithdrawn" ValidatorCommissionEvent = "ValidatorCommissionWithdrawn" + CommunityPoolFundedEvent = "CommunityPoolFunded" ) const ( @@ -51,6 +53,7 @@ var ( DelegationRewardsEventSig = crypto.Keccak256Hash([]byte("DelegationRewardsWithdrawn(address,string,uint256)")) MultipleDelegationRewardsEventSig = crypto.Keccak256Hash([]byte("MultipleDelegationRewardsWithdrawn(address,string[],uint256[])")) ValidatorCommissionEventSig = crypto.Keccak256Hash([]byte("ValidatorCommissionWithdrawn(string,uint256)")) + CommunityPoolFundedEventSig = crypto.Keccak256Hash([]byte("CommunityPoolFunded(address,uint256)")) ) // Embed abi json file to the executable binary. Needed when importing as dependency. @@ -62,12 +65,14 @@ type PrecompileExecutor struct { distrKeeper utils.DistributionKeeper distributionQuerier utils.DistributionQuerier evmKeeper utils.EVMKeeper + bankKeeper utils.BankKeeper address common.Address SetWithdrawAddrID []byte WithdrawDelegationRewardsID []byte WithdrawMultipleDelegationRewardsID []byte WithdrawValidatorCommissionID []byte + FundCommunityPoolID []byte RewardsID []byte ParamsID []byte ValidatorOutstandingRewardsID []byte @@ -88,6 +93,7 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) distrKeeper: keepers.DistributionK(), distributionQuerier: keepers.DistributionQ(), evmKeeper: keepers.EVMK(), + bankKeeper: keepers.BankK(), address: common.HexToAddress(DistrAddress), abi: newAbi, } @@ -102,6 +108,8 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) p.WithdrawMultipleDelegationRewardsID = m.ID case WithdrawValidatorCommissionMethod: p.WithdrawValidatorCommissionID = m.ID + case FundCommunityPoolMethod: + p.FundCommunityPoolID = m.ID case RewardsMethod: p.RewardsID = m.ID case ParamsMethod: @@ -160,6 +168,11 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller return nil, 0, errors.New("cannot call distr precompile from staticcall") } return p.withdrawValidatorCommission(ctx, method, caller, evm) + case FundCommunityPoolMethod: + if readOnly { + return nil, 0, errors.New("cannot call distr precompile from staticcall") + } + return p.fundCommunityPool(ctx, method, caller, args, value, evm, hooks) case RewardsMethod: return p.rewards(ctx, method, args) case ParamsMethod: @@ -190,7 +203,7 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { // distribution methods are views. func (PrecompileExecutor) IsTransaction(method string) bool { switch method { - case SetWithdrawAddressMethod, WithdrawDelegationRewardsMethod, WithdrawMultipleDelegationRewardsMethod, WithdrawValidatorCommissionMethod: + case SetWithdrawAddressMethod, WithdrawDelegationRewardsMethod, WithdrawMultipleDelegationRewardsMethod, WithdrawValidatorCommissionMethod, FundCommunityPoolMethod: return true default: return false @@ -805,3 +818,58 @@ func (p PrecompileExecutor) withdrawValidatorCommission(ctx sdk.Context, method } return } + +func (p PrecompileExecutor) fundCommunityPool(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int, evm *vm.EVM, hooks *tracing.Hooks) (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 := pcommon.ValidateArgsLength(args, 0); err != nil { + rerr = err + return + } + depositor, found := p.evmKeeper.GetSeiAddress(ctx, caller) + if !found { + rerr = types.NewAssociationMissingErr(caller.Hex()) + return + } + if value == nil || value.Sign() == 0 { + rerr = errors.New("set `value` field to non-zero to fund community pool") + return + } + + coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), depositor, value, p.bankKeeper, p.evmKeeper, hooks, evm.GetDepth()) + if err != nil { + rerr = err + return + } + + if err := p.distrKeeper.FundCommunityPool(ctx, sdk.NewCoins(coin), depositor); err != nil { + rerr = err + return + } + + ret, rerr = method.Outputs.Pack(true) + remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) + if rerr != nil { + return + } + + logData, err := p.abi.Events[CommunityPoolFundedEvent].Inputs.NonIndexed().Pack(coin.Amount.BigInt()) + if err != nil { + rerr = err + return + } + if err := pcommon.EmitEVMLog(evm, p.address, []common.Hash{ + CommunityPoolFundedEventSig, + common.BytesToHash(caller.Bytes()), + }, logData); err != nil { + rerr = err + return + } + return +} diff --git a/precompiles/distribution/distribution_test.go b/precompiles/distribution/distribution_test.go index fcfd6cca57..1984aecb7a 100644 --- a/precompiles/distribution/distribution_test.go +++ b/precompiles/distribution/distribution_test.go @@ -886,6 +886,10 @@ func (tk *TestDistributionKeeper) GetDelegatorWithdrawAddr(ctx sdk.Context, delA return delAddr } +func (tk *TestDistributionKeeper) FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error { + return nil +} + func (tk *TestDistributionKeeper) DelegationTotalRewards(ctx context.Context, req *distrtypes.QueryDelegationTotalRewardsRequest) (*distrtypes.QueryDelegationTotalRewardsResponse, error) { uatomCoins := 1 val1useiCoins := 5 @@ -936,6 +940,10 @@ func (tk *TestEmptyRewardsDistributionKeeper) GetDelegatorWithdrawAddr(ctx sdk.C return delAddr } +func (tk *TestEmptyRewardsDistributionKeeper) FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error { + return nil +} + func TestPrecompile_RunAndCalculateGas_Rewards(t *testing.T) { callerSeiAddress, callerEvmAddress := testkeeper.MockAddressPair() _, notAssociatedCallerEvmAddress := testkeeper.MockAddressPair() @@ -1610,3 +1618,78 @@ func TestWithdrawValidatorCommission_InputValidation(t *testing.T) { }) } } + +func TestFundCommunityPool(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + + privKey := testkeeper.MockPrivateKey() + testPrivHex := hex.EncodeToString(privKey.Bytes()) + key, _ := crypto.HexToECDSA(testPrivHex) + seiAddr, evmAddr := testkeeper.PrivateKeyToAddresses(privKey) + k.SetAddressMapping(ctx, seiAddr, evmAddr) + amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(200000000))) + require.Nil(t, k.BankKeeper().MintCoins(ctx, evmtypes.ModuleName, amt)) + require.Nil(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, seiAddr, amt)) + + beforePool := testApp.DistrKeeper.GetFeePoolCommunityCoins(ctx) + + // fund the community pool with 100usei sent as tx value + abi := pcommon.MustGetABI(f, "abi.json") + args, err := abi.Pack("fundCommunityPool") + require.Nil(t, err) + addr := common.HexToAddress(distribution.DistrAddress) + txData := ethtypes.LegacyTx{ + GasPrice: big.NewInt(1000000000000), + Gas: 20000000, + To: &addr, + Value: big.NewInt(100_000_000_000_000), + Data: args, + Nonce: 0, + } + chainID := k.ChainID(ctx) + chainCfg := evmtypes.DefaultChainConfig() + ethCfg := chainCfg.EthereumConfig(chainID) + blockNum := big.NewInt(ctx.BlockHeight()) + signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix())) + tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) + require.Nil(t, err) + txwrapper, err := ethtx.NewLegacyTx(tx) + require.Nil(t, err) + req, err := evmtypes.NewMsgEVMTransaction(txwrapper) + require.Nil(t, err) + + msgServer := keeper.NewMsgServerImpl(k) + ante.Preprocess(ctx, req, k.ChainID(ctx), false) + res, err := msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req) + require.Nil(t, err) + require.Empty(t, res.VmError) + + afterPool := testApp.DistrKeeper.GetFeePoolCommunityCoins(ctx) + require.Equal(t, sdk.NewDecCoins(sdk.NewDecCoin("usei", sdk.NewInt(100))), afterPool.Sub(beforePool)) + + receipt, err := k.GetTransientReceipt(ctx, tx.Hash(), 0) + require.Nil(t, err) + require.Equal(t, 1, len(receipt.Logs)) + require.Equal(t, distribution.CommunityPoolFundedEventSig, common.HexToHash(receipt.Logs[0].Topics[0])) + require.Equal(t, common.BytesToHash(evmAddr.Bytes()), common.HexToHash(receipt.Logs[0].Topics[1])) + require.NotEmpty(t, receipt.Logs[0].Data) + + // error cases + p, err := distribution.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().(*distribution.PrecompileExecutor) + // should error because of read only call + _, _, err = p.RunAndCalculateGas(&evm, evmAddr, evmAddr, executor.FundCommunityPoolID, 100000, big.NewInt(100), nil, true, false) + require.NotNil(t, err) + // should error because caller is not associated + _, unassociatedEvmAddr := testkeeper.MockAddressPair() + _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvmAddr, unassociatedEvmAddr, executor.FundCommunityPoolID, 100000, big.NewInt(100), nil, false, false) + require.NotNil(t, err) + // should error because value is zero + _, _, err = p.RunAndCalculateGas(&evm, evmAddr, evmAddr, executor.FundCommunityPoolID, 100000, big.NewInt(0), nil, false, false) + require.NotNil(t, err) +} diff --git a/precompiles/feegrant/Feegrant.sol b/precompiles/feegrant/Feegrant.sol index 8db29633e6..13e9967118 100644 --- a/precompiles/feegrant/Feegrant.sol +++ b/precompiles/feegrant/Feegrant.sol @@ -6,6 +6,16 @@ address constant FEEGRANT_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000 IFeegrant constant FEEGRANT_CONTRACT = IFeegrant(FEEGRANT_PRECOMPILE_ADDRESS); interface IFeegrant { + // Transactions + // Grants a fee allowance from the caller (granter) to the grantee. The + // allowance is protobuf-JSON encoded (the same encoding the queries + // return), e.g. + // {"@type":"/cosmos.feegrant.v1beta1.BasicAllowance","spend_limit":[{"denom":"usei","amount":"1000"}],"expiration":null} + function grantAllowance(address grantee, bytes memory allowance) external returns (bool success); + + // Revokes the fee allowance granted by the caller to the grantee. + function revokeAllowance(address grantee) external returns (bool success); + // Queries // Returns the fee allowance granted to the grantee by the granter. The // allowance field of the returned grant is JSON-encoded. diff --git a/precompiles/feegrant/abi.json b/precompiles/feegrant/abi.json index c79c89ee50..8c61c82c5a 100644 --- a/precompiles/feegrant/abi.json +++ b/precompiles/feegrant/abi.json @@ -145,5 +145,48 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "grantee", + "type": "address" + }, + { + "internalType": "bytes", + "name": "allowance", + "type": "bytes" + } + ], + "name": "grantAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "grantee", + "type": "address" + } + ], + "name": "revokeAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" } ] diff --git a/precompiles/feegrant/feegrant.go b/precompiles/feegrant/feegrant.go index 34a5d6094d..6107e5af04 100644 --- a/precompiles/feegrant/feegrant.go +++ b/precompiles/feegrant/feegrant.go @@ -16,9 +16,12 @@ import ( 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" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) const ( + GrantAllowanceMethod = "grantAllowance" + RevokeAllowanceMethod = "revokeAllowance" AllowanceMethod = "allowance" AllowancesMethod = "allowances" AllowancesByGranterMethod = "allowancesByGranter" @@ -34,10 +37,13 @@ const ( var f embed.FS type PrecompileExecutor struct { - evmKeeper utils.EVMKeeper - feegrantQuerier utils.FeegrantQuerier - cdc codec.Codec + evmKeeper utils.EVMKeeper + feegrantMsgServer utils.FeegrantMsgServer + feegrantQuerier utils.FeegrantQuerier + cdc codec.Codec + GrantAllowanceID []byte + RevokeAllowanceID []byte AllowanceID []byte AllowancesID []byte AllowancesByGranterID []byte @@ -47,13 +53,18 @@ 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(), + evmKeeper: keepers.EVMK(), + feegrantMsgServer: keepers.FeegrantMS(), + feegrantQuerier: keepers.FeegrantQ(), + cdc: keepers.Codec(), } for name, m := range newAbi.Methods { switch name { + case GrantAllowanceMethod: + p.GrantAllowanceID = m.ID + case RevokeAllowanceMethod: + p.RevokeAllowanceID = m.ID case AllowanceMethod: p.AllowanceID = m.ID case AllowancesMethod: @@ -93,9 +104,109 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller case AllowancesByGranterMethod: return p.allowancesByGranter(ctx, method, args, value) } + + // Transaction methods act on behalf of the caller, so they must not be + // reachable through delegatecall (which would let a contract act on + // behalf of its own caller) or staticcall. + if ctx.EVMPrecompileCalledFromDelegateCall() { + return nil, 0, errors.New("cannot delegatecall feegrant") + } + if readOnly { + return nil, 0, errors.New("cannot call feegrant precompile from staticcall") + } + switch method.Name { + case GrantAllowanceMethod: + return p.grantAllowance(ctx, method, caller, args, value) + case RevokeAllowanceMethod: + return p.revokeAllowance(ctx, method, caller, args, value) + } return } +// grantAllowance stores a fee allowance from the caller (granter) to the +// grantee. The allowance is a protobuf-JSON encoded FeeAllowanceI (the same +// encoding the query methods return), e.g. +// {"@type":"/cosmos.feegrant.v1beta1.BasicAllowance","spend_limit":[...],"expiration":null}. +func (p PrecompileExecutor) grantAllowance(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, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 2); err != nil { + return nil, 0, err + } + + granter, found := p.evmKeeper.GetSeiAddress(ctx, caller) + if !found { + return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) + } + + grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + var allowance feegranttypes.FeeAllowanceI + if err := p.cdc.UnmarshalInterfaceJSON(args[1].([]byte), &allowance); err != nil { + return nil, 0, fmt.Errorf("failed to parse allowance JSON: %w", err) + } + + msg, err := feegranttypes.NewMsgGrantAllowance(allowance, granter, grantee) + if err != nil { + return nil, 0, err + } + + if err := msg.ValidateBasic(); err != nil { + return nil, 0, err + } + + if _, err := p.feegrantMsgServer.GrantAllowance(sdk.WrapSDKContext(ctx), msg); err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(true) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + +// revokeAllowance removes the caller's fee allowance to the grantee. +func (p PrecompileExecutor) revokeAllowance(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, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 1); err != nil { + return nil, 0, err + } + + granter, found := p.evmKeeper.GetSeiAddress(ctx, caller) + if !found { + return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) + } + + grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) + if err != nil { + return nil, 0, err + } + + msg := feegranttypes.NewMsgRevokeAllowance(granter, grantee) + if err := msg.ValidateBasic(); err != nil { + return nil, 0, err + } + + if _, err := p.feegrantMsgServer.RevokeAllowance(sdk.WrapSDKContext(ctx), &msg); err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(true) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + type Grant struct { Granter string Grantee string @@ -260,6 +371,13 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { return p.evmKeeper } -func (PrecompileExecutor) IsTransaction(string) bool { - return false +// IsTransaction returns true for methods that mutate state; all other feegrant +// methods are views. +func (PrecompileExecutor) IsTransaction(method string) bool { + switch method { + case GrantAllowanceMethod, RevokeAllowanceMethod: + return true + default: + return false + } } diff --git a/precompiles/feegrant/feegrant_test.go b/precompiles/feegrant/feegrant_test.go index 719eb5da3e..8bab523c98 100644 --- a/precompiles/feegrant/feegrant_test.go +++ b/precompiles/feegrant/feegrant_test.go @@ -159,3 +159,80 @@ func TestFeegrantQueries(t *testing.T) { require.Nil(t, ret) }) } + +func TestGrantAndRevokeAllowance(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) + k := &testApp.EvmKeeper + cdc := testApp.AppCodec() + + granterSei, granterEvm := testkeeper.MockAddressPair() + granteeSei, granteeEvm := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, granterSei, granterEvm) + k.SetAddressMapping(ctx, granteeSei, granteeEvm) + + p, err := feegrant.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: granterEvm}} + executor := p.GetExecutor().(*feegrant.PrecompileExecutor) + + allowanceJSON, err := cdc.MarshalInterfaceJSON(&feegranttypes.BasicAllowance{ + SpendLimit: sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))), + }) + require.NoError(t, err) + + grantMethod, err := p.ABI.MethodById(executor.GrantAllowanceID) + require.NoError(t, err) + grantArgs, err := grantMethod.Inputs.Pack(granteeEvm, allowanceJSON) + require.NoError(t, err) + + // should error because of read only call + _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, true, false) + require.Error(t, err) + // should error because of delegatecall + _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, false, true) + require.Error(t, err) + // should error because caller is not associated + _, unassociatedEvm := testkeeper.MockAddressPair() + _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvm, unassociatedEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, false, false) + require.Error(t, err) + // should error because the allowance JSON is invalid + badAllowanceArgs, err := grantMethod.Inputs.Pack(granteeEvm, []byte("{")) + require.NoError(t, err) + _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, badAllowanceArgs...), 1000000, nil, nil, false, false) + require.Error(t, err) + + ret, _, err := p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, false, false) + require.NoError(t, err) + outputs, err := grantMethod.Outputs.Unpack(ret) + require.NoError(t, err) + require.True(t, outputs[0].(bool)) + + stored, err := testApp.FeeGrantKeeper.GetAllowance(statedb.Ctx(), granterSei, granteeSei) + require.NoError(t, err) + basic, ok := stored.(*feegranttypes.BasicAllowance) + require.True(t, ok) + require.Equal(t, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))), basic.SpendLimit) + + // re-granting while one exists should error + _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, false, false) + require.Error(t, err) + + // revoke the allowance + revokeMethod, err := p.ABI.MethodById(executor.RevokeAllowanceID) + require.NoError(t, err) + revokeArgs, err := revokeMethod.Inputs.Pack(granteeEvm) + require.NoError(t, err) + ret, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.RevokeAllowanceID, revokeArgs...), 1000000, nil, nil, false, false) + require.NoError(t, err) + outputs, err = revokeMethod.Outputs.Unpack(ret) + require.NoError(t, err) + require.True(t, outputs[0].(bool)) + _, err = testApp.FeeGrantKeeper.GetAllowance(statedb.Ctx(), granterSei, granteeSei) + require.Error(t, err) + + // revoking a non-existent allowance should error + _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.RevokeAllowanceID, revokeArgs...), 1000000, nil, nil, false, false) + require.Error(t, err) +} diff --git a/precompiles/slashing/Slashing.sol b/precompiles/slashing/Slashing.sol index 33619a9a9e..160bd92c49 100644 --- a/precompiles/slashing/Slashing.sol +++ b/precompiles/slashing/Slashing.sol @@ -6,6 +6,11 @@ address constant SLASHING_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000 ISlashing constant SLASHING_CONTRACT = ISlashing(SLASHING_PRECOMPILE_ADDRESS); interface ISlashing { + // Transactions + // Unjails the validator whose operator address is the caller's associated + // Sei address. + function unjail() external returns (bool success); + // Queries function params() external view returns (SlashingParams memory params); diff --git a/precompiles/slashing/abi.json b/precompiles/slashing/abi.json index 4f91c7f29b..5319ddef55 100644 --- a/precompiles/slashing/abi.json +++ b/precompiles/slashing/abi.json @@ -152,5 +152,18 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [], + "name": "unjail", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" } ] diff --git a/precompiles/slashing/slashing.go b/precompiles/slashing/slashing.go index 461981bfad..aa89601e53 100644 --- a/precompiles/slashing/slashing.go +++ b/precompiles/slashing/slashing.go @@ -2,6 +2,7 @@ package slashing import ( "embed" + "errors" "fmt" "math/big" @@ -14,9 +15,11 @@ import ( 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" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) const ( + UnjailMethod = "unjail" ParamsMethod = "params" SigningInfoMethod = "signingInfo" SigningInfosMethod = "signingInfos" @@ -32,9 +35,11 @@ const ( var f embed.FS type PrecompileExecutor struct { - evmKeeper utils.EVMKeeper - slashingQuerier utils.SlashingQuerier + evmKeeper utils.EVMKeeper + slashingMsgServer utils.SlashingMsgServer + slashingQuerier utils.SlashingQuerier + UnjailID []byte ParamsID []byte SigningInfoID []byte SigningInfosID []byte @@ -44,12 +49,15 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) newAbi := pcommon.MustGetABI(f, "abi.json") p := &PrecompileExecutor{ - evmKeeper: keepers.EVMK(), - slashingQuerier: keepers.SlashingQ(), + evmKeeper: keepers.EVMK(), + slashingMsgServer: keepers.SlashingMS(), + slashingQuerier: keepers.SlashingQ(), } for name, m := range newAbi.Methods { switch name { + case UnjailMethod: + p.UnjailID = m.ID case ParamsMethod: p.ParamsID = m.ID case SigningInfoMethod: @@ -89,9 +97,55 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller case SigningInfosMethod: return p.signingInfos(ctx, method, args, value) } + + // Transaction methods act on behalf of the caller, so they must not be + // reachable through delegatecall (which would let a contract act on + // behalf of its own caller) or staticcall. + if ctx.EVMPrecompileCalledFromDelegateCall() { + return nil, 0, errors.New("cannot delegatecall slashing") + } + if readOnly { + return nil, 0, errors.New("cannot call slashing precompile from staticcall") + } + switch method.Name { + case UnjailMethod: + return p.unjail(ctx, method, caller, args, value) + } return } +// unjail unjails the validator whose operator address is the caller's +// associated Sei address. +func (p PrecompileExecutor) unjail(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, 0, err + } + + if err := pcommon.ValidateArgsLength(args, 0); err != nil { + return nil, 0, err + } + + seiAddr, found := p.evmKeeper.GetSeiAddress(ctx, caller) + if !found { + return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) + } + + msg := slashingtypes.NewMsgUnjail(sdk.ValAddress(seiAddr)) + if err := msg.ValidateBasic(); err != nil { + return nil, 0, err + } + + if _, err := p.slashingMsgServer.Unjail(sdk.WrapSDKContext(ctx), msg); err != nil { + return nil, 0, err + } + + bz, err := method.Outputs.Pack(true) + if err != nil { + return nil, 0, err + } + return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil +} + type SlashingParams struct { SignedBlocksWindow int64 MinSignedPerWindow string @@ -223,6 +277,13 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { return p.evmKeeper } -func (PrecompileExecutor) IsTransaction(string) bool { - return false +// IsTransaction returns true for methods that mutate state; all other slashing +// methods are views. +func (PrecompileExecutor) IsTransaction(method string) bool { + switch method { + case UnjailMethod: + return true + default: + return false + } } diff --git a/precompiles/slashing/slashing_test.go b/precompiles/slashing/slashing_test.go index 71f9ed6d2a..bc9efe2a47 100644 --- a/precompiles/slashing/slashing_test.go +++ b/precompiles/slashing/slashing_test.go @@ -7,13 +7,18 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" + "github.com/sei-protocol/sei-chain/app" "github.com/sei-protocol/sei-chain/precompiles/slashing" "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/ed25519" + crptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" slashingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/teststaking" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/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" + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" "github.com/stretchr/testify/require" ) @@ -191,3 +196,89 @@ func TestPrecompile_Run_SigningInfos(t *testing.T) { require.Error(t, err) require.Equal(t, vm.ErrExecutionReverted, err) } + +func setupValidator(t *testing.T, ctx sdk.Context, a *app.App, bondStatus stakingtypes.BondStatus, valPub crptotypes.PubKey) sdk.ValAddress { + valAddr := sdk.ValAddress(valPub.Address()) + bondDenom := a.StakingKeeper.GetParams(ctx).BondDenom + selfBond := sdk.NewCoins(sdk.Coin{Amount: sdk.NewInt(100), Denom: bondDenom}) + + require.NoError(t, a.BankKeeper.MintCoins(ctx, minttypes.ModuleName, selfBond)) + require.NoError(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, sdk.AccAddress(valAddr), selfBond)) + + sh := teststaking.NewHelper(t, ctx, a.StakingKeeper) + msg := sh.CreateValidatorMsg(valAddr, valPub, selfBond[0].Amount) + sh.Handle(msg, true) + + val, found := a.StakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + + val = val.UpdateStatus(bondStatus) + a.StakingKeeper.SetValidator(ctx, val) + + consAddr, err := val.GetConsAddr() + require.NoError(t, err) + + signingInfo := slashingtypes.NewValidatorSigningInfo( + consAddr, + ctx.BlockHeight(), + 0, + time.Unix(0, 0), + false, + 0, + ) + a.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, signingInfo) + + return valAddr +} + +func TestUnjail(t *testing.T) { + testApp := testkeeper.EVMTestApp + ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2).WithBlockTime(time.Now()) + k := &testApp.EvmKeeper + + valPub := ed25519.GenPrivKey().PubKey() + valAddr := setupValidator(t, ctx, testApp, stakingtypes.Unbonded, valPub) + consAddr := sdk.ConsAddress(valPub.Address()) + testApp.StakingKeeper.Jail(ctx, consAddr) + require.True(t, testApp.StakingKeeper.Validator(ctx, valAddr).IsJailed()) + + operatorSeiAddr := sdk.AccAddress(valAddr) + _, operatorEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, operatorSeiAddr, operatorEvmAddr) + // an associated address that is not a validator operator + nonValidatorSeiAddr, nonValidatorEvmAddr := testkeeper.MockAddressPair() + k.SetAddressMapping(ctx, nonValidatorSeiAddr, nonValidatorEvmAddr) + + p, err := slashing.NewPrecompile(testApp.GetPrecompileKeepers()) + require.NoError(t, err) + statedb := state.NewDBImpl(ctx, k, true) + evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: operatorEvmAddr}} + executor := p.GetExecutor().(*slashing.PrecompileExecutor) + method, err := p.ABI.MethodById(executor.UnjailID) + require.NoError(t, err) + + // should error because of read only call + _, _, err = p.RunAndCalculateGas(&evm, operatorEvmAddr, operatorEvmAddr, executor.UnjailID, 1000000, nil, nil, true, false) + require.Error(t, err) + // should error because of delegatecall + _, _, err = p.RunAndCalculateGas(&evm, operatorEvmAddr, operatorEvmAddr, executor.UnjailID, 1000000, nil, nil, false, true) + require.Error(t, err) + // should error because it's not payable + _, _, err = p.RunAndCalculateGas(&evm, operatorEvmAddr, operatorEvmAddr, executor.UnjailID, 1000000, big.NewInt(1), nil, false, false) + require.Error(t, err) + // should error because caller is not associated + _, unassociatedEvmAddr := testkeeper.MockAddressPair() + _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvmAddr, unassociatedEvmAddr, executor.UnjailID, 1000000, nil, nil, false, false) + require.Error(t, err) + // should error because caller is not a validator operator + _, _, err = p.RunAndCalculateGas(&evm, nonValidatorEvmAddr, nonValidatorEvmAddr, executor.UnjailID, 1000000, nil, nil, false, false) + require.Error(t, err) + + // happy path + ret, _, err := p.RunAndCalculateGas(&evm, operatorEvmAddr, operatorEvmAddr, executor.UnjailID, 1000000, nil, nil, false, false) + require.NoError(t, err) + outputs, err := method.Outputs.Unpack(ret) + require.NoError(t, err) + require.True(t, outputs[0].(bool)) + require.False(t, testApp.StakingKeeper.Validator(statedb.Ctx(), valAddr).IsJailed()) +} diff --git a/precompiles/utils/expected_keepers.go b/precompiles/utils/expected_keepers.go index 4760584cce..b2f42ae1f1 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -27,6 +27,7 @@ import ( 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" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" minttypes "github.com/sei-protocol/sei-chain/x/mint/types" oracletypes "github.com/sei-protocol/sei-chain/x/oracle/types" ) @@ -36,8 +37,10 @@ type Keepers interface { BankMS() BankMsgServer BankQ() BankQuerier EVMK() EVMKeeper + EvmMS() EvmMsgServer AccountK() AccountKeeper AuthQ() AuthQuerier + AuthzMS() AuthzMsgServer AuthzQ() AuthzQuerier OracleK() OracleKeeper WasmdK() WasmdKeeper @@ -50,9 +53,11 @@ type Keepers interface { DistributionK() DistributionKeeper DistributionQ() DistributionQuerier EvidenceQ() EvidenceQuerier + FeegrantMS() FeegrantMsgServer FeegrantQ() FeegrantQuerier MintQ() MintQuerier ParamsQ() ParamsQuerier + SlashingMS() SlashingMsgServer SlashingQ() SlashingQuerier UpgradeQ() UpgradeQuerier TransferK() TransferKeeper @@ -69,8 +74,10 @@ 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) EvmMS() EvmMsgServer { return nil } func (ek *EmptyKeepers) AccountK() AccountKeeper { return nil } func (ek *EmptyKeepers) AuthQ() AuthQuerier { return nil } +func (ek *EmptyKeepers) AuthzMS() AuthzMsgServer { return nil } func (ek *EmptyKeepers) AuthzQ() AuthzQuerier { return nil } func (ek *EmptyKeepers) OracleK() OracleKeeper { return nil } func (ek *EmptyKeepers) WasmdK() WasmdKeeper { return nil } @@ -85,9 +92,11 @@ func (ek *EmptyKeepers) DistributionQ() DistributionQuerier { return nil } func (ek *EmptyKeepers) EvidenceQ() EvidenceQuerier { return nil } +func (ek *EmptyKeepers) FeegrantMS() FeegrantMsgServer { 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) SlashingMS() SlashingMsgServer { return nil } func (ek *EmptyKeepers) SlashingQ() SlashingQuerier { return nil } func (ek *EmptyKeepers) UpgradeQ() UpgradeQuerier { return nil } func (ek *EmptyKeepers) TransferK() TransferKeeper { return nil } @@ -111,6 +120,26 @@ type BankKeeper interface { type BankMsgServer interface { Send(goCtx context.Context, msg *banktypes.MsgSend) (*banktypes.MsgSendResponse, error) + MultiSend(goCtx context.Context, msg *banktypes.MsgMultiSend) (*banktypes.MsgMultiSendResponse, error) +} + +type EvmMsgServer interface { + AssociateContractAddress(goCtx context.Context, msg *evmtypes.MsgAssociateContractAddress) (*evmtypes.MsgAssociateContractAddressResponse, error) +} + +type AuthzMsgServer interface { + Grant(goCtx context.Context, msg *authz.MsgGrant) (*authz.MsgGrantResponse, error) + Exec(goCtx context.Context, msg *authz.MsgExec) (*authz.MsgExecResponse, error) + Revoke(goCtx context.Context, msg *authz.MsgRevoke) (*authz.MsgRevokeResponse, error) +} + +type FeegrantMsgServer interface { + GrantAllowance(goCtx context.Context, msg *feegrant.MsgGrantAllowance) (*feegrant.MsgGrantAllowanceResponse, error) + RevokeAllowance(goCtx context.Context, msg *feegrant.MsgRevokeAllowance) (*feegrant.MsgRevokeAllowanceResponse, error) +} + +type SlashingMsgServer interface { + Unjail(goCtx context.Context, msg *slashingtypes.MsgUnjail) (*slashingtypes.MsgUnjailResponse, error) } type EVMKeeper interface { @@ -216,6 +245,7 @@ type DistributionKeeper interface { WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (sdk.Coins, error) WithdrawValidatorCommission(ctx sdk.Context, valAddr sdk.ValAddress) (sdk.Coins, error) DelegationTotalRewards(c context.Context, req *distrtypes.QueryDelegationTotalRewardsRequest) (*distrtypes.QueryDelegationTotalRewardsResponse, error) + FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error } type TransferKeeper interface { From 77f6fa5785fa519f0c5cb22e1bdd4680a5d266f4 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 29 Jul 2026 15:53:53 +0800 Subject: [PATCH 2/8] Drop addr associateContractAddress (deprecated msg) Co-Authored-By: Claude Fable 5 --- app/precompiles.go | 4 -- precompiles/addr/Addr.sol | 5 -- precompiles/addr/abi.json | 2 +- precompiles/addr/addr.go | 59 +++++----------------- precompiles/addr/addr_test.go | 70 --------------------------- precompiles/utils/expected_keepers.go | 7 --- 6 files changed, 12 insertions(+), 135 deletions(-) diff --git a/app/precompiles.go b/app/precompiles.go index 19ab50325d..ff6d67b889 100644 --- a/app/precompiles.go +++ b/app/precompiles.go @@ -10,7 +10,6 @@ import ( slashingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/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" - evmkeeper "github.com/sei-protocol/sei-chain/x/evm/keeper" mintkeeper "github.com/sei-protocol/sei-chain/x/mint/keeper" ) @@ -19,7 +18,6 @@ type PrecompileKeepers struct { putils.BankMsgServer putils.BankQuerier putils.EVMKeeper - putils.EvmMsgServer putils.AccountKeeper putils.AuthQuerier putils.AuthzMsgServer @@ -56,7 +54,6 @@ func NewPrecompileKeepers(a *App) *PrecompileKeepers { BankMsgServer: bankkeeper.NewMsgServerImpl(a.BankKeeper), BankQuerier: a.BankKeeper, EVMKeeper: &a.EvmKeeper, - EvmMsgServer: evmkeeper.NewMsgServerImpl(&a.EvmKeeper), AccountKeeper: a.AccountKeeper, AuthQuerier: a.AccountKeeper, AuthzMsgServer: a.AuthzKeeper, @@ -92,7 +89,6 @@ func (pk *PrecompileKeepers) BankK() putils.BankKeeper { return 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) EvmMS() putils.EvmMsgServer { return pk.EvmMsgServer } func (pk *PrecompileKeepers) AccountK() putils.AccountKeeper { return pk.AccountKeeper } func (pk *PrecompileKeepers) AuthQ() putils.AuthQuerier { return pk.AuthQuerier } func (pk *PrecompileKeepers) AuthzMS() putils.AuthzMsgServer { return pk.AuthzMsgServer } diff --git a/precompiles/addr/Addr.sol b/precompiles/addr/Addr.sol index b8521a08e5..e83f62da77 100644 --- a/precompiles/addr/Addr.sol +++ b/precompiles/addr/Addr.sol @@ -21,11 +21,6 @@ interface IAddr { string memory pubKeyHex ) external returns (string memory seiAddr, address evmAddr); - // Associates a CosmWasm contract address with its EVM counterpart - function associateContractAddress( - string memory cwAddr - ) external returns (string memory seiAddr, address evmAddr); - // Queries function getSeiAddr(address addr) external view returns (string memory response); function getEvmAddr(string memory addr) external view returns (address response); diff --git a/precompiles/addr/abi.json b/precompiles/addr/abi.json index 356ebb0563..f0425eaf83 100755 --- a/precompiles/addr/abi.json +++ b/precompiles/addr/abi.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"string","name":"v","type":"string"},{"internalType":"string","name":"r","type":"string"},{"internalType":"string","name":"s","type":"string"},{"internalType":"string","name":"customMessage","type":"string"}],"name":"associate","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"pubKeyHex","type":"string"}],"name":"associatePubKey","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"cwAddr","type":"string"}],"name":"associateContractAddress","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getSeiAddr","outputs":[{"internalType":"string","name":"response","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"addr","type":"string"}],"name":"getEvmAddr","outputs":[{"internalType":"address","name":"response","type":"address"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"string","name":"v","type":"string"},{"internalType":"string","name":"r","type":"string"},{"internalType":"string","name":"s","type":"string"},{"internalType":"string","name":"customMessage","type":"string"}],"name":"associate","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"pubKeyHex","type":"string"}],"name":"associatePubKey","outputs":[{"internalType":"string","name":"seiAddr","type":"string"},{"internalType":"address","name":"evmAddr","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getSeiAddr","outputs":[{"internalType":"string","name":"response","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"addr","type":"string"}],"name":"getEvmAddr","outputs":[{"internalType":"address","name":"response","type":"address"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/precompiles/addr/addr.go b/precompiles/addr/addr.go index c21ab04430..ec8f435654 100644 --- a/precompiles/addr/addr.go +++ b/precompiles/addr/addr.go @@ -30,11 +30,10 @@ import ( ) const ( - GetSeiAddressMethod = "getSeiAddr" - GetEvmAddressMethod = "getEvmAddr" - Associate = "associate" - AssociatePubKey = "associatePubKey" - AssociateContractAddress = "associateContractAddress" + GetSeiAddressMethod = "getSeiAddr" + GetEvmAddressMethod = "getEvmAddr" + Associate = "associate" + AssociatePubKey = "associatePubKey" ) const ( @@ -48,15 +47,13 @@ var f embed.FS type PrecompileExecutor struct { evmKeeper putils.EVMKeeper - evmMsgServer putils.EvmMsgServer bankKeeper putils.BankKeeper accountKeeper putils.AccountKeeper - GetSeiAddressID []byte - GetEvmAddressID []byte - AssociateID []byte - AssociatePubKeyID []byte - AssociateContractAddressID []byte + GetSeiAddressID []byte + GetEvmAddressID []byte + AssociateID []byte + AssociatePubKeyID []byte } func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error) { @@ -65,7 +62,6 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error p := &PrecompileExecutor{ evmKeeper: keepers.EVMK(), - evmMsgServer: keepers.EvmMS(), bankKeeper: keepers.BankK(), accountKeeper: keepers.AccountK(), } @@ -80,8 +76,6 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error p.AssociateID = m.ID case AssociatePubKey: p.AssociatePubKeyID = m.ID - case AssociateContractAddress: - p.AssociateContractAddressID = m.ID } } @@ -90,13 +84,13 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error // 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.AssociateID) || bytes.Equal(method.ID, p.AssociatePubKeyID) || bytes.Equal(method.ID, p.AssociateContractAddressID) { + if bytes.Equal(method.ID, p.AssociateID) || bytes.Equal(method.ID, p.AssociatePubKeyID) { return 50000 } return pcommon.DefaultGasCost(input, p.IsTransaction(method.Name)) } -func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller common.Address, _ common.Address, args []interface{}, value *big.Int, readOnly bool, _ *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (ret []byte, remainingGas uint64, err error) { +func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, _ common.Address, _ common.Address, args []interface{}, value *big.Int, readOnly bool, _ *vm.EVM, suppliedGas uint64, hooks *tracing.Hooks) (ret []byte, remainingGas uint64, err error) { // Needed to catch gas meter panics defer func() { if r := recover(); r != nil { @@ -118,11 +112,6 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller return nil, 0, errors.New("cannot call associate pub key precompile from staticcall") } return p.associatePublicKey(ctx, method, args, value) - case AssociateContractAddress: - if readOnly { - return nil, 0, errors.New("cannot call associate contract address precompile from staticcall") - } - return p.associateContractAddress(ctx, method, caller, args, value) } return } @@ -265,35 +254,9 @@ func (p PrecompileExecutor) associateAddresses(ctx sdk.Context, method *abi.Meth return ret, pcommon.GetRemainingGas(ctx, p.evmKeeper), err } -func (p PrecompileExecutor) associateContractAddress(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int) (ret []byte, remainingGas uint64, err error) { - if err := pcommon.ValidateNonPayable(value); err != nil { - return nil, 0, err - } - - if err := pcommon.ValidateArgsLength(args, 1); err != nil { - return nil, 0, err - } - - msg := &types.MsgAssociateContractAddress{ - Sender: p.evmKeeper.GetSeiAddressOrDefault(ctx, caller).String(), - Address: args[0].(string), - } - if err := msg.ValidateBasic(); err != nil { - return nil, 0, err - } - - if _, err := p.evmMsgServer.AssociateContractAddress(sdk.WrapSDKContext(ctx), msg); err != nil { - return nil, 0, err - } - - seiAddr := sdk.MustAccAddressFromBech32(msg.Address) - ret, err = method.Outputs.Pack(seiAddr.String(), common.BytesToAddress(seiAddr)) - return ret, pcommon.GetRemainingGas(ctx, p.evmKeeper), err -} - func (PrecompileExecutor) IsTransaction(method string) bool { switch method { - case Associate, AssociatePubKey, AssociateContractAddress: + case Associate, AssociatePubKey: return true default: return false diff --git a/precompiles/addr/addr_test.go b/precompiles/addr/addr_test.go index 281416636c..68ad339939 100644 --- a/precompiles/addr/addr_test.go +++ b/precompiles/addr/addr_test.go @@ -5,18 +5,14 @@ import ( "fmt" "math/big" "testing" - "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/sei-protocol/sei-chain/precompiles/addr" - sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" - evmkeeper "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/stretchr/testify/require" ) @@ -408,69 +404,3 @@ func TestGetAddr(t *testing.T) { require.Equal(t, 1, len(unpacked)) require.Equal(t, evmAddr, unpacked[0].(common.Address)) } - -func TestAssociateContractAddress(t *testing.T) { - testApp := testkeeper.EVMTestApp - ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2).WithBlockTime(time.Now()) - k := &testApp.EvmKeeper - - // a CW->ERC20 pointer is a real wasm contract, which is what - // associateContractAddress expects to be pointed at - dummySeiAddr, dummyEvmAddr := testkeeper.MockAddressPair() - res, err := evmkeeper.NewMsgServerImpl(k).RegisterPointer(sdk.WrapSDKContext(ctx), &evmtypes.MsgRegisterPointer{ - Sender: dummySeiAddr.String(), - PointerType: evmtypes.PointerType_ERC20, - ErcAddress: dummyEvmAddr.Hex(), - }) - require.Nil(t, err) - cwAddr := res.PointerAddress - - callerPrivKey := testkeeper.MockPrivateKey() - callerSeiAddress, callerEvmAddress := testkeeper.PrivateKeyToAddresses(callerPrivKey) - k.SetAddressMapping(ctx, callerSeiAddress, callerEvmAddress) - - pre, err := addr.NewPrecompile(testApp.GetPrecompileKeepers()) - require.Nil(t, err) - executor := pre.GetExecutor().(*addr.PrecompileExecutor) - method, err := pre.ABI.MethodById(executor.AssociateContractAddressID) - require.Nil(t, err) - - statedb := state.NewDBImpl(ctx, k, true) - evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: callerEvmAddress}} - - args, err := method.Inputs.Pack(cwAddr) - require.Nil(t, err) - - // should error because of read only call - _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, args...), 100000, nil, nil, true, false) - require.NotNil(t, err) - // should error because it's not payable - _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, args...), 100000, big.NewInt(1), nil, false, false) - require.NotNil(t, err) - // should error because the address is not a wasm contract - nonContractArgs, err := method.Inputs.Pack(dummySeiAddr.String()) - require.Nil(t, err) - _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, nonContractArgs...), 100000, nil, nil, false, false) - require.NotNil(t, err) - // should error because the address is not valid bech32 - invalidArgs, err := method.Inputs.Pack("not-bech32") - require.Nil(t, err) - _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, invalidArgs...), 100000, nil, nil, false, false) - require.NotNil(t, err) - - // happy path - ret, _, err := pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, args...), 100000, nil, nil, false, false) - require.Nil(t, err) - outputs, err := method.Outputs.Unpack(ret) - require.Nil(t, err) - require.Equal(t, cwAddr, outputs[0].(string)) - cwSeiAddr := sdk.MustAccAddressFromBech32(cwAddr) - require.Equal(t, common.BytesToAddress(cwSeiAddr), outputs[1].(common.Address)) - associatedEvmAddr, found := k.GetEVMAddress(statedb.Ctx(), cwSeiAddr) - require.True(t, found) - require.Equal(t, common.BytesToAddress(cwSeiAddr), associatedEvmAddr) - - // re-associating the same contract should fail - _, _, err = pre.RunAndCalculateGas(&evm, callerEvmAddress, callerEvmAddress, append(executor.AssociateContractAddressID, args...), 100000, nil, nil, false, false) - require.NotNil(t, err) -} diff --git a/precompiles/utils/expected_keepers.go b/precompiles/utils/expected_keepers.go index b2f42ae1f1..a410f04911 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -27,7 +27,6 @@ import ( 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" - evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" minttypes "github.com/sei-protocol/sei-chain/x/mint/types" oracletypes "github.com/sei-protocol/sei-chain/x/oracle/types" ) @@ -37,7 +36,6 @@ type Keepers interface { BankMS() BankMsgServer BankQ() BankQuerier EVMK() EVMKeeper - EvmMS() EvmMsgServer AccountK() AccountKeeper AuthQ() AuthQuerier AuthzMS() AuthzMsgServer @@ -74,7 +72,6 @@ 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) EvmMS() EvmMsgServer { return nil } func (ek *EmptyKeepers) AccountK() AccountKeeper { return nil } func (ek *EmptyKeepers) AuthQ() AuthQuerier { return nil } func (ek *EmptyKeepers) AuthzMS() AuthzMsgServer { return nil } @@ -123,10 +120,6 @@ type BankMsgServer interface { MultiSend(goCtx context.Context, msg *banktypes.MsgMultiSend) (*banktypes.MsgMultiSendResponse, error) } -type EvmMsgServer interface { - AssociateContractAddress(goCtx context.Context, msg *evmtypes.MsgAssociateContractAddress) (*evmtypes.MsgAssociateContractAddressResponse, error) -} - type AuthzMsgServer interface { Grant(goCtx context.Context, msg *authz.MsgGrant) (*authz.MsgGrantResponse, error) Exec(goCtx context.Context, msg *authz.MsgExec) (*authz.MsgExecResponse, error) From 99f081ec84f7242dd9dba775e23d04706844f8ce Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 30 Jul 2026 12:04:08 +0800 Subject: [PATCH 3/8] Rework bank multiSend to batch-send the caller's own coins Drop the ERC20-pointer gating: multiSend no longer requires the denom to have a native pointer nor the caller to be it. The caller can only spend its own funds (association required), so delegatecall is rejected. Co-Authored-By: Claude Fable 5 --- precompiles/bank/Bank.sol | 3 ++- precompiles/bank/abi.json | 2 +- precompiles/bank/bank.go | 27 +++++++++++++++---------- precompiles/bank/bank_test.go | 38 ++++++++++++++++++----------------- 4 files changed, 39 insertions(+), 31 deletions(-) diff --git a/precompiles/bank/Bank.sol b/precompiles/bank/Bank.sol index 2e6f85ae71..527d6670fa 100644 --- a/precompiles/bank/Bank.sol +++ b/precompiles/bank/Bank.sol @@ -16,8 +16,9 @@ interface IBank { uint256 amount ) external returns (bool success); + // Batch-sends the caller's own coins of the given denom. The caller must + // have an associated Sei address. function multiSend( - address fromAddress, address[] memory toAddresses, string memory denom, uint256[] memory amounts diff --git a/precompiles/bank/abi.json b/precompiles/bank/abi.json index 2df3247b25..c0ccc7a84d 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":"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":"address","name":"fromAddress","type":"address"},{"internalType":"address[]","name":"toAddresses","type":"address[]"},{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"multiSend","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","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 +[{"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":"address[]","name":"toAddresses","type":"address[]"},{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"multiSend","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","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 7db1944092..e125a0db01 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -17,6 +17,7 @@ import ( 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" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) const ( @@ -254,34 +255,38 @@ func (p PrecompileExecutor) send(ctx sdk.Context, caller common.Address, method return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err } +// multiSend batch-sends the caller's own coins of the given denom to the +// given receivers. Unlike send, it is not restricted to pointer contracts: +// the caller can only spend its own funds, so it requires an associated Sei +// address and rejects delegatecall (which would let a contract spend its own +// caller's funds). func (p PrecompileExecutor) multiSend(ctx sdk.Context, caller common.Address, method *abi.Method, args []interface{}, value *big.Int, readOnly bool) ([]byte, uint64, error) { if readOnly { return nil, 0, errors.New("cannot call multiSend from staticcall") } + if ctx.EVMPrecompileCalledFromDelegateCall() { + return nil, 0, errors.New("cannot delegatecall multiSend") + } if err := pcommon.ValidateNonPayable(value); err != nil { return nil, 0, err } - if err := pcommon.ValidateArgsLength(args, 4); err != nil { + if err := pcommon.ValidateArgsLength(args, 3); err != nil { return nil, 0, err } - denom := args[2].(string) + denom := args[1].(string) if denom == "" { return nil, 0, errors.New("invalid denom") } - pointer, _, exists := p.evmKeeper.GetERC20NativePointer(ctx, denom) - if !exists || pointer.Cmp(caller) != 0 { - return nil, 0, fmt.Errorf("only pointer %s can send %s but got %s", pointer.Hex(), denom, caller.Hex()) + senderSeiAddr, ok := p.evmKeeper.GetSeiAddress(ctx, caller) + if !ok { + return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) } - toAddresses := args[1].([]common.Address) - amounts := args[3].([]*big.Int) + toAddresses := args[0].([]common.Address) + amounts := args[2].([]*big.Int) if len(toAddresses) == 0 || len(toAddresses) != len(amounts) { return nil, 0, errors.New("toAddresses and amounts must be non-empty and of equal length") } - senderSeiAddr, err := p.accAddressFromArg(ctx, args[0]) - if err != nil { - return nil, 0, err - } total := sdk.ZeroInt() outputs := make([]banktypes.Output, 0, len(toAddresses)) diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go index 884329cf22..fb9c06b484 100644 --- a/precompiles/bank/bank_test.go +++ b/precompiles/bank/bank_test.go @@ -363,9 +363,6 @@ func TestMultiSend(t *testing.T) { require.Nil(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin("ufoo", sdk.NewInt(10000000))))) require.Nil(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, senderAddr, sdk.NewCoins(sdk.NewCoin("ufoo", sdk.NewInt(10000000))))) - _, pointerAddr := testkeeper.MockAddressPair() - k.SetERC20NativePointer(ctx, "ufoo", pointerAddr) - // One linked receiver, one unlinked receiver receiver1SeiAddr, receiver1EvmAddr := testkeeper.MockAddressPair() k.SetAddressMapping(ctx, receiver1SeiAddr, receiver1EvmAddr) @@ -382,32 +379,37 @@ func TestMultiSend(t *testing.T) { multiSend, err := p.ABI.MethodById(executor.MultiSendID) require.Nil(t, err) - args, err := multiSend.Inputs.Pack(senderEVMAddr, []common.Address{receiver1EvmAddr, receiver2EvmAddr}, "ufoo", []*big.Int{big.NewInt(100), big.NewInt(200)}) + args, err := multiSend.Inputs.Pack([]common.Address{receiver1EvmAddr, receiver2EvmAddr}, "ufoo", []*big.Int{big.NewInt(100), big.NewInt(200)}) require.Nil(t, err) - // should error because the caller is not the denom's pointer - _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, false) - require.NotNil(t, err) // should error because of read only call - _, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, args...), 200000, nil, nil, true, false) + _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, nil, nil, true, false) + require.NotNil(t, err) + // should error because of delegatecall + _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, true) require.NotNil(t, err) // should error because it's not payable - _, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, args...), 200000, big.NewInt(1), nil, false, false) + _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, big.NewInt(1), nil, false, false) + require.NotNil(t, err) + // should error because caller is not associated + _, unassociatedEvmAddr := testkeeper.MockAddressPair() + _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvmAddr, unassociatedEvmAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, false) require.NotNil(t, err) // should error because denom is empty - invalidDenomArgs, err := multiSend.Inputs.Pack(senderEVMAddr, []common.Address{receiver1EvmAddr}, "", []*big.Int{big.NewInt(100)}) + invalidDenomArgs, err := multiSend.Inputs.Pack([]common.Address{receiver1EvmAddr}, "", []*big.Int{big.NewInt(100)}) require.Nil(t, err) - _, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, invalidDenomArgs...), 200000, nil, nil, false, false) + _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, invalidDenomArgs...), 200000, nil, nil, false, false) require.NotNil(t, err) // should error because toAddresses and amounts lengths differ - mismatchedArgs, err := multiSend.Inputs.Pack(senderEVMAddr, []common.Address{receiver1EvmAddr}, "ufoo", []*big.Int{big.NewInt(1), big.NewInt(2)}) + mismatchedArgs, err := multiSend.Inputs.Pack([]common.Address{receiver1EvmAddr}, "ufoo", []*big.Int{big.NewInt(1), big.NewInt(2)}) require.Nil(t, err) - _, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, mismatchedArgs...), 200000, nil, nil, false, false) + _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, mismatchedArgs...), 200000, nil, nil, false, false) require.NotNil(t, err) - // success case sends to both linked and unlinked receivers; balances are - // checked on the statedb's context since that's where precompile writes land - ret, _, err := p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, false) + // success case sends the caller's own coins to both linked and unlinked + // receivers; balances are checked on the statedb's context since that's + // where precompile writes land + ret, _, err := p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, false) require.Nil(t, err) outputs, err := multiSend.Outputs.Unpack(ret) require.Nil(t, err) @@ -417,9 +419,9 @@ func TestMultiSend(t *testing.T) { require.Equal(t, int64(9999700), k.BankKeeper().GetBalance(statedb.Ctx(), senderAddr, "ufoo").Amount.Int64()) // zero amounts short circuit without moving funds - zeroArgs, err := multiSend.Inputs.Pack(senderEVMAddr, []common.Address{receiver1EvmAddr}, "ufoo", []*big.Int{big.NewInt(0)}) + zeroArgs, err := multiSend.Inputs.Pack([]common.Address{receiver1EvmAddr}, "ufoo", []*big.Int{big.NewInt(0)}) require.Nil(t, err) - ret, _, err = p.RunAndCalculateGas(&evm, pointerAddr, pointerAddr, append(executor.MultiSendID, zeroArgs...), 200000, nil, nil, false, false) + ret, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, zeroArgs...), 200000, nil, nil, false, false) require.Nil(t, err) outputs, err = multiSend.Outputs.Unpack(ret) require.Nil(t, err) From a849a0aee045b56c0e1324b9b62fb99c1e696af8 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 30 Jul 2026 12:04:08 +0800 Subject: [PATCH 4/8] Drop authz msg methods Co-Authored-By: Claude Fable 5 --- app/precompiles.go | 3 - precompiles/authz/Authz.sol | 39 ----- precompiles/authz/abi.json | 2 +- precompiles/authz/authz.go | 227 +------------------------- precompiles/authz/authz_test.go | 115 ------------- precompiles/utils/expected_keepers.go | 8 - 6 files changed, 10 insertions(+), 384 deletions(-) diff --git a/app/precompiles.go b/app/precompiles.go index ff6d67b889..c45c4e6880 100644 --- a/app/precompiles.go +++ b/app/precompiles.go @@ -20,7 +20,6 @@ type PrecompileKeepers struct { putils.EVMKeeper putils.AccountKeeper putils.AuthQuerier - putils.AuthzMsgServer putils.AuthzQuerier putils.OracleKeeper putils.WasmdKeeper @@ -56,7 +55,6 @@ func NewPrecompileKeepers(a *App) *PrecompileKeepers { EVMKeeper: &a.EvmKeeper, AccountKeeper: a.AccountKeeper, AuthQuerier: a.AccountKeeper, - AuthzMsgServer: a.AuthzKeeper, AuthzQuerier: a.AuthzKeeper, OracleKeeper: a.OracleKeeper, WasmdKeeper: wasmkeeper.NewDefaultPermissionKeeper(a.WasmKeeper), @@ -91,7 +89,6 @@ func (pk *PrecompileKeepers) BankQ() putils.BankQuerier { return 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) AuthzMS() putils.AuthzMsgServer { return pk.AuthzMsgServer } 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 } diff --git a/precompiles/authz/Authz.sol b/precompiles/authz/Authz.sol index d82cd9710a..e475869aad 100644 --- a/precompiles/authz/Authz.sol +++ b/precompiles/authz/Authz.sol @@ -6,45 +6,6 @@ address constant AUTHZ_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000 IAuthz constant AUTHZ_CONTRACT = IAuthz(AUTHZ_PRECOMPILE_ADDRESS); interface IAuthz { - // Transactions - - /** - * @notice Grant an authorization from the caller (granter) to a grantee - * @param grantee The grantee's EVM address (must be associated with a Sei address) - * @param authorization The authorization as protobuf-JSON bytes, e.g. - * {"@type":"/cosmos.bank.v1beta1.SendAuthorization","spend_limit":[{"denom":"usei","amount":"100"}]} - * @param expiration The expiration time of the grant as a Unix timestamp in seconds - * @return success True if the grant was stored successfully - */ - function grant( - address grantee, - bytes memory authorization, - int64 expiration - ) external returns (bool success); - - /** - * @notice Execute messages with the caller as the authz grantee. Messages - * whose signer is not the caller require a matching grant. EVM - * messages are not allowed. - * @param msgs The messages to execute, each as protobuf-JSON bytes, e.g. - * {"@type":"/cosmos.bank.v1beta1.MsgSend","from_address":"sei1...","to_address":"sei1...","amount":[...]} - * @return responses The protobuf-encoded response of each message - */ - function exec( - bytes[] memory msgs - ) external returns (bytes[] memory responses); - - /** - * @notice Revoke the caller's grant to a grantee for the given message type URL - * @param grantee The grantee's EVM address (must be associated with a Sei address) - * @param msgTypeUrl The message type URL of the authorization to revoke, e.g. "/cosmos.bank.v1beta1.MsgSend" - * @return success True if the grant was revoked successfully - */ - function revoke( - address grantee, - string memory msgTypeUrl - ) external returns (bool success); - // Queries /** diff --git a/precompiles/authz/abi.json b/precompiles/authz/abi.json index a4b19c3790..17e9770acf 100644 --- a/precompiles/authz/abi.json +++ b/precompiles/authz/abi.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"bytes[]","name":"msgs","type":"bytes[]"}],"name":"exec","outputs":[{"internalType":"bytes[]","name":"responses","type":"bytes[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"grantee","type":"address"},{"internalType":"bytes","name":"authorization","type":"bytes"},{"internalType":"int64","name":"expiration","type":"int64"}],"name":"grant","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"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"},{"inputs":[{"internalType":"address","name":"grantee","type":"address"},{"internalType":"string","name":"msgTypeUrl","type":"string"}],"name":"revoke","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] +[{"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 index 1b45fb60d7..30afd111bc 100644 --- a/precompiles/authz/authz.go +++ b/precompiles/authz/authz.go @@ -2,10 +2,8 @@ package authz import ( "embed" - "errors" "fmt" "math/big" - "time" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -17,23 +15,14 @@ import ( 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" - evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) const ( - GrantMethod = "grant" - ExecMethod = "exec" - RevokeMethod = "revoke" GrantsMethod = "grants" GranterGrantsMethod = "granterGrants" GranteeGrantsMethod = "granteeGrants" ) -// maxNestedMsgs caps MsgExec nesting when scanning exec messages, mirroring -// app/antedecorators.AuthzNestedMessageDecorator (which precompile calls -// bypass since they don't go through the cosmos ante handlers). -const maxNestedMsgs = 5 - const ( AuthzAddress = "0x000000000000000000000000000000000000100E" ) @@ -44,14 +33,10 @@ const ( var f embed.FS type PrecompileExecutor struct { - evmKeeper utils.EVMKeeper - authzMsgServer utils.AuthzMsgServer - authzQuerier utils.AuthzQuerier - cdc codec.Codec - - GrantID []byte - ExecID []byte - RevokeID []byte + evmKeeper utils.EVMKeeper + authzQuerier utils.AuthzQuerier + cdc codec.Codec + GrantsID []byte GranterGrantsID []byte GranteeGrantsID []byte @@ -61,20 +46,13 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) newAbi := pcommon.MustGetABI(f, "abi.json") p := &PrecompileExecutor{ - evmKeeper: keepers.EVMK(), - authzMsgServer: keepers.AuthzMS(), - authzQuerier: keepers.AuthzQ(), - cdc: keepers.Codec(), + evmKeeper: keepers.EVMK(), + authzQuerier: keepers.AuthzQ(), + cdc: keepers.Codec(), } for name, m := range newAbi.Methods { switch name { - case GrantMethod: - p.GrantID = m.ID - case ExecMethod: - p.ExecID = m.ID - case RevokeMethod: - p.RevokeID = m.ID case GrantsMethod: p.GrantsID = m.ID case GranterGrantsMethod: @@ -114,189 +92,9 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller case GranteeGrantsMethod: return p.granteeGrants(ctx, method, args, value) } - - // Transaction methods act on behalf of the caller, so they must not be - // reachable through delegatecall (which would let a contract act on - // behalf of its own caller) or staticcall. - if ctx.EVMPrecompileCalledFromDelegateCall() { - return nil, 0, errors.New("cannot delegatecall authz") - } - if readOnly { - return nil, 0, errors.New("cannot call authz precompile from staticcall") - } - switch method.Name { - case GrantMethod: - return p.grant(ctx, method, caller, args, value) - case ExecMethod: - return p.exec(ctx, method, caller, args, value) - case RevokeMethod: - return p.revoke(ctx, method, caller, args, value) - } return } -// grant stores an authorization from the caller (granter) to the grantee. The -// authorization is a protobuf-JSON encoded Authorization (the same encoding -// the query methods return), e.g. -// {"@type":"/cosmos.bank.v1beta1.SendAuthorization","spend_limit":[...]}. -func (p PrecompileExecutor) grant(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, 0, err - } - - if err := pcommon.ValidateArgsLength(args, 3); err != nil { - return nil, 0, err - } - - granter, found := p.evmKeeper.GetSeiAddress(ctx, caller) - if !found { - return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) - } - - grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) - if err != nil { - return nil, 0, err - } - - var authorization authztypes.Authorization - if err := p.cdc.UnmarshalInterfaceJSON(args[1].([]byte), &authorization); err != nil { - return nil, 0, fmt.Errorf("failed to parse authorization JSON: %w", err) - } - - expiration := time.Unix(args[2].(int64), 0).UTC() - msg, err := authztypes.NewMsgGrant(granter, grantee, authorization, expiration) - if err != nil { - return nil, 0, err - } - - if err := msg.ValidateBasic(); err != nil { - return nil, 0, err - } - - if _, err := p.authzMsgServer.Grant(sdk.WrapSDKContext(ctx), msg); err != nil { - return nil, 0, err - } - - bz, err := method.Outputs.Pack(true) - if err != nil { - return nil, 0, err - } - return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil -} - -// exec executes messages (each protobuf-JSON encoded) with the caller as the -// authz grantee. Messages whose signer is not the caller require a matching -// grant. EVM messages are rejected, mirroring the cosmos ante handler's -// restriction on nested EVM messages in MsgExec. -func (p PrecompileExecutor) exec(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, 0, err - } - - if err := pcommon.ValidateArgsLength(args, 1); err != nil { - return nil, 0, err - } - - grantee, found := p.evmKeeper.GetSeiAddress(ctx, caller) - if !found { - return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) - } - - msgBzs := args[0].([][]byte) - msgs := make([]sdk.Msg, len(msgBzs)) - for i, msgBz := range msgBzs { - var m sdk.Msg - if err := p.cdc.UnmarshalInterfaceJSON(msgBz, &m); err != nil { - return nil, 0, fmt.Errorf("failed to parse message JSON: %w", err) - } - msgs[i] = m - } - - if err := checkNoNestedEVMMessages(msgs, 0); err != nil { - return nil, 0, err - } - - msg := authztypes.NewMsgExec(grantee, msgs) - if err := msg.ValidateBasic(); err != nil { - return nil, 0, err - } - - resp, err := p.authzMsgServer.Exec(sdk.WrapSDKContext(ctx), &msg) - if err != nil { - return nil, 0, err - } - - results := resp.Results - if results == nil { - results = [][]byte{} - } - bz, err := method.Outputs.Pack(results) - if err != nil { - return nil, 0, err - } - return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil -} - -// revoke deletes the caller's grant to the grantee for the given msg type url. -func (p PrecompileExecutor) revoke(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, 0, err - } - - if err := pcommon.ValidateArgsLength(args, 2); err != nil { - return nil, 0, err - } - - granter, found := p.evmKeeper.GetSeiAddress(ctx, caller) - if !found { - return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) - } - - grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) - if err != nil { - return nil, 0, err - } - - msg := authztypes.NewMsgRevoke(granter, grantee, args[1].(string)) - if err := msg.ValidateBasic(); err != nil { - return nil, 0, err - } - - if _, err := p.authzMsgServer.Revoke(sdk.WrapSDKContext(ctx), &msg); err != nil { - return nil, 0, err - } - - bz, err := method.Outputs.Pack(true) - if err != nil { - return nil, 0, err - } - return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil -} - -// checkNoNestedEVMMessages rejects EVM messages anywhere in the message tree, -// recursing through nested MsgExecs, mirroring -// app/antedecorators.AuthzNestedMessageDecorator. -func checkNoNestedEVMMessages(msgs []sdk.Msg, nestedLvl int) error { - if nestedLvl >= maxNestedMsgs { - return errors.New("permission denied, more nested msgs than permitted") - } - for _, m := range msgs { - switch m := m.(type) { - case *evmtypes.MsgEVMTransaction: - return errors.New("permission denied, authz exec contains evm message") - case *authztypes.MsgExec: - nested, err := m.GetMessages() - if err != nil { - return err - } - if err := checkNoNestedEVMMessages(nested, nestedLvl+1); err != nil { - return err - } - } - } - return nil -} - type Grant struct { Authorization []byte Expiration int64 @@ -488,13 +286,6 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { return p.evmKeeper } -// IsTransaction returns true for methods that mutate state; all other authz -// methods are views. -func (PrecompileExecutor) IsTransaction(method string) bool { - switch method { - case GrantMethod, ExecMethod, RevokeMethod: - return true - default: - return false - } +func (PrecompileExecutor) IsTransaction(string) bool { + return false } diff --git a/precompiles/authz/authz_test.go b/precompiles/authz/authz_test.go index d53b2bccda..bc0707fc0c 100644 --- a/precompiles/authz/authz_test.go +++ b/precompiles/authz/authz_test.go @@ -8,12 +8,10 @@ import ( "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" - authztypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/authz" 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" - evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" "github.com/stretchr/testify/require" ) @@ -147,116 +145,3 @@ func TestGranteeGrants(t *testing.T) { require.Contains(t, authorizationJSON, "SendAuthorization") require.Equal(t, expiration.Unix(), grant.FieldByName("Expiration").Int()) } - -func TestGrantExecRevoke(t *testing.T) { - testApp := testkeeper.EVMTestApp - ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) - k := &testApp.EvmKeeper - cdc := testApp.AppCodec() - - granterSeiAddr, granterEvmAddr := testkeeper.MockAddressPair() - granteeSeiAddr, granteeEvmAddr := testkeeper.MockAddressPair() - thirdSeiAddr, thirdEvmAddr := testkeeper.MockAddressPair() - k.SetAddressMapping(ctx, granterSeiAddr, granterEvmAddr) - k.SetAddressMapping(ctx, granteeSeiAddr, granteeEvmAddr) - k.SetAddressMapping(ctx, thirdSeiAddr, thirdEvmAddr) - - amt := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))) - require.Nil(t, testApp.BankKeeper.MintCoins(ctx, evmtypes.ModuleName, amt)) - require.Nil(t, testApp.BankKeeper.SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, granterSeiAddr, amt)) - - 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) - sendMsgTypeURL := sdk.MsgTypeURL(&banktypes.MsgSend{}) - - // grant a send authorization to the grantee - authorizationJSON, err := cdc.MarshalInterfaceJSON(banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(500))))) - require.Nil(t, err) - expiration := time.Unix(1893456000, 0).UTC() - - grantMethod, err := p.ABI.MethodById(executor.GrantID) - require.Nil(t, err) - grantArgs, err := grantMethod.Inputs.Pack(granteeEvmAddr, authorizationJSON, expiration.Unix()) - require.Nil(t, err) - - // should error because of read only call - _, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantID, grantArgs...), 1000000, nil, nil, true, false) - require.NotNil(t, err) - // should error because of delegatecall - _, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantID, grantArgs...), 1000000, nil, nil, false, true) - require.NotNil(t, err) - // should error because caller is not associated - _, unassociatedEvmAddr := testkeeper.MockAddressPair() - _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvmAddr, unassociatedEvmAddr, append(executor.GrantID, grantArgs...), 1000000, nil, nil, false, false) - require.NotNil(t, err) - // should error because the authorization JSON is invalid - badAuthorizationArgs, err := grantMethod.Inputs.Pack(granteeEvmAddr, []byte("{"), expiration.Unix()) - require.Nil(t, err) - _, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantID, badAuthorizationArgs...), 1000000, nil, nil, false, false) - require.NotNil(t, err) - - ret, _, err := p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.GrantID, grantArgs...), 1000000, nil, nil, false, false) - require.Nil(t, err) - outputs, err := grantMethod.Outputs.Unpack(ret) - require.Nil(t, err) - require.True(t, outputs[0].(bool)) - authorization, _ := testApp.AuthzKeeper.GetCleanAuthorization(statedb.Ctx(), granteeSeiAddr, granterSeiAddr, sendMsgTypeURL) - require.NotNil(t, authorization) - - // exec a send on behalf of the granter as the grantee - sendJSON, err := cdc.MarshalInterfaceJSON(&banktypes.MsgSend{ - FromAddress: granterSeiAddr.String(), - ToAddress: granteeSeiAddr.String(), - Amount: sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(300))), - }) - require.Nil(t, err) - execMethod, err := p.ABI.MethodById(executor.ExecID) - require.Nil(t, err) - execArgs, err := execMethod.Inputs.Pack([][]byte{sendJSON}) - require.Nil(t, err) - - // should error because a third party has no grant for the granter's send - _, _, err = p.RunAndCalculateGas(&evm, thirdEvmAddr, thirdEvmAddr, append(executor.ExecID, execArgs...), 1000000, nil, nil, false, false) - require.NotNil(t, err) - - ret, _, err = p.RunAndCalculateGas(&evm, granteeEvmAddr, granteeEvmAddr, append(executor.ExecID, execArgs...), 1000000, nil, nil, false, false) - require.Nil(t, err) - execOutputs, err := execMethod.Outputs.Unpack(ret) - require.Nil(t, err) - require.Len(t, execOutputs, 1) - require.Equal(t, int64(300), testApp.BankKeeper.GetBalance(statedb.Ctx(), granteeSeiAddr, "usei").Amount.Int64()) - require.Equal(t, int64(700), testApp.BankKeeper.GetBalance(statedb.Ctx(), granterSeiAddr, "usei").Amount.Int64()) - - // exec of an EVM message is rejected - evmMsgJSON, err := cdc.MarshalInterfaceJSON(&evmtypes.MsgEVMTransaction{}) - require.Nil(t, err) - evmExecArgs, err := execMethod.Inputs.Pack([][]byte{evmMsgJSON}) - require.Nil(t, err) - _, _, err = p.RunAndCalculateGas(&evm, granteeEvmAddr, granteeEvmAddr, append(executor.ExecID, evmExecArgs...), 1000000, nil, nil, false, false) - require.NotNil(t, err) - - // exec of a nested MsgExec containing an EVM message is rejected - nestedExec := authztypes.NewMsgExec(granteeSeiAddr, []sdk.Msg{&evmtypes.MsgEVMTransaction{}}) - nestedExecJSON, err := cdc.MarshalInterfaceJSON(&nestedExec) - require.Nil(t, err) - nestedExecArgs, err := execMethod.Inputs.Pack([][]byte{nestedExecJSON}) - require.Nil(t, err) - _, _, err = p.RunAndCalculateGas(&evm, granteeEvmAddr, granteeEvmAddr, append(executor.ExecID, nestedExecArgs...), 1000000, nil, nil, false, false) - require.NotNil(t, err) - - // revoke the grant - revokeMethod, err := p.ABI.MethodById(executor.RevokeID) - require.Nil(t, err) - revokeArgs, err := revokeMethod.Inputs.Pack(granteeEvmAddr, sendMsgTypeURL) - require.Nil(t, err) - ret, _, err = p.RunAndCalculateGas(&evm, granterEvmAddr, granterEvmAddr, append(executor.RevokeID, revokeArgs...), 1000000, nil, nil, false, false) - require.Nil(t, err) - outputs, err = revokeMethod.Outputs.Unpack(ret) - require.Nil(t, err) - require.True(t, outputs[0].(bool)) - authorization, _ = testApp.AuthzKeeper.GetCleanAuthorization(statedb.Ctx(), granteeSeiAddr, granterSeiAddr, sendMsgTypeURL) - require.Nil(t, authorization) -} diff --git a/precompiles/utils/expected_keepers.go b/precompiles/utils/expected_keepers.go index a410f04911..dcd7cdf025 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -38,7 +38,6 @@ type Keepers interface { EVMK() EVMKeeper AccountK() AccountKeeper AuthQ() AuthQuerier - AuthzMS() AuthzMsgServer AuthzQ() AuthzQuerier OracleK() OracleKeeper WasmdK() WasmdKeeper @@ -74,7 +73,6 @@ 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) AuthzMS() AuthzMsgServer { return nil } func (ek *EmptyKeepers) AuthzQ() AuthzQuerier { return nil } func (ek *EmptyKeepers) OracleK() OracleKeeper { return nil } func (ek *EmptyKeepers) WasmdK() WasmdKeeper { return nil } @@ -120,12 +118,6 @@ type BankMsgServer interface { MultiSend(goCtx context.Context, msg *banktypes.MsgMultiSend) (*banktypes.MsgMultiSendResponse, error) } -type AuthzMsgServer interface { - Grant(goCtx context.Context, msg *authz.MsgGrant) (*authz.MsgGrantResponse, error) - Exec(goCtx context.Context, msg *authz.MsgExec) (*authz.MsgExecResponse, error) - Revoke(goCtx context.Context, msg *authz.MsgRevoke) (*authz.MsgRevokeResponse, error) -} - type FeegrantMsgServer interface { GrantAllowance(goCtx context.Context, msg *feegrant.MsgGrantAllowance) (*feegrant.MsgGrantAllowanceResponse, error) RevokeAllowance(goCtx context.Context, msg *feegrant.MsgRevokeAllowance) (*feegrant.MsgRevokeAllowanceResponse, error) From 8a81e7cc7d68f9eadec39fc60178f8c3c78fab45 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 30 Jul 2026 12:35:58 +0800 Subject: [PATCH 5/8] Drop bank multiSend Co-Authored-By: Claude Fable 5 --- precompiles/bank/Bank.sol | 8 --- precompiles/bank/abi.json | 2 +- precompiles/bank/bank.go | 80 --------------------------- precompiles/bank/bank_test.go | 78 -------------------------- precompiles/utils/expected_keepers.go | 1 - 5 files changed, 1 insertion(+), 168 deletions(-) diff --git a/precompiles/bank/Bank.sol b/precompiles/bank/Bank.sol index 527d6670fa..3436ef854d 100644 --- a/precompiles/bank/Bank.sol +++ b/precompiles/bank/Bank.sol @@ -16,14 +16,6 @@ interface IBank { uint256 amount ) external returns (bool success); - // Batch-sends the caller's own coins of the given denom. The caller must - // have an associated Sei address. - function multiSend( - address[] memory toAddresses, - string memory denom, - uint256[] memory amounts - ) external returns (bool success); - function sendNative( string memory toNativeAddress ) payable external returns (bool success); diff --git a/precompiles/bank/abi.json b/precompiles/bank/abi.json index c0ccc7a84d..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":"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":"address[]","name":"toAddresses","type":"address[]"},{"internalType":"string","name":"denom","type":"string"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"multiSend","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","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 +[{"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 e125a0db01..7478d3284e 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -17,12 +17,10 @@ import ( 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" - evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) const ( SendMethod = "send" - MultiSendMethod = "multiSend" SendNativeMethod = "sendNative" BalanceMethod = "balance" AllBalancesMethod = "all_balances" @@ -55,7 +53,6 @@ type PrecompileExecutor struct { address common.Address SendID []byte - MultiSendID []byte SendNativeID []byte BalanceID []byte AllBalancesID []byte @@ -119,8 +116,6 @@ func NewPrecompile(keepers putils.Keepers) (*pcommon.DynamicGasPrecompile, error switch name { case SendMethod: p.SendID = m.ID - case MultiSendMethod: - p.MultiSendID = m.ID case SendNativeMethod: p.SendNativeID = m.ID case BalanceMethod: @@ -172,8 +167,6 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller switch method.Name { case SendMethod: return p.send(ctx, caller, method, args, value, readOnly) - case MultiSendMethod: - return p.multiSend(ctx, caller, method, args, value, readOnly) case SendNativeMethod: return p.sendNative(ctx, method, args, caller, callingContract, value, readOnly, hooks, evm) case BalanceMethod: @@ -255,77 +248,6 @@ func (p PrecompileExecutor) send(ctx sdk.Context, caller common.Address, method return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err } -// multiSend batch-sends the caller's own coins of the given denom to the -// given receivers. Unlike send, it is not restricted to pointer contracts: -// the caller can only spend its own funds, so it requires an associated Sei -// address and rejects delegatecall (which would let a contract spend its own -// caller's funds). -func (p PrecompileExecutor) multiSend(ctx sdk.Context, caller common.Address, method *abi.Method, args []interface{}, value *big.Int, readOnly bool) ([]byte, uint64, error) { - if readOnly { - return nil, 0, errors.New("cannot call multiSend from staticcall") - } - if ctx.EVMPrecompileCalledFromDelegateCall() { - return nil, 0, errors.New("cannot delegatecall multiSend") - } - if err := pcommon.ValidateNonPayable(value); err != nil { - return nil, 0, err - } - - if err := pcommon.ValidateArgsLength(args, 3); err != nil { - return nil, 0, err - } - denom := args[1].(string) - if denom == "" { - return nil, 0, errors.New("invalid denom") - } - senderSeiAddr, ok := p.evmKeeper.GetSeiAddress(ctx, caller) - if !ok { - return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) - } - toAddresses := args[0].([]common.Address) - amounts := args[2].([]*big.Int) - if len(toAddresses) == 0 || len(toAddresses) != len(amounts) { - return nil, 0, errors.New("toAddresses and amounts must be non-empty and of equal length") - } - - total := sdk.ZeroInt() - outputs := make([]banktypes.Output, 0, len(toAddresses)) - for i, toAddress := range toAddresses { - if amounts[i].Cmp(utils.Big0) == 0 { - // zero-amount entries are no-ops, mirroring send's zero short circuit - continue - } - receiverSeiAddr, err := p.accAddressFromArg(ctx, toAddress) - if err != nil { - return nil, 0, err - } - amount := sdk.NewIntFromBigInt(amounts[i]) - total = total.Add(amount) - outputs = append(outputs, banktypes.NewOutput(receiverSeiAddr, sdk.NewCoins(sdk.NewCoin(denom, amount)))) - } - if len(outputs) == 0 { - // short circuit - bz, err := method.Outputs.Pack(true) - return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err - } - - msg := banktypes.NewMsgMultiSend( - []banktypes.Input{banktypes.NewInput(senderSeiAddr, sdk.NewCoins(sdk.NewCoin(denom, total)))}, - outputs, - ) - - if err := msg.ValidateBasic(); err != nil { - return nil, 0, err - } - - if _, err := p.bankMsgServer.MultiSend(sdk.WrapSDKContext(ctx), msg); err != nil { - return nil, 0, err - } - - bz, err := method.Outputs.Pack(true) - return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), err -} - func (p PrecompileExecutor) sendNative(ctx sdk.Context, method *abi.Method, args []interface{}, caller common.Address, callingContract common.Address, value *big.Int, readOnly bool, hooks *tracing.Hooks, evm *vm.EVM) ([]byte, uint64, error) { if readOnly { return nil, 0, errors.New("cannot call sendNative from staticcall") @@ -723,8 +645,6 @@ func (PrecompileExecutor) IsTransaction(method string) bool { switch method { case SendMethod: return true - case MultiSendMethod: - return true case SendNativeMethod: return true default: diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go index fb9c06b484..37e5630947 100644 --- a/precompiles/bank/bank_test.go +++ b/precompiles/bank/bank_test.go @@ -351,84 +351,6 @@ func TestSendForUnlinkedReceiver(t *testing.T) { require.NotNil(t, err) } -func TestMultiSend(t *testing.T) { - testApp := testkeeper.EVMTestApp - ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) - k := &testApp.EvmKeeper - - // Setup sender addresses and environment - privKey := testkeeper.MockPrivateKey() - senderAddr, senderEVMAddr := testkeeper.PrivateKeyToAddresses(privKey) - k.SetAddressMapping(ctx, senderAddr, senderEVMAddr) - require.Nil(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin("ufoo", sdk.NewInt(10000000))))) - require.Nil(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, senderAddr, sdk.NewCoins(sdk.NewCoin("ufoo", sdk.NewInt(10000000))))) - - // One linked receiver, one unlinked receiver - receiver1SeiAddr, receiver1EvmAddr := testkeeper.MockAddressPair() - k.SetAddressMapping(ctx, receiver1SeiAddr, receiver1EvmAddr) - _, receiver2EvmAddr := testkeeper.MockAddressPair() - - p, err := bank.NewPrecompile(testApp.GetPrecompileKeepers()) - require.Nil(t, err) - statedb := state.NewDBImpl(ctx, k, true) - evm := vm.EVM{ - StateDB: statedb, - TxContext: vm.TxContext{Origin: senderEVMAddr}, - } - executor := p.GetExecutor().(*bank.PrecompileExecutor) - - multiSend, err := p.ABI.MethodById(executor.MultiSendID) - require.Nil(t, err) - args, err := multiSend.Inputs.Pack([]common.Address{receiver1EvmAddr, receiver2EvmAddr}, "ufoo", []*big.Int{big.NewInt(100), big.NewInt(200)}) - require.Nil(t, err) - - // should error because of read only call - _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, nil, nil, true, false) - require.NotNil(t, err) - // should error because of delegatecall - _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, true) - require.NotNil(t, err) - // should error because it's not payable - _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, big.NewInt(1), nil, false, false) - require.NotNil(t, err) - // should error because caller is not associated - _, unassociatedEvmAddr := testkeeper.MockAddressPair() - _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvmAddr, unassociatedEvmAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, false) - require.NotNil(t, err) - // should error because denom is empty - invalidDenomArgs, err := multiSend.Inputs.Pack([]common.Address{receiver1EvmAddr}, "", []*big.Int{big.NewInt(100)}) - require.Nil(t, err) - _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, invalidDenomArgs...), 200000, nil, nil, false, false) - require.NotNil(t, err) - // should error because toAddresses and amounts lengths differ - mismatchedArgs, err := multiSend.Inputs.Pack([]common.Address{receiver1EvmAddr}, "ufoo", []*big.Int{big.NewInt(1), big.NewInt(2)}) - require.Nil(t, err) - _, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, mismatchedArgs...), 200000, nil, nil, false, false) - require.NotNil(t, err) - - // success case sends the caller's own coins to both linked and unlinked - // receivers; balances are checked on the statedb's context since that's - // where precompile writes land - ret, _, err := p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, args...), 200000, nil, nil, false, false) - require.Nil(t, err) - outputs, err := multiSend.Outputs.Unpack(ret) - require.Nil(t, err) - require.True(t, outputs[0].(bool)) - require.Equal(t, int64(100), k.BankKeeper().GetBalance(statedb.Ctx(), receiver1SeiAddr, "ufoo").Amount.Int64()) - require.Equal(t, int64(200), k.BankKeeper().GetBalance(statedb.Ctx(), sdk.AccAddress(receiver2EvmAddr[:]), "ufoo").Amount.Int64()) - require.Equal(t, int64(9999700), k.BankKeeper().GetBalance(statedb.Ctx(), senderAddr, "ufoo").Amount.Int64()) - - // zero amounts short circuit without moving funds - zeroArgs, err := multiSend.Inputs.Pack([]common.Address{receiver1EvmAddr}, "ufoo", []*big.Int{big.NewInt(0)}) - require.Nil(t, err) - ret, _, err = p.RunAndCalculateGas(&evm, senderEVMAddr, senderEVMAddr, append(executor.MultiSendID, zeroArgs...), 200000, nil, nil, false, false) - require.Nil(t, err) - outputs, err = multiSend.Outputs.Unpack(ret) - require.Nil(t, err) - require.True(t, outputs[0].(bool)) - require.Equal(t, int64(100), k.BankKeeper().GetBalance(statedb.Ctx(), receiver1SeiAddr, "ufoo").Amount.Int64()) -} - func TestMetadata(t *testing.T) { k := &testkeeper.EVMTestApp.EvmKeeper ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) diff --git a/precompiles/utils/expected_keepers.go b/precompiles/utils/expected_keepers.go index dcd7cdf025..efaaeb55c8 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -115,7 +115,6 @@ type BankKeeper interface { type BankMsgServer interface { Send(goCtx context.Context, msg *banktypes.MsgSend) (*banktypes.MsgSendResponse, error) - MultiSend(goCtx context.Context, msg *banktypes.MsgMultiSend) (*banktypes.MsgMultiSendResponse, error) } type FeegrantMsgServer interface { From 350b1da8933cb884f529f6ec9afd264375e30c82 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 30 Jul 2026 12:42:09 +0800 Subject: [PATCH 6/8] Allowlist the distr precompile as payable for event suppression fundCommunityPool is the first payable method on the distribution precompile, so distribution.DistrAddress must be in payablePrecompiles (both the x/evm and giga copies): otherwise the EVM value transfer into the precompile emits a coin_received event for the precompile address with no offsetting event, since the refund inside HandlePaymentUsei runs on a discarded event manager. Also assert the absence of that event in TestFundCommunityPool; the assertion fails without the allowlist entry. Co-Authored-By: Claude Fable 5 --- giga/deps/xevm/keeper/precompile.go | 10 ++++++---- precompiles/distribution/distribution_test.go | 16 ++++++++++++++++ x/evm/keeper/precompile.go | 10 ++++++---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/giga/deps/xevm/keeper/precompile.go b/giga/deps/xevm/keeper/precompile.go index 24a5714935..acae065781 100644 --- a/giga/deps/xevm/keeper/precompile.go +++ b/giga/deps/xevm/keeper/precompile.go @@ -3,6 +3,7 @@ package keeper import ( "github.com/ethereum/go-ethereum/common" "github.com/sei-protocol/sei-chain/precompiles/bank" + "github.com/sei-protocol/sei-chain/precompiles/distribution" "github.com/sei-protocol/sei-chain/precompiles/gov" "github.com/sei-protocol/sei-chain/precompiles/staking" "github.com/sei-protocol/sei-chain/precompiles/wasmd" @@ -11,10 +12,11 @@ import ( // add any payable precompiles here // these will suppress transfer events to/from the precompile address var payablePrecompiles = map[string]struct{}{ - bank.BankAddress: {}, - staking.StakingAddress: {}, - gov.GovAddress: {}, - wasmd.WasmdAddress: {}, + bank.BankAddress: {}, + staking.StakingAddress: {}, + gov.GovAddress: {}, + wasmd.WasmdAddress: {}, + distribution.DistrAddress: {}, } func IsPayablePrecompile(addr *common.Address) bool { diff --git a/precompiles/distribution/distribution_test.go b/precompiles/distribution/distribution_test.go index 1984aecb7a..87194f9f89 100644 --- a/precompiles/distribution/distribution_test.go +++ b/precompiles/distribution/distribution_test.go @@ -19,6 +19,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/secp256k1" crptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" slashingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/teststaking" stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" @@ -1669,6 +1670,21 @@ func TestFundCommunityPool(t *testing.T) { afterPool := testApp.DistrKeeper.GetFeePoolCommunityCoins(ctx) require.Equal(t, sdk.NewDecCoins(sdk.NewDecCoin("usei", sdk.NewInt(100))), afterPool.Sub(beforePool)) + // the distr precompile is in the payable allowlist, so the EVM value + // transfer into it must not leak an (unbalanced) coin_received event for + // the precompile address + distrSeiAddr := k.GetSeiAddressOrDefault(ctx, common.HexToAddress(distribution.DistrAddress)) + for _, event := range ctx.EventManager().Events() { + if event.Type != banktypes.EventTypeCoinReceived { + continue + } + for _, attr := range event.Attributes { + if string(attr.Key) == banktypes.AttributeKeyReceiver { + require.NotEqual(t, distrSeiAddr.String(), string(attr.Value)) + } + } + } + receipt, err := k.GetTransientReceipt(ctx, tx.Hash(), 0) require.Nil(t, err) require.Equal(t, 1, len(receipt.Logs)) diff --git a/x/evm/keeper/precompile.go b/x/evm/keeper/precompile.go index 24a5714935..acae065781 100644 --- a/x/evm/keeper/precompile.go +++ b/x/evm/keeper/precompile.go @@ -3,6 +3,7 @@ package keeper import ( "github.com/ethereum/go-ethereum/common" "github.com/sei-protocol/sei-chain/precompiles/bank" + "github.com/sei-protocol/sei-chain/precompiles/distribution" "github.com/sei-protocol/sei-chain/precompiles/gov" "github.com/sei-protocol/sei-chain/precompiles/staking" "github.com/sei-protocol/sei-chain/precompiles/wasmd" @@ -11,10 +12,11 @@ import ( // add any payable precompiles here // these will suppress transfer events to/from the precompile address var payablePrecompiles = map[string]struct{}{ - bank.BankAddress: {}, - staking.StakingAddress: {}, - gov.GovAddress: {}, - wasmd.WasmdAddress: {}, + bank.BankAddress: {}, + staking.StakingAddress: {}, + gov.GovAddress: {}, + wasmd.WasmdAddress: {}, + distribution.DistrAddress: {}, } func IsPayablePrecompile(addr *common.Address) bool { From 68d3043d7f337677b9d868c144f472c8ca52f623 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 30 Jul 2026 23:55:25 +0800 Subject: [PATCH 7/8] Drop distribution fundCommunityPool Also reverts the distr entry in payablePrecompiles, which existed only for fundCommunityPool. Co-Authored-By: Claude Fable 5 --- giga/deps/xevm/keeper/precompile.go | 10 +- precompiles/distribution/Distribution.sol | 7 -- precompiles/distribution/abi.json | 2 +- precompiles/distribution/distribution.go | 70 +------------ precompiles/distribution/distribution_test.go | 99 ------------------- precompiles/utils/expected_keepers.go | 1 - x/evm/keeper/precompile.go | 10 +- 7 files changed, 10 insertions(+), 189 deletions(-) diff --git a/giga/deps/xevm/keeper/precompile.go b/giga/deps/xevm/keeper/precompile.go index acae065781..24a5714935 100644 --- a/giga/deps/xevm/keeper/precompile.go +++ b/giga/deps/xevm/keeper/precompile.go @@ -3,7 +3,6 @@ package keeper import ( "github.com/ethereum/go-ethereum/common" "github.com/sei-protocol/sei-chain/precompiles/bank" - "github.com/sei-protocol/sei-chain/precompiles/distribution" "github.com/sei-protocol/sei-chain/precompiles/gov" "github.com/sei-protocol/sei-chain/precompiles/staking" "github.com/sei-protocol/sei-chain/precompiles/wasmd" @@ -12,11 +11,10 @@ import ( // add any payable precompiles here // these will suppress transfer events to/from the precompile address var payablePrecompiles = map[string]struct{}{ - bank.BankAddress: {}, - staking.StakingAddress: {}, - gov.GovAddress: {}, - wasmd.WasmdAddress: {}, - distribution.DistrAddress: {}, + bank.BankAddress: {}, + staking.StakingAddress: {}, + gov.GovAddress: {}, + wasmd.WasmdAddress: {}, } func IsPayablePrecompile(addr *common.Address) bool { diff --git a/precompiles/distribution/Distribution.sol b/precompiles/distribution/Distribution.sol index f82bd784d2..d9004a1db4 100644 --- a/precompiles/distribution/Distribution.sol +++ b/precompiles/distribution/Distribution.sol @@ -16,7 +16,6 @@ interface IDistr { event DelegationRewardsWithdrawn(address indexed delegator, string validator, uint256 amount); event MultipleDelegationRewardsWithdrawn(address indexed delegator, string[] validators, uint256[] amounts); event ValidatorCommissionWithdrawn(string indexed validator, uint256 amount); - event CommunityPoolFunded(address indexed depositor, uint256 amount); // Transactions @@ -43,12 +42,6 @@ interface IDistr { /// @return success True if commission was withdrawn successfully function withdrawValidatorCommission() external returns (bool success); - /// @notice Funds the community pool with the usei sent as the call's value - /// @dev The caller must have a valid associated Sei address; the value must - /// have no non-zero wei remainder (1usei = 10^12 wei) - /// @return success True if the community pool was funded successfully - function fundCommunityPool() external payable returns (bool success); - // Queries /// @notice Gets all pending rewards for a delegator diff --git a/precompiles/distribution/abi.json b/precompiles/distribution/abi.json index 6d01ccdc87..8a12f1b3a1 100755 --- a/precompiles/distribution/abi.json +++ b/precompiles/distribution/abi.json @@ -1 +1 @@ -[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"depositor","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"CommunityPoolFunded","type":"event"},{"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":"fundCommunityPool","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","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 +[{"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 16b2c92d28..e027b1109a 100644 --- a/precompiles/distribution/distribution.go +++ b/precompiles/distribution/distribution.go @@ -26,7 +26,6 @@ const ( WithdrawDelegationRewardsMethod = "withdrawDelegationRewards" WithdrawMultipleDelegationRewardsMethod = "withdrawMultipleDelegationRewards" WithdrawValidatorCommissionMethod = "withdrawValidatorCommission" - FundCommunityPoolMethod = "fundCommunityPool" RewardsMethod = "rewards" ParamsMethod = "params" ValidatorOutstandingRewardsMethod = "validatorOutstandingRewards" @@ -41,7 +40,6 @@ const ( DelegationRewardsEvent = "DelegationRewardsWithdrawn" MultipleDelegationRewardsEvent = "MultipleDelegationRewardsWithdrawn" ValidatorCommissionEvent = "ValidatorCommissionWithdrawn" - CommunityPoolFundedEvent = "CommunityPoolFunded" ) const ( @@ -53,7 +51,6 @@ var ( DelegationRewardsEventSig = crypto.Keccak256Hash([]byte("DelegationRewardsWithdrawn(address,string,uint256)")) MultipleDelegationRewardsEventSig = crypto.Keccak256Hash([]byte("MultipleDelegationRewardsWithdrawn(address,string[],uint256[])")) ValidatorCommissionEventSig = crypto.Keccak256Hash([]byte("ValidatorCommissionWithdrawn(string,uint256)")) - CommunityPoolFundedEventSig = crypto.Keccak256Hash([]byte("CommunityPoolFunded(address,uint256)")) ) // Embed abi json file to the executable binary. Needed when importing as dependency. @@ -65,14 +62,12 @@ type PrecompileExecutor struct { distrKeeper utils.DistributionKeeper distributionQuerier utils.DistributionQuerier evmKeeper utils.EVMKeeper - bankKeeper utils.BankKeeper address common.Address SetWithdrawAddrID []byte WithdrawDelegationRewardsID []byte WithdrawMultipleDelegationRewardsID []byte WithdrawValidatorCommissionID []byte - FundCommunityPoolID []byte RewardsID []byte ParamsID []byte ValidatorOutstandingRewardsID []byte @@ -93,7 +88,6 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) distrKeeper: keepers.DistributionK(), distributionQuerier: keepers.DistributionQ(), evmKeeper: keepers.EVMK(), - bankKeeper: keepers.BankK(), address: common.HexToAddress(DistrAddress), abi: newAbi, } @@ -108,8 +102,6 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) p.WithdrawMultipleDelegationRewardsID = m.ID case WithdrawValidatorCommissionMethod: p.WithdrawValidatorCommissionID = m.ID - case FundCommunityPoolMethod: - p.FundCommunityPoolID = m.ID case RewardsMethod: p.RewardsID = m.ID case ParamsMethod: @@ -168,11 +160,6 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller return nil, 0, errors.New("cannot call distr precompile from staticcall") } return p.withdrawValidatorCommission(ctx, method, caller, evm) - case FundCommunityPoolMethod: - if readOnly { - return nil, 0, errors.New("cannot call distr precompile from staticcall") - } - return p.fundCommunityPool(ctx, method, caller, args, value, evm, hooks) case RewardsMethod: return p.rewards(ctx, method, args) case ParamsMethod: @@ -203,7 +190,7 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { // distribution methods are views. func (PrecompileExecutor) IsTransaction(method string) bool { switch method { - case SetWithdrawAddressMethod, WithdrawDelegationRewardsMethod, WithdrawMultipleDelegationRewardsMethod, WithdrawValidatorCommissionMethod, FundCommunityPoolMethod: + case SetWithdrawAddressMethod, WithdrawDelegationRewardsMethod, WithdrawMultipleDelegationRewardsMethod, WithdrawValidatorCommissionMethod: return true default: return false @@ -818,58 +805,3 @@ func (p PrecompileExecutor) withdrawValidatorCommission(ctx sdk.Context, method } return } - -func (p PrecompileExecutor) fundCommunityPool(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int, evm *vm.EVM, hooks *tracing.Hooks) (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 := pcommon.ValidateArgsLength(args, 0); err != nil { - rerr = err - return - } - depositor, found := p.evmKeeper.GetSeiAddress(ctx, caller) - if !found { - rerr = types.NewAssociationMissingErr(caller.Hex()) - return - } - if value == nil || value.Sign() == 0 { - rerr = errors.New("set `value` field to non-zero to fund community pool") - return - } - - coin, err := pcommon.HandlePaymentUsei(ctx, p.evmKeeper.GetSeiAddressOrDefault(ctx, p.address), depositor, value, p.bankKeeper, p.evmKeeper, hooks, evm.GetDepth()) - if err != nil { - rerr = err - return - } - - if err := p.distrKeeper.FundCommunityPool(ctx, sdk.NewCoins(coin), depositor); err != nil { - rerr = err - return - } - - ret, rerr = method.Outputs.Pack(true) - remainingGas = pcommon.GetRemainingGas(ctx, p.evmKeeper) - if rerr != nil { - return - } - - logData, err := p.abi.Events[CommunityPoolFundedEvent].Inputs.NonIndexed().Pack(coin.Amount.BigInt()) - if err != nil { - rerr = err - return - } - if err := pcommon.EmitEVMLog(evm, p.address, []common.Hash{ - CommunityPoolFundedEventSig, - common.BytesToHash(caller.Bytes()), - }, logData); err != nil { - rerr = err - return - } - return -} diff --git a/precompiles/distribution/distribution_test.go b/precompiles/distribution/distribution_test.go index 87194f9f89..fcfd6cca57 100644 --- a/precompiles/distribution/distribution_test.go +++ b/precompiles/distribution/distribution_test.go @@ -19,7 +19,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/secp256k1" crptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" - banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" slashingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/teststaking" stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" @@ -887,10 +886,6 @@ func (tk *TestDistributionKeeper) GetDelegatorWithdrawAddr(ctx sdk.Context, delA return delAddr } -func (tk *TestDistributionKeeper) FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error { - return nil -} - func (tk *TestDistributionKeeper) DelegationTotalRewards(ctx context.Context, req *distrtypes.QueryDelegationTotalRewardsRequest) (*distrtypes.QueryDelegationTotalRewardsResponse, error) { uatomCoins := 1 val1useiCoins := 5 @@ -941,10 +936,6 @@ func (tk *TestEmptyRewardsDistributionKeeper) GetDelegatorWithdrawAddr(ctx sdk.C return delAddr } -func (tk *TestEmptyRewardsDistributionKeeper) FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error { - return nil -} - func TestPrecompile_RunAndCalculateGas_Rewards(t *testing.T) { callerSeiAddress, callerEvmAddress := testkeeper.MockAddressPair() _, notAssociatedCallerEvmAddress := testkeeper.MockAddressPair() @@ -1619,93 +1610,3 @@ func TestWithdrawValidatorCommission_InputValidation(t *testing.T) { }) } } - -func TestFundCommunityPool(t *testing.T) { - testApp := testkeeper.EVMTestApp - ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) - k := &testApp.EvmKeeper - - privKey := testkeeper.MockPrivateKey() - testPrivHex := hex.EncodeToString(privKey.Bytes()) - key, _ := crypto.HexToECDSA(testPrivHex) - seiAddr, evmAddr := testkeeper.PrivateKeyToAddresses(privKey) - k.SetAddressMapping(ctx, seiAddr, evmAddr) - amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(200000000))) - require.Nil(t, k.BankKeeper().MintCoins(ctx, evmtypes.ModuleName, amt)) - require.Nil(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, evmtypes.ModuleName, seiAddr, amt)) - - beforePool := testApp.DistrKeeper.GetFeePoolCommunityCoins(ctx) - - // fund the community pool with 100usei sent as tx value - abi := pcommon.MustGetABI(f, "abi.json") - args, err := abi.Pack("fundCommunityPool") - require.Nil(t, err) - addr := common.HexToAddress(distribution.DistrAddress) - txData := ethtypes.LegacyTx{ - GasPrice: big.NewInt(1000000000000), - Gas: 20000000, - To: &addr, - Value: big.NewInt(100_000_000_000_000), - Data: args, - Nonce: 0, - } - chainID := k.ChainID(ctx) - chainCfg := evmtypes.DefaultChainConfig() - ethCfg := chainCfg.EthereumConfig(chainID) - blockNum := big.NewInt(ctx.BlockHeight()) - signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix())) - tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) - require.Nil(t, err) - txwrapper, err := ethtx.NewLegacyTx(tx) - require.Nil(t, err) - req, err := evmtypes.NewMsgEVMTransaction(txwrapper) - require.Nil(t, err) - - msgServer := keeper.NewMsgServerImpl(k) - ante.Preprocess(ctx, req, k.ChainID(ctx), false) - res, err := msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req) - require.Nil(t, err) - require.Empty(t, res.VmError) - - afterPool := testApp.DistrKeeper.GetFeePoolCommunityCoins(ctx) - require.Equal(t, sdk.NewDecCoins(sdk.NewDecCoin("usei", sdk.NewInt(100))), afterPool.Sub(beforePool)) - - // the distr precompile is in the payable allowlist, so the EVM value - // transfer into it must not leak an (unbalanced) coin_received event for - // the precompile address - distrSeiAddr := k.GetSeiAddressOrDefault(ctx, common.HexToAddress(distribution.DistrAddress)) - for _, event := range ctx.EventManager().Events() { - if event.Type != banktypes.EventTypeCoinReceived { - continue - } - for _, attr := range event.Attributes { - if string(attr.Key) == banktypes.AttributeKeyReceiver { - require.NotEqual(t, distrSeiAddr.String(), string(attr.Value)) - } - } - } - - receipt, err := k.GetTransientReceipt(ctx, tx.Hash(), 0) - require.Nil(t, err) - require.Equal(t, 1, len(receipt.Logs)) - require.Equal(t, distribution.CommunityPoolFundedEventSig, common.HexToHash(receipt.Logs[0].Topics[0])) - require.Equal(t, common.BytesToHash(evmAddr.Bytes()), common.HexToHash(receipt.Logs[0].Topics[1])) - require.NotEmpty(t, receipt.Logs[0].Data) - - // error cases - p, err := distribution.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().(*distribution.PrecompileExecutor) - // should error because of read only call - _, _, err = p.RunAndCalculateGas(&evm, evmAddr, evmAddr, executor.FundCommunityPoolID, 100000, big.NewInt(100), nil, true, false) - require.NotNil(t, err) - // should error because caller is not associated - _, unassociatedEvmAddr := testkeeper.MockAddressPair() - _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvmAddr, unassociatedEvmAddr, executor.FundCommunityPoolID, 100000, big.NewInt(100), nil, false, false) - require.NotNil(t, err) - // should error because value is zero - _, _, err = p.RunAndCalculateGas(&evm, evmAddr, evmAddr, executor.FundCommunityPoolID, 100000, big.NewInt(0), nil, false, false) - require.NotNil(t, err) -} diff --git a/precompiles/utils/expected_keepers.go b/precompiles/utils/expected_keepers.go index efaaeb55c8..0b65bac481 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -229,7 +229,6 @@ type DistributionKeeper interface { WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (sdk.Coins, error) WithdrawValidatorCommission(ctx sdk.Context, valAddr sdk.ValAddress) (sdk.Coins, error) DelegationTotalRewards(c context.Context, req *distrtypes.QueryDelegationTotalRewardsRequest) (*distrtypes.QueryDelegationTotalRewardsResponse, error) - FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error } type TransferKeeper interface { diff --git a/x/evm/keeper/precompile.go b/x/evm/keeper/precompile.go index acae065781..24a5714935 100644 --- a/x/evm/keeper/precompile.go +++ b/x/evm/keeper/precompile.go @@ -3,7 +3,6 @@ package keeper import ( "github.com/ethereum/go-ethereum/common" "github.com/sei-protocol/sei-chain/precompiles/bank" - "github.com/sei-protocol/sei-chain/precompiles/distribution" "github.com/sei-protocol/sei-chain/precompiles/gov" "github.com/sei-protocol/sei-chain/precompiles/staking" "github.com/sei-protocol/sei-chain/precompiles/wasmd" @@ -12,11 +11,10 @@ import ( // add any payable precompiles here // these will suppress transfer events to/from the precompile address var payablePrecompiles = map[string]struct{}{ - bank.BankAddress: {}, - staking.StakingAddress: {}, - gov.GovAddress: {}, - wasmd.WasmdAddress: {}, - distribution.DistrAddress: {}, + bank.BankAddress: {}, + staking.StakingAddress: {}, + gov.GovAddress: {}, + wasmd.WasmdAddress: {}, } func IsPayablePrecompile(addr *common.Address) bool { From da9e73c59c8310ed73de6e45d08ba6fdf44a5712 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 30 Jul 2026 23:57:46 +0800 Subject: [PATCH 8/8] Drop feegrant allowance msg methods Co-Authored-By: Claude Fable 5 --- app/precompiles.go | 4 - precompiles/feegrant/Feegrant.sol | 10 -- precompiles/feegrant/abi.json | 43 --------- precompiles/feegrant/feegrant.go | 134 ++------------------------ precompiles/feegrant/feegrant_test.go | 77 --------------- precompiles/utils/expected_keepers.go | 7 -- 6 files changed, 8 insertions(+), 267 deletions(-) diff --git a/app/precompiles.go b/app/precompiles.go index c45c4e6880..4c74d724bd 100644 --- a/app/precompiles.go +++ b/app/precompiles.go @@ -5,7 +5,6 @@ import ( "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" - feegrantkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/feegrant/keeper" govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" slashingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/keeper" stakingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/keeper" @@ -32,7 +31,6 @@ type PrecompileKeepers struct { putils.DistributionKeeper putils.DistributionQuerier putils.EvidenceQuerier - putils.FeegrantMsgServer putils.FeegrantQuerier putils.MintQuerier putils.ParamsQuerier @@ -67,7 +65,6 @@ func NewPrecompileKeepers(a *App) *PrecompileKeepers { DistributionKeeper: a.DistrKeeper, DistributionQuerier: a.DistrKeeper, EvidenceQuerier: a.EvidenceKeeper, - FeegrantMsgServer: feegrantkeeper.NewMsgServerImpl(a.FeeGrantKeeper), FeegrantQuerier: a.FeeGrantKeeper, MintQuerier: mintkeeper.NewQuerier(a.MintKeeper), ParamsQuerier: a.ParamsKeeper, @@ -103,7 +100,6 @@ func (pk *PrecompileKeepers) DistributionQ() putils.DistributionQuerier { return pk.DistributionQuerier } func (pk *PrecompileKeepers) EvidenceQ() putils.EvidenceQuerier { return pk.EvidenceQuerier } -func (pk *PrecompileKeepers) FeegrantMS() putils.FeegrantMsgServer { return pk.FeegrantMsgServer } 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 } diff --git a/precompiles/feegrant/Feegrant.sol b/precompiles/feegrant/Feegrant.sol index 13e9967118..8db29633e6 100644 --- a/precompiles/feegrant/Feegrant.sol +++ b/precompiles/feegrant/Feegrant.sol @@ -6,16 +6,6 @@ address constant FEEGRANT_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000 IFeegrant constant FEEGRANT_CONTRACT = IFeegrant(FEEGRANT_PRECOMPILE_ADDRESS); interface IFeegrant { - // Transactions - // Grants a fee allowance from the caller (granter) to the grantee. The - // allowance is protobuf-JSON encoded (the same encoding the queries - // return), e.g. - // {"@type":"/cosmos.feegrant.v1beta1.BasicAllowance","spend_limit":[{"denom":"usei","amount":"1000"}],"expiration":null} - function grantAllowance(address grantee, bytes memory allowance) external returns (bool success); - - // Revokes the fee allowance granted by the caller to the grantee. - function revokeAllowance(address grantee) external returns (bool success); - // Queries // Returns the fee allowance granted to the grantee by the granter. The // allowance field of the returned grant is JSON-encoded. diff --git a/precompiles/feegrant/abi.json b/precompiles/feegrant/abi.json index 8c61c82c5a..c79c89ee50 100644 --- a/precompiles/feegrant/abi.json +++ b/precompiles/feegrant/abi.json @@ -145,48 +145,5 @@ ], "stateMutability": "view", "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "grantee", - "type": "address" - }, - { - "internalType": "bytes", - "name": "allowance", - "type": "bytes" - } - ], - "name": "grantAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "grantee", - "type": "address" - } - ], - "name": "revokeAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" } ] diff --git a/precompiles/feegrant/feegrant.go b/precompiles/feegrant/feegrant.go index 6107e5af04..34a5d6094d 100644 --- a/precompiles/feegrant/feegrant.go +++ b/precompiles/feegrant/feegrant.go @@ -16,12 +16,9 @@ import ( 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" - evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) const ( - GrantAllowanceMethod = "grantAllowance" - RevokeAllowanceMethod = "revokeAllowance" AllowanceMethod = "allowance" AllowancesMethod = "allowances" AllowancesByGranterMethod = "allowancesByGranter" @@ -37,13 +34,10 @@ const ( var f embed.FS type PrecompileExecutor struct { - evmKeeper utils.EVMKeeper - feegrantMsgServer utils.FeegrantMsgServer - feegrantQuerier utils.FeegrantQuerier - cdc codec.Codec + evmKeeper utils.EVMKeeper + feegrantQuerier utils.FeegrantQuerier + cdc codec.Codec - GrantAllowanceID []byte - RevokeAllowanceID []byte AllowanceID []byte AllowancesID []byte AllowancesByGranterID []byte @@ -53,18 +47,13 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) newAbi := pcommon.MustGetABI(f, "abi.json") p := &PrecompileExecutor{ - evmKeeper: keepers.EVMK(), - feegrantMsgServer: keepers.FeegrantMS(), - feegrantQuerier: keepers.FeegrantQ(), - cdc: keepers.Codec(), + evmKeeper: keepers.EVMK(), + feegrantQuerier: keepers.FeegrantQ(), + cdc: keepers.Codec(), } for name, m := range newAbi.Methods { switch name { - case GrantAllowanceMethod: - p.GrantAllowanceID = m.ID - case RevokeAllowanceMethod: - p.RevokeAllowanceID = m.ID case AllowanceMethod: p.AllowanceID = m.ID case AllowancesMethod: @@ -104,109 +93,9 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller case AllowancesByGranterMethod: return p.allowancesByGranter(ctx, method, args, value) } - - // Transaction methods act on behalf of the caller, so they must not be - // reachable through delegatecall (which would let a contract act on - // behalf of its own caller) or staticcall. - if ctx.EVMPrecompileCalledFromDelegateCall() { - return nil, 0, errors.New("cannot delegatecall feegrant") - } - if readOnly { - return nil, 0, errors.New("cannot call feegrant precompile from staticcall") - } - switch method.Name { - case GrantAllowanceMethod: - return p.grantAllowance(ctx, method, caller, args, value) - case RevokeAllowanceMethod: - return p.revokeAllowance(ctx, method, caller, args, value) - } return } -// grantAllowance stores a fee allowance from the caller (granter) to the -// grantee. The allowance is a protobuf-JSON encoded FeeAllowanceI (the same -// encoding the query methods return), e.g. -// {"@type":"/cosmos.feegrant.v1beta1.BasicAllowance","spend_limit":[...],"expiration":null}. -func (p PrecompileExecutor) grantAllowance(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, 0, err - } - - if err := pcommon.ValidateArgsLength(args, 2); err != nil { - return nil, 0, err - } - - granter, found := p.evmKeeper.GetSeiAddress(ctx, caller) - if !found { - return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) - } - - grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) - if err != nil { - return nil, 0, err - } - - var allowance feegranttypes.FeeAllowanceI - if err := p.cdc.UnmarshalInterfaceJSON(args[1].([]byte), &allowance); err != nil { - return nil, 0, fmt.Errorf("failed to parse allowance JSON: %w", err) - } - - msg, err := feegranttypes.NewMsgGrantAllowance(allowance, granter, grantee) - if err != nil { - return nil, 0, err - } - - if err := msg.ValidateBasic(); err != nil { - return nil, 0, err - } - - if _, err := p.feegrantMsgServer.GrantAllowance(sdk.WrapSDKContext(ctx), msg); err != nil { - return nil, 0, err - } - - bz, err := method.Outputs.Pack(true) - if err != nil { - return nil, 0, err - } - return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil -} - -// revokeAllowance removes the caller's fee allowance to the grantee. -func (p PrecompileExecutor) revokeAllowance(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, 0, err - } - - if err := pcommon.ValidateArgsLength(args, 1); err != nil { - return nil, 0, err - } - - granter, found := p.evmKeeper.GetSeiAddress(ctx, caller) - if !found { - return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) - } - - grantee, err := pcommon.GetSeiAddressFromArg(ctx, args[0], p.evmKeeper) - if err != nil { - return nil, 0, err - } - - msg := feegranttypes.NewMsgRevokeAllowance(granter, grantee) - if err := msg.ValidateBasic(); err != nil { - return nil, 0, err - } - - if _, err := p.feegrantMsgServer.RevokeAllowance(sdk.WrapSDKContext(ctx), &msg); err != nil { - return nil, 0, err - } - - bz, err := method.Outputs.Pack(true) - if err != nil { - return nil, 0, err - } - return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil -} - type Grant struct { Granter string Grantee string @@ -371,13 +260,6 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { return p.evmKeeper } -// IsTransaction returns true for methods that mutate state; all other feegrant -// methods are views. -func (PrecompileExecutor) IsTransaction(method string) bool { - switch method { - case GrantAllowanceMethod, RevokeAllowanceMethod: - return true - default: - return false - } +func (PrecompileExecutor) IsTransaction(string) bool { + return false } diff --git a/precompiles/feegrant/feegrant_test.go b/precompiles/feegrant/feegrant_test.go index 8bab523c98..719eb5da3e 100644 --- a/precompiles/feegrant/feegrant_test.go +++ b/precompiles/feegrant/feegrant_test.go @@ -159,80 +159,3 @@ func TestFeegrantQueries(t *testing.T) { require.Nil(t, ret) }) } - -func TestGrantAndRevokeAllowance(t *testing.T) { - testApp := testkeeper.EVMTestApp - ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2) - k := &testApp.EvmKeeper - cdc := testApp.AppCodec() - - granterSei, granterEvm := testkeeper.MockAddressPair() - granteeSei, granteeEvm := testkeeper.MockAddressPair() - k.SetAddressMapping(ctx, granterSei, granterEvm) - k.SetAddressMapping(ctx, granteeSei, granteeEvm) - - p, err := feegrant.NewPrecompile(testApp.GetPrecompileKeepers()) - require.NoError(t, err) - statedb := state.NewDBImpl(ctx, k, true) - evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: granterEvm}} - executor := p.GetExecutor().(*feegrant.PrecompileExecutor) - - allowanceJSON, err := cdc.MarshalInterfaceJSON(&feegranttypes.BasicAllowance{ - SpendLimit: sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))), - }) - require.NoError(t, err) - - grantMethod, err := p.ABI.MethodById(executor.GrantAllowanceID) - require.NoError(t, err) - grantArgs, err := grantMethod.Inputs.Pack(granteeEvm, allowanceJSON) - require.NoError(t, err) - - // should error because of read only call - _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, true, false) - require.Error(t, err) - // should error because of delegatecall - _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, false, true) - require.Error(t, err) - // should error because caller is not associated - _, unassociatedEvm := testkeeper.MockAddressPair() - _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvm, unassociatedEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, false, false) - require.Error(t, err) - // should error because the allowance JSON is invalid - badAllowanceArgs, err := grantMethod.Inputs.Pack(granteeEvm, []byte("{")) - require.NoError(t, err) - _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, badAllowanceArgs...), 1000000, nil, nil, false, false) - require.Error(t, err) - - ret, _, err := p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, false, false) - require.NoError(t, err) - outputs, err := grantMethod.Outputs.Unpack(ret) - require.NoError(t, err) - require.True(t, outputs[0].(bool)) - - stored, err := testApp.FeeGrantKeeper.GetAllowance(statedb.Ctx(), granterSei, granteeSei) - require.NoError(t, err) - basic, ok := stored.(*feegranttypes.BasicAllowance) - require.True(t, ok) - require.Equal(t, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000))), basic.SpendLimit) - - // re-granting while one exists should error - _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.GrantAllowanceID, grantArgs...), 1000000, nil, nil, false, false) - require.Error(t, err) - - // revoke the allowance - revokeMethod, err := p.ABI.MethodById(executor.RevokeAllowanceID) - require.NoError(t, err) - revokeArgs, err := revokeMethod.Inputs.Pack(granteeEvm) - require.NoError(t, err) - ret, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.RevokeAllowanceID, revokeArgs...), 1000000, nil, nil, false, false) - require.NoError(t, err) - outputs, err = revokeMethod.Outputs.Unpack(ret) - require.NoError(t, err) - require.True(t, outputs[0].(bool)) - _, err = testApp.FeeGrantKeeper.GetAllowance(statedb.Ctx(), granterSei, granteeSei) - require.Error(t, err) - - // revoking a non-existent allowance should error - _, _, err = p.RunAndCalculateGas(&evm, granterEvm, granterEvm, append(executor.RevokeAllowanceID, revokeArgs...), 1000000, nil, nil, false, false) - require.Error(t, err) -} diff --git a/precompiles/utils/expected_keepers.go b/precompiles/utils/expected_keepers.go index 0b65bac481..f2558fa192 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -50,7 +50,6 @@ type Keepers interface { DistributionK() DistributionKeeper DistributionQ() DistributionQuerier EvidenceQ() EvidenceQuerier - FeegrantMS() FeegrantMsgServer FeegrantQ() FeegrantQuerier MintQ() MintQuerier ParamsQ() ParamsQuerier @@ -87,7 +86,6 @@ func (ek *EmptyKeepers) DistributionQ() DistributionQuerier { return nil } func (ek *EmptyKeepers) EvidenceQ() EvidenceQuerier { return nil } -func (ek *EmptyKeepers) FeegrantMS() FeegrantMsgServer { return nil } func (ek *EmptyKeepers) FeegrantQ() FeegrantQuerier { return nil } func (ek *EmptyKeepers) MintQ() MintQuerier { return nil } func (ek *EmptyKeepers) ParamsQ() ParamsQuerier { return nil } @@ -117,11 +115,6 @@ type BankMsgServer interface { Send(goCtx context.Context, msg *banktypes.MsgSend) (*banktypes.MsgSendResponse, error) } -type FeegrantMsgServer interface { - GrantAllowance(goCtx context.Context, msg *feegrant.MsgGrantAllowance) (*feegrant.MsgGrantAllowanceResponse, error) - RevokeAllowance(goCtx context.Context, msg *feegrant.MsgRevokeAllowance) (*feegrant.MsgRevokeAllowanceResponse, error) -} - type SlashingMsgServer interface { Unjail(goCtx context.Context, msg *slashingtypes.MsgUnjail) (*slashingtypes.MsgUnjailResponse, error) }