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
10 changes: 10 additions & 0 deletions app/ante/ante_cosmos.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ func NewCosmosAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHandl
sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}),
sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}),
),
// Vesting accounts can delegate locked coins, but the EVM state view only
// tracks spendable balance. Delegating more than the spendable balance makes
// the StateDB subtract more than it holds, which reconciles back to bank as a
// mint (or a burn for the victim). Block vesting-account creation outright so
// the precondition cannot be created permissionlessly.
NewBlockedMsgsDecorator(
sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}),
sdk.MsgTypeURL(&sdkvesting.MsgCreatePermanentLockedAccount{}),
sdk.MsgTypeURL(&sdkvesting.MsgCreatePeriodicVestingAccount{}),
),

ante.NewSetUpContextDecorator(),
wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), // after setup context to enforce limits early
Expand Down
91 changes: 91 additions & 0 deletions app/ante/blocked_msgs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package ante

import (
"fmt"

errorsmod "cosmossdk.io/errors"

sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/authz"
)

// maxNestedBlockedMsgs caps how deep the decorator recurses into nested
// authz.MsgExec messages while looking for blocked msg types.
const maxNestedBlockedMsgs = 7

// BlockedMsgsDecorator rejects a fixed set of msg type URLs anywhere in a tx:
// at the top level, and nested inside authz.MsgExec (arbitrarily deep, up to
// maxNestedBlockedMsgs).
//
// It complements cosmosante.NewAuthzLimiterDecorator, which only blocks msgs
// carried *inside* an authz message and lets the same msg through when it is
// submitted directly.
type BlockedMsgsDecorator struct {
// blockedMsgTypes is the set of msg type URLs to reject.
blockedMsgTypes map[string]struct{}
}

// NewBlockedMsgsDecorator creates a decorator that rejects the given msg type
// URLs regardless of where they appear in the tx.
func NewBlockedMsgsDecorator(blockedMsgTypes ...string) BlockedMsgsDecorator {
blocked := make(map[string]struct{}, len(blockedMsgTypes))
for _, msgType := range blockedMsgTypes {
blocked[msgType] = struct{}{}
}

return BlockedMsgsDecorator{blockedMsgTypes: blocked}
}

func (bmd BlockedMsgsDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
if err := bmd.checkBlockedMsgs(tx.GetMsgs(), 1); err != nil {
return ctx, errorsmod.Wrapf(errortypes.ErrUnauthorized, "%s", err.Error())
}

return next(ctx, tx, simulate)
}

// checkBlockedMsgs walks the msgs and returns an error on the first blocked msg
// type it finds. authz.MsgExec is unwrapped so a blocked msg cannot be smuggled
// through the authz module; authz.MsgGrant is checked so a grant for a blocked
// msg type cannot be created either.
func (bmd BlockedMsgsDecorator) checkBlockedMsgs(msgs []sdk.Msg, nestedLvl int) error {
if nestedLvl >= maxNestedBlockedMsgs {
return fmt.Errorf("found more nested msgs than permitted; got: %d, expected: <%d", nestedLvl, maxNestedBlockedMsgs)
}

for _, msg := range msgs {
switch msg := msg.(type) {
case *authz.MsgExec:
innerMsgs, err := msg.GetMessages()
if err != nil {
return err
}
if err := bmd.checkBlockedMsgs(innerMsgs, nestedLvl+1); err != nil {
return err
}
case *authz.MsgGrant:
authorization, err := msg.GetAuthorization()
if err != nil {
return err
}
if err := bmd.rejectIfBlocked(authorization.MsgTypeURL()); err != nil {
return err
}
default:
if err := bmd.rejectIfBlocked(sdk.MsgTypeURL(msg)); err != nil {
return err
}
}
}

return nil
}

func (bmd BlockedMsgsDecorator) rejectIfBlocked(msgTypeURL string) error {
if _, blocked := bmd.blockedMsgTypes[msgTypeURL]; blocked {
return fmt.Errorf("found blocked msg type: %s", msgTypeURL)
}

return nil
}
147 changes: 147 additions & 0 deletions app/ante/blocked_msgs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package ante_test

import (
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
"github.com/cosmos/cosmos-sdk/x/authz"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/stretchr/testify/require"

"github.com/pushchain/push-chain-node/app/ante"
)

// blockedVestingMsgURLs mirrors the list wired into NewCosmosAnteHandler.
var blockedVestingMsgURLs = []string{
sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}),
sdk.MsgTypeURL(&sdkvesting.MsgCreatePermanentLockedAccount{}),
sdk.MsgTypeURL(&sdkvesting.MsgCreatePeriodicVestingAccount{}),
}

func vestingTestAddrs() (from, to sdk.AccAddress) {
return sdk.AccAddress([]byte("from________________")), sdk.AccAddress([]byte("to__________________"))
}

// nestMsgExec wraps msgs in `depth` levels of authz.MsgExec.
func nestMsgExec(grantee sdk.AccAddress, depth int, msgs []sdk.Msg) sdk.Msg {
inner := msgs
var out sdk.Msg
for i := 0; i < depth; i++ {
exec := authz.NewMsgExec(grantee, inner)
out = &exec
inner = []sdk.Msg{out}
}
return out
}

// TestBlockedMsgsDecorator_VestingMsgs asserts that all three vesting-account
// creation msgs are rejected at the TOP LEVEL of a tx (F-2026-18201). Before
// this decorator only MsgCreateVestingAccount was blocked, and only when nested
// inside an authz.MsgExec, so a plain top-level tx created the vesting account
// that the staking-precompile underflow attack needs.
func TestBlockedMsgsDecorator_VestingMsgs(t *testing.T) {
from, to := vestingTestAddrs()
amount := sdk.NewCoins(sdk.NewInt64Coin("upc", 1_000_000))
future := time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC)

createVesting := sdkvesting.NewMsgCreateVestingAccount(from, to, amount, future.Unix(), false)
createPermanentLocked := sdkvesting.NewMsgCreatePermanentLockedAccount(from, to, amount)
createPeriodicVesting := sdkvesting.NewMsgCreatePeriodicVestingAccount(from, to, 0, []sdkvesting.Period{
{Length: 3600, Amount: amount},
})
send := banktypes.NewMsgSend(from, to, amount)

decorator := ante.NewBlockedMsgsDecorator(blockedVestingMsgURLs...)

testCases := []struct {
name string
msgs []sdk.Msg
expFail bool
}{
{"allowed msg passes", []sdk.Msg{send}, false},
{"top-level MsgCreateVestingAccount", []sdk.Msg{createVesting}, true},
{"top-level MsgCreatePermanentLockedAccount", []sdk.Msg{createPermanentLocked}, true},
{"top-level MsgCreatePeriodicVestingAccount", []sdk.Msg{createPeriodicVesting}, true},
{"blocked msg alongside allowed msgs", []sdk.Msg{send, createPermanentLocked, send}, true},
{
"blocked msg inside authz.MsgExec",
[]sdk.Msg{nestMsgExec(from, 1, []sdk.Msg{createPermanentLocked})},
true,
},
{
"blocked msg inside deeply nested authz.MsgExec",
[]sdk.Msg{nestMsgExec(from, 4, []sdk.Msg{createPeriodicVesting})},
true,
},
{
"allowed msg inside authz.MsgExec passes",
[]sdk.Msg{nestMsgExec(from, 2, []sdk.Msg{send})},
false,
},
{
"nesting deeper than the cap is rejected",
[]sdk.Msg{nestMsgExec(from, 8, []sdk.Msg{send})},
true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tx := mockFeeTx{msgs: tc.msgs}

called := false
next := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) {
called = true
return ctx, nil
}

_, err := decorator.AnteHandle(sdk.Context{}, tx, false, next)
if tc.expFail {
require.Error(t, err)
require.ErrorIs(t, err, sdkerrors.ErrUnauthorized)
require.False(t, called, "blocked tx must not reach the next decorator")
return
}

require.NoError(t, err)
require.True(t, called, "allowed tx must reach the next decorator")
})
}
}

// TestBlockedMsgsDecorator_AuthzGrant asserts that an authz grant for a blocked
// vesting msg type cannot be created either, so the block cannot be sidestepped
// by pre-authorizing a grantee.
func TestBlockedMsgsDecorator_AuthzGrant(t *testing.T) {
from, to := vestingTestAddrs()
future := time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC)

decorator := ante.NewBlockedMsgsDecorator(blockedVestingMsgURLs...)

for _, msgURL := range blockedVestingMsgURLs {
t.Run(msgURL, func(t *testing.T) {
grant, err := authz.NewMsgGrant(from, to, authz.NewGenericAuthorization(msgURL), &future)
require.NoError(t, err)

_, err = decorator.AnteHandle(sdk.Context{}, mockFeeTx{msgs: []sdk.Msg{grant}}, false, noopAnteNext)
require.Error(t, err)
require.ErrorIs(t, err, sdkerrors.ErrUnauthorized)
})
}

t.Run("grant for an allowed msg type passes", func(t *testing.T) {
grant, err := authz.NewMsgGrant(from, to,
authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktypes.MsgSend{})), &future)
require.NoError(t, err)

_, err = decorator.AnteHandle(sdk.Context{}, mockFeeTx{msgs: []sdk.Msg{grant}}, false, noopAnteNext)
require.NoError(t, err)
})
}

func noopAnteNext(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) {
return ctx, nil
}
Loading
Loading