Skip to content

fix: F-2026-18201 | [Dual Defense] Staking Precompile and Vesting Underflow StateDB Balance Enabling Native Mint and Drain - #315

Open
0xNilesh wants to merge 1 commit into
audit-fixesfrom
F-2026-18201
Open

fix: F-2026-18201 | [Dual Defense] Staking Precompile and Vesting Underflow StateDB Balance Enabling Native Mint and Drain#315
0xNilesh wants to merge 1 commit into
audit-fixesfrom
F-2026-18201

Conversation

@0xNilesh

Copy link
Copy Markdown
Member

Problem

The staking-precompile underflow attack (F-2026-18201) needs one precondition: a vesting account. The EVM state view tracks only spendable balance, while Cosmos lets a vesting account delegate locked coins. Delegating more than the spendable balance makes the StateDB subtract more than it holds; x/vm/keeper/statedb.go then reconciles that bogus view back into bank by minting the difference (or burning a victim's real coins on the wrap-transfer variant).

On this chain that precondition was permissionless. app/ante/ante_cosmos.go wired NewAuthzLimiterDecorator with MsgCreateVestingAccount, but that decorator only blocks a msg type when it appears inside an authz.MsgExec — a plain top-level tx went straight through.

Worse, and not mentioned in Hacken's report: MsgCreatePermanentLockedAccount and MsgCreatePeriodicVestingAccount were not blocked anywhere at all. Permanent-locked is the stronger primitive — 100% locked, fully delegatable.

This was verified empirically, not assumed: with the new decorator removed, the added end-to-end test shows all three msgs succeed at the top level on audit-fixes today.

Fix

New BlockedMsgsDecorator (app/ante/blocked_msgs.go), wired into NewCosmosAnteHandler for all three vesting msg types:

  • /cosmos.vesting.v1beta1.MsgCreateVestingAccount
  • /cosmos.vesting.v1beta1.MsgCreatePermanentLockedAccount
  • /cosmos.vesting.v1beta1.MsgCreatePeriodicVestingAccount

It rejects them wherever they appear:

  • at the top level of a tx (the gap this PR closes);
  • nested inside authz.MsgExec, recursively, with the same nesting cap the authz limiter uses;
  • as the authorization of an authz.MsgGrant, so the block cannot be sidestepped by pre-authorizing a grantee.

It sits before SetUpContextDecorator, so a blocked tx fails before gas metering and fee deduction — no state moves.

Provenance

This is the chain-side half of a dual defense. The root cause is upstream cosmos/evm: stateObject.SubBalance subtracts without checking the balance first, and uint256 is unsigned, so it wraps. Upstream fixed it on main in 264aa70 ("fix: harden statedb balance and event amount handling", #1176, 2026-05-15).

That fix is in no tagged release — v0.6.x, v0.7.x and v1.0.0-rc0/rc1/rc2 all lack it — so upgrading does not help; it has to be cherry-picked. That is the companion PR: pushchain/push-chain-evm#40.

Follow-up: once #40 merges, the github.com/cosmos/evm pin in go.mod (currently v1.0.0-rc2.0.20260616081105-96231e7a76c0, which predates the guard) must be bumped so this chain actually picks up the StateDB guard. This PR removes the precondition; #40 removes the vulnerability.

Tests

app/ante/blocked_msgs_test.go — unit tests on the decorator (13 subtests):

  • each of the three vesting msg types rejected at top level;
  • allowed msgs still pass;
  • blocked msg alongside allowed msgs, inside authz.MsgExec, and inside deeply nested MsgExec;
  • authz.MsgGrant for each of the three rejected; grant for an allowed type passes;
  • over-deep nesting rejected;
  • blocked txs never reach the next decorator.

test/integration/ante/vesting_blocked_test.go — end-to-end through baseapp: builds a real chain app (SetupWithGenesisValSet, so InitChain and real EVM genesis run), signs a real tx with a funded account, and delivers it via FinalizeBlock. For each of the three msg types it asserts:

  • the tx is rejected, specifically by the blocked-msgs decorator (found blocked msg type) — not incidentally by a fee or gas check; the sender is funded with enough upc and pays a real fee well above the dynamic min gas price, so without the decorator the tx would succeed;
  • no vesting account is created at the target address;
  • the victim's spendable balance is unchanged;
  • the sender's spendable balance is unchanged;
  • total native supply is unchanged across the tx (block inflation is zeroed in the mint module first, so the only thing that could move supply is the tx itself).

Verified both directions — with the decorator removed, the end-to-end test fails with vesting account creation must be rejected in ante, got success for all three msg types.

go test -tags='ledger test_ledger_mock test' ./app/... ./test/integration/ante/...

app, app/ante, app/decorators, app/upgrades/purge-expired-outbounds, test/integration/anteall ok, 0 failures.

Not covered here

The "delegate more than spendable through the staking precompile must fail, not mint" case is tested in the EVM repo (pushchain/push-chain-evm#40), where the guard lives. It cannot be tested from this repo yet: the go.mod pin predates the guard, and this repo has no staking-precompile test harness (no test calls a precompile, and the shared setup leaves ActiveStaticPrecompiles empty so precompile dispatch would not even happen).

…201)

Rejects all three cosmos vesting MsgCreate* types at the top level, not just inside authz.MsgExec.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant