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
5 changes: 5 additions & 0 deletions ante/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ante

import (
anteinterfaces "github.com/cosmos/evm/ante/interfaces"
"github.com/cosmos/evm/stakingguard"
ibckeeper "github.com/cosmos/ibc-go/v10/modules/core/keeper"

errorsmod "cosmossdk.io/errors"
Expand Down Expand Up @@ -33,6 +34,10 @@ type HandlerOptions struct {
// use dynamic fee checker or the cosmos-sdk default one for native transactions
DynamicFeeChecker bool
PendingTxListener PendingTxListener
// StakingGuardAllowlist, when set, turns on the PoA staking-guard ante decorator for the
// Cosmos/authz path. It blocks validator creation and unjail, limits the delegation family
// to the allowlisted vault, and recurses into authz.MsgExec.
StakingGuardAllowlist stakingguard.StakingPolicyFunc
}

// Validate checks if the keepers are defined
Expand Down
5 changes: 5 additions & 0 deletions ante/cosmos.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ante
import (
cosmosante "github.com/cosmos/evm/ante/cosmos"
evmante "github.com/cosmos/evm/ante/evm"
"github.com/cosmos/evm/stakingguard"
evmtypes "github.com/cosmos/evm/x/vm/types"
ibcante "github.com/cosmos/ibc-go/v10/modules/core/ante"
poaante "github.com/xrplevm/node/v10/x/poa/ante"
Expand Down Expand Up @@ -42,5 +43,9 @@ func newCosmosAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHandl
ante.NewIncrementSequenceDecorator(options.AccountKeeper),
ibcante.NewRedundantRelayDecorator(options.IBCKeeper),
poaante.NewPoaDecorator(),
// PoA staking guard: blocks MsgCreateValidator and MsgUnjail, limits the delegation
// family to the allowlisted vault, and recurses into authz.MsgExec. The vendored PoA
// decorator above handles neither validator management nor authz unwrapping.
stakingguard.NewDecorator(options.StakingGuardAllowlist),
)
}
8 changes: 4 additions & 4 deletions contracts/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

147 changes: 131 additions & 16 deletions contracts/solidity/StakedBondVault.json

Large diffs are not rendered by default.

302 changes: 260 additions & 42 deletions contracts/solidity/StakedBondVault.sol

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions evmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,9 @@ func NewExampleApp(
app.GovKeeper,
app.SlashingKeeper,
appCodec,
// PoA staking guard: only the configured community-pool vault may delegate via
// the staking precompile; validator creation and unjail are blocked.
precompiletypes.WithStakingGuard(app.stakingGuardAllowlist),
),
)

Expand Down Expand Up @@ -847,6 +850,7 @@ func (app *EVMD) setAnteHandler(txConfig client.TxConfig, maxGasWanted uint64) {
MaxTxGasWanted: maxGasWanted,
DynamicFeeChecker: true,
PendingTxListener: app.onPendingTx,
StakingGuardAllowlist: app.stakingGuardAllowlist,
}
if err := options.Validate(); err != nil {
panic(err)
Expand All @@ -855,6 +859,19 @@ func (app *EVMD) setAnteHandler(txConfig client.TxConfig, maxGasWanted uint64) {
app.SetAnteHandler(evmante.NewAnteHandler(options))
}

// stakingGuardAllowlist implements stakingguard.StakingPolicyFunc. Enforcement kicks in once
// the community-pool vault is configured as the EVM scheduler's target contract. From then on
// only that vault may use the staking precompile's delegation family, and validator creation
// and unjail are blocked. Before the vault is set (default or test genesis) the guard stays
// off. The vendored PoA ante blocks Cosmos-path delegation on its own regardless.
func (app *EVMD) stakingGuardAllowlist(ctx sdk.Context) (allowed []sdk.AccAddress, enforced bool) {
target := app.EVMKeeper.GetParams(ctx).Scheduler.TargetContract
if target == "" || !common.IsHexAddress(target) {
return nil, false
}
return []sdk.AccAddress{sdk.AccAddress(common.HexToAddress(target).Bytes())}, true
}

func (app *EVMD) onPendingTx(hash common.Hash) {
for _, listener := range app.pendingTxListeners {
listener(hash)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
testapp "github.com/cosmos/evm/testutil/app"
)

// The PoA staking guard is off in these suites because their genesis configures no
// community-pool vault (EVM scheduler target), so the staking precompile behaves as upstream.
// The guard's own enforcement (delegation limited to the vault, validator creation and unjail
// blocked) is covered by the x/vm StakedBondVault suite,
// TestStakingGuardBlocksNonVaultDelegationViaPrecompile, and the stakingguard unit tests.
func TestStakingPrecompileTestSuite(t *testing.T) {
create := testapp.ToEvmAppCreator[evm.StakingPrecompileApp](integration.CreateEvmd, "evm.StakingPrecompileApp")
s := staking.NewPrecompileTestSuite(create)
Expand Down
13 changes: 13 additions & 0 deletions precompiles/types/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
evmaddress "github.com/cosmos/evm/encoding/address"
ibcutils "github.com/cosmos/evm/ibc"
cmn "github.com/cosmos/evm/precompiles/common"
"github.com/cosmos/evm/stakingguard"
erc20Keeper "github.com/cosmos/evm/x/erc20/keeper"
transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper"
channelkeeper "github.com/cosmos/ibc-go/v10/modules/core/04-channel/keeper"
Expand All @@ -28,6 +29,10 @@ type Optionals struct {
AddressCodec address.Codec // used by gov/staking
ValidatorAddrCodec address.Codec // used by slashing
ConsensusAddrCodec address.Codec // used by slashing
// StakingGuardAllowlist, when set, applies the PoA staking policy to the staking and
// slashing precompiles: only allowlisted delegators may delegate, and validator creation
// and unjail are blocked. A nil value (the default) leaves the precompiles unguarded.
StakingGuardAllowlist stakingguard.StakingPolicyFunc
}

func defaultOptionals() Optionals {
Expand Down Expand Up @@ -58,6 +63,14 @@ func WithConsensusAddrCodec(codec address.Codec) Option {
}
}

// WithStakingGuard enables PoA staking enforcement on the staking and slashing
// precompiles using the given allowlist (read from live state at call time).
func WithStakingGuard(allowed stakingguard.StakingPolicyFunc) Option {
return func(opts *Optionals) {
opts.StakingGuardAllowlist = allowed
}
}

const bech32PrecompileBaseGas = 6_000

// DefaultStaticPrecompiles returns the list of all available static precompiled contracts from Cosmos EVM.
Expand Down
20 changes: 18 additions & 2 deletions precompiles/types/static_precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
slashingprecompile "github.com/cosmos/evm/precompiles/slashing"
stakingprecompile "github.com/cosmos/evm/precompiles/staking"
teeprecompile "github.com/cosmos/evm/precompiles/tee"
"github.com/cosmos/evm/stakingguard"

erc20Keeper "github.com/cosmos/evm/x/erc20/keeper"
transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper"
Expand All @@ -29,7 +30,9 @@ import (
distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper"
slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

type StaticPrecompiles map[common.Address]vm.PrecompiledContract
Expand Down Expand Up @@ -68,9 +71,16 @@ func (s StaticPrecompiles) WithStakingPrecompile(
opt(&options)
}

var stakingMsgServer stakingtypes.MsgServer = stakingkeeper.NewMsgServerImpl(&stakingKeeper)
if options.StakingGuardAllowlist != nil {
// Guard the EVM path: only the allowlisted vault may delegate, and validator creation
// is blocked. Gov onboarding goes through the module's own router, not this server.
stakingMsgServer = stakingguard.NewStakingMsgServer(stakingMsgServer, options.StakingGuardAllowlist)
}

stakingPrecompile := stakingprecompile.NewPrecompile(
stakingKeeper,
stakingkeeper.NewMsgServerImpl(&stakingKeeper),
stakingMsgServer,
stakingkeeper.NewQuerier(&stakingKeeper),
bankKeeper,
options.AddressCodec,
Expand Down Expand Up @@ -176,9 +186,15 @@ func (s StaticPrecompiles) WithSlashingPrecompile(
opt(&options)
}

var slashingMsgServer slashingtypes.MsgServer = slashingkeeper.NewMsgServerImpl(slashingKeeper)
if options.StakingGuardAllowlist != nil {
// Guard the EVM path: the slashing precompile's MsgUnjail is blocked on this PoA chain.
slashingMsgServer = stakingguard.NewSlashingMsgServer(slashingMsgServer, options.StakingGuardAllowlist)
}

slashingPrecompile := slashingprecompile.NewPrecompile(
slashingKeeper,
slashingkeeper.NewMsgServerImpl(slashingKeeper),
slashingMsgServer,
bankKeeper,
options.ValidatorAddrCodec,
options.ConsensusAddrCodec,
Expand Down
52 changes: 52 additions & 0 deletions stakingguard/ante.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package stakingguard

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/authz"
)

// maxAuthzNestingDepth bounds authz.MsgExec recursion to avoid pathological nesting.
const maxAuthzNestingDepth = 8

// Decorator is an ante decorator that enforces the PoA staking policy on the Cosmos tx
// path, recursing into authz.MsgExec so wrapped staking messages cannot bypass the guard.
// It complements the vendored PoA ante (which blocks the delegation family) by also
// blocking MsgCreateValidator / MsgUnjail and unwrapping authz.
type Decorator struct {
allowed StakingPolicyFunc
}

// NewDecorator returns a staking-guard ante decorator.
func NewDecorator(allowed StakingPolicyFunc) Decorator {
return Decorator{allowed: allowed}
}

// AnteHandle implements sdk.AnteDecorator.
func (d Decorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
for _, msg := range tx.GetMsgs() {
if err := d.checkRecursive(ctx, msg, 0); err != nil {
return ctx, err
}
}
return next(ctx, tx, simulate)
}

// checkRecursive applies the policy to msg, descending into authz.MsgExec payloads.
func (d Decorator) checkRecursive(ctx sdk.Context, msg sdk.Msg, depth int) error {
if exec, ok := msg.(*authz.MsgExec); ok {
if depth >= maxAuthzNestingDepth {
return ErrDelegationRestricted.Wrap("authz nesting too deep")
}
inner, err := exec.GetMessages()
if err != nil {
return err
}
for _, im := range inner {
if err := d.checkRecursive(ctx, im, depth+1); err != nil {
return err
}
}
return nil
}
return CheckMsg(ctx, msg, d.allowed)
}
74 changes: 74 additions & 0 deletions stakingguard/msgserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package stakingguard

import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

// NewStakingMsgServer wraps a staking MsgServer so that every delegation-family and
// validator-creation call routed through it (e.g. by the staking precompile) is checked
// against the PoA policy before reaching the underlying server.
func NewStakingMsgServer(inner stakingtypes.MsgServer, allowed StakingPolicyFunc) stakingtypes.MsgServer {
return &stakingMsgServer{MsgServer: inner, allowed: allowed}
}

type stakingMsgServer struct {
stakingtypes.MsgServer // EditValidator + UpdateParams fall through unguarded
allowed StakingPolicyFunc
}

func (s *stakingMsgServer) CreateValidator(ctx context.Context, msg *stakingtypes.MsgCreateValidator) (*stakingtypes.MsgCreateValidatorResponse, error) {
if err := CheckMsg(sdk.UnwrapSDKContext(ctx), msg, s.allowed); err != nil {
return nil, err
}
return s.MsgServer.CreateValidator(ctx, msg)
}

func (s *stakingMsgServer) Delegate(ctx context.Context, msg *stakingtypes.MsgDelegate) (*stakingtypes.MsgDelegateResponse, error) {
if err := CheckMsg(sdk.UnwrapSDKContext(ctx), msg, s.allowed); err != nil {
return nil, err
}
return s.MsgServer.Delegate(ctx, msg)
}

func (s *stakingMsgServer) Undelegate(ctx context.Context, msg *stakingtypes.MsgUndelegate) (*stakingtypes.MsgUndelegateResponse, error) {
if err := CheckMsg(sdk.UnwrapSDKContext(ctx), msg, s.allowed); err != nil {
return nil, err
}
return s.MsgServer.Undelegate(ctx, msg)
}

func (s *stakingMsgServer) BeginRedelegate(ctx context.Context, msg *stakingtypes.MsgBeginRedelegate) (*stakingtypes.MsgBeginRedelegateResponse, error) {
if err := CheckMsg(sdk.UnwrapSDKContext(ctx), msg, s.allowed); err != nil {
return nil, err
}
return s.MsgServer.BeginRedelegate(ctx, msg)
}

func (s *stakingMsgServer) CancelUnbondingDelegation(ctx context.Context, msg *stakingtypes.MsgCancelUnbondingDelegation) (*stakingtypes.MsgCancelUnbondingDelegationResponse, error) {
if err := CheckMsg(sdk.UnwrapSDKContext(ctx), msg, s.allowed); err != nil {
return nil, err
}
return s.MsgServer.CancelUnbondingDelegation(ctx, msg)
}

// NewSlashingMsgServer wraps a slashing MsgServer so that MsgUnjail routed through it
// (e.g. by the slashing precompile) is rejected under the PoA policy.
func NewSlashingMsgServer(inner slashingtypes.MsgServer, allowed StakingPolicyFunc) slashingtypes.MsgServer {
return &slashingMsgServer{MsgServer: inner, allowed: allowed}
}

type slashingMsgServer struct {
slashingtypes.MsgServer // UpdateParams falls through unguarded
allowed StakingPolicyFunc
}

func (s *slashingMsgServer) Unjail(ctx context.Context, msg *slashingtypes.MsgUnjail) (*slashingtypes.MsgUnjailResponse, error) {
if err := CheckMsg(sdk.UnwrapSDKContext(ctx), msg, s.allowed); err != nil {
return nil, err
}
return s.MsgServer.Unjail(ctx, msg)
}
Loading