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
Open
Conversation
…201) Rejects all three cosmos vesting MsgCreate* types at the top level, not just inside authz.MsgExec.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.gothen 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.gowiredNewAuthzLimiterDecoratorwithMsgCreateVestingAccount, but that decorator only blocks a msg type when it appears inside anauthz.MsgExec— a plain top-level tx went straight through.Worse, and not mentioned in Hacken's report:
MsgCreatePermanentLockedAccountandMsgCreatePeriodicVestingAccountwere 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-fixestoday.Fix
New
BlockedMsgsDecorator(app/ante/blocked_msgs.go), wired intoNewCosmosAnteHandlerfor all three vesting msg types:/cosmos.vesting.v1beta1.MsgCreateVestingAccount/cosmos.vesting.v1beta1.MsgCreatePermanentLockedAccount/cosmos.vesting.v1beta1.MsgCreatePeriodicVestingAccountIt rejects them wherever they appear:
authz.MsgExec, recursively, with the same nesting cap the authz limiter uses;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.SubBalancesubtracts without checking the balance first, anduint256is unsigned, so it wraps. Upstream fixed it onmainin264aa70("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/evmpin ingo.mod(currentlyv1.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):authz.MsgExec, and inside deeply nestedMsgExec;authz.MsgGrantfor each of the three rejected; grant for an allowed type passes;test/integration/ante/vesting_blocked_test.go— end-to-end through baseapp: builds a real chain app (SetupWithGenesisValSet, soInitChainand real EVM genesis run), signs a real tx with a funded account, and delivers it viaFinalizeBlock. For each of the three msg types it asserts:found blocked msg type) — not incidentally by a fee or gas check; the sender is funded with enoughupcand pays a real fee well above the dynamic min gas price, so without the decorator the tx would succeed;Verified both directions — with the decorator removed, the end-to-end test fails with
vesting account creation must be rejected in ante, got successfor all three msg types.→
app,app/ante,app/decorators,app/upgrades/purge-expired-outbounds,test/integration/ante— all 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.modpin predates the guard, and this repo has no staking-precompile test harness (no test calls a precompile, and the shared setup leavesActiveStaticPrecompilesempty so precompile dispatch would not even happen).