diff --git a/app/precompiles.go b/app/precompiles.go index bf1b9d751e..4c74d724bd 100644 --- a/app/precompiles.go +++ b/app/precompiles.go @@ -6,6 +6,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/codec" bankkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/keeper" govkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/keeper" + 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" mintkeeper "github.com/sei-protocol/sei-chain/x/mint/keeper" @@ -33,6 +34,7 @@ type PrecompileKeepers struct { putils.FeegrantQuerier putils.MintQuerier putils.ParamsQuerier + putils.SlashingMsgServer putils.SlashingQuerier putils.UpgradeQuerier putils.TransferKeeper @@ -66,6 +68,7 @@ func NewPrecompileKeepers(a *App) *PrecompileKeepers { 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, @@ -100,6 +103,7 @@ func (pk *PrecompileKeepers) EvidenceQ() putils.EvidenceQuerier { return pk.E 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/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..f2558fa192 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -53,6 +53,7 @@ type Keepers interface { FeegrantQ() FeegrantQuerier MintQ() MintQuerier ParamsQ() ParamsQuerier + SlashingMS() SlashingMsgServer SlashingQ() SlashingQuerier UpgradeQ() UpgradeQuerier TransferK() TransferKeeper @@ -88,6 +89,7 @@ func (ek *EmptyKeepers) EvidenceQ() EvidenceQuerier { return nil } func (ek *EmptyKeepers) FeegrantQ() FeegrantQuerier { return nil } func (ek *EmptyKeepers) MintQ() MintQuerier { return nil } func (ek *EmptyKeepers) ParamsQ() ParamsQuerier { return nil } +func (ek *EmptyKeepers) 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 } @@ -113,6 +115,10 @@ type BankMsgServer interface { Send(goCtx context.Context, msg *banktypes.MsgSend) (*banktypes.MsgSendResponse, error) } +type SlashingMsgServer interface { + Unjail(goCtx context.Context, msg *slashingtypes.MsgUnjail) (*slashingtypes.MsgUnjailResponse, error) +} + type EVMKeeper interface { GetSeiAddress(sdk.Context, common.Address) (sdk.AccAddress, bool) GetSeiAddressOrDefault(ctx sdk.Context, evmAddress common.Address) sdk.AccAddress // only used for getting precompile Sei addresses