Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -33,6 +34,7 @@ type PrecompileKeepers struct {
putils.FeegrantQuerier
putils.MintQuerier
putils.ParamsQuerier
putils.SlashingMsgServer
putils.SlashingQuerier
putils.UpgradeQuerier
putils.TransferKeeper
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }
Expand Down
5 changes: 5 additions & 0 deletions precompiles/slashing/Slashing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
13 changes: 13 additions & 0 deletions precompiles/slashing/abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,18 @@
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "unjail",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
73 changes: 67 additions & 6 deletions precompiles/slashing/slashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package slashing

import (
"embed"
"errors"
"fmt"
"math/big"

Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Worth an explicit note that placing the delegatecall guard after the query dispatch is deliberate. gov, distribution, and staking all reject delegatecall at the top of Execute for every method including views; here the existing params/signingInfo/signingInfos views stay delegatecall-reachable. That's the right call (tightening them would be a consensus-visible behavior change for existing contracts), but the current comment only explains why tx methods are guarded, not why the guard sits below the view switch — a future reader could easily "fix" the ordering to match gov and silently break callers.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This naked return yields (nil, 0, nil) — reported success, empty output, and remainingGas == 0 so the caller's entire gas is consumed. Pre-existing shape (gov/distribution do the same), but now that Execute has two dispatch switches it's easier for a future method added to abi.json to land here unhandled. Returning an explicit fmt.Errorf("unknown method %s", method.Name) would fail loudly instead.

}

// 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
Expand Down Expand Up @@ -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
}
}
91 changes: 91 additions & 0 deletions precompiles/slashing/slashing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Typo in the import alias: crptotypescryptotypes (used again on the setupValidator signature below).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Typo in the import alias: crptotypescryptotypes (the alias used elsewhere in the repo).

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"
)

Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] bondStatus is only ever passed stakingtypes.Unbonded, and since CreateValidatorMsg/Handle already leaves the validator Unbonded until the staking EndBlocker runs, val.UpdateStatus(bondStatus) + SetValidator are effectively no-ops. Consider dropping the parameter (and the redundant status write) until a second caller actually needs it.

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())
}
6 changes: 6 additions & 0 deletions precompiles/utils/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Keepers interface {
FeegrantQ() FeegrantQuerier
MintQ() MintQuerier
ParamsQ() ParamsQuerier
SlashingMS() SlashingMsgServer
SlashingQ() SlashingQuerier
UpgradeQ() UpgradeQuerier
TransferK() TransferKeeper
Expand Down Expand Up @@ -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 }
Expand All @@ -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
Expand Down
Loading