Skip to content

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

Merged
0xNilesh merged 2 commits into
audit-fixesfrom
F-2026-18201
Aug 24, 2026
Merged

fix: F-2026-18201 | [Dual Defense] Staking Precompile and Vesting Underflow StateDB Balance Enabling Native Mint and Drain#40
0xNilesh merged 2 commits into
audit-fixesfrom
F-2026-18201

Conversation

@0xNilesh

@0xNilesh 0xNilesh commented Aug 20, 2026

Copy link
Copy Markdown
Member

Problem

stateObject.SubBalance subtracts without checking the balance first:

return s.SetBalance(new(uint256.Int).Sub(s.Balance(), amount))

uint256 is unsigned, so subtracting more than the account holds wraps to ~2^256 instead of erroring.

That is reachable from the precompile balance handler. precompiles/common/balance_handler.go translates bank coin_spent events into stateDB.SubBalance(...) with no balance check (only a BlockedAddr bypass). The EVM state view tracks spendable balance, but Cosmos lets vesting accounts delegate locked coins — so delegating D > spendable S through the staking precompile makes the StateDB subtract more than it holds.

x/vm/keeper/statedb.go then reconciles the bogus EVM view back into bank: SetBalance computes delta = amount - balance and mints if positive / burns if negative. Payoffs:

  • Mint — move a finite amount out of the wrapped account and destroy the helper so the full wrap never materialises.
  • Drain — send a victim 2^256 - B so their balance wraps to ~0 and reconciliation burns their real coins.

Reported by Hacken as F-2026-18201 (High). Same family as Evmos GHSA-pxv8-qhrh-jc7v and cosmos/evm ASA-2026-002.

Fix

Cherry-picks the security-relevant part of upstream cosmos/evm 264aa70"fix: harden statedb balance and event amount handling (cosmos#1176)", 2026-05-15:

  • x/vm/statedb/state_object.goSubBalance now panics on underflow instead of wrapping. The panic is recovered by baseapp, the tx fails, and no mint/burn happens.
  • precompiles/common/utils.goParseAmount is denom-aware (base vs extended denom) instead of always running the base amount through the 18-decimal conversion. This is the other half of the same upstream commit. It is a no-op for Push (app/config.go sets ExtendedDenom == BaseDenom at 18 decimals and precisebank is not wired), but keeps us aligned with upstream.

A straight git cherry-pick 264aa70 does not apply — upstream bundled an unrelated "enforce 18-decimal coin configuration" change into the same commit, which conflicts with the fork in README.md, x/vm/types/denom_config.go and x/vm/wrappers/feemarket_test.go. Those hunks are deliberately not taken; only the balance/event hardening and its tests are.

Provenance

Not Push-authored — git blame on the affected files shows only upstream authors. The fix exists only on upstream main; it is in no tagged release:

Version Date Has fix
v0.6.0 2026-03-02 NO
v0.7.0 2026-05-05 NO
v0.6.1 / v0.7.1 2026-07-27 NO
v1.0.0-rc0 / rc1 / rc2 2025-05-28 / 06-13 / 06-13 NO
main current YES

So upgrading to a released tag does not help — the patch has to be cherry-picked. Our fork is pinned at v1.0.0-rc2.0.20260803..., descended from a June 2025 upstream point.

Tests

  • x/vm/statedb/statedb_test.goTestSubBalanceUnderflowPanics (from the upstream commit): subtracting 2 from a balance of 1 panics with state balance underflow for <addr>: have=1 sub=2 instead of wrapping.
  • precompiles/common/balance_handler_test.goTestAfterBalanceChangeSpendMoreThanBalancePanics (added here): reproduces the attack at the precompile boundary. A spender with a StateDB balance of 5 emits a bank coin_spent event for 10 — exactly what delegating locked coins through the staking precompile produces. BalanceHandler.AfterBalanceChange must hard-fail, and the balance must not have wrapped to ~2^256.
  • precompiles/common/balance_handler_test.goTestParseAmount/unrelated_denom_is_ignored (from the upstream commit).

Both new tests were verified to fail with the guard reverted and pass with it in place.

go test -tags=test ./x/vm/... ./precompiles/...

12 packages ok, 0 failures.

Related

Chain-side companion PR: pushchain/push-chain-node#315 — blocks permissionless vesting-account creation in the ante handler, removing the precondition. The two are a dual defense: cosmos#315 removes the precondition, this PR removes the vulnerability.

After this PR merges, the github.com/cosmos/evm pin in push-chain-node/go.mod (currently v1.0.0-rc2.0.20260616081105-96231e7a76c0, which predates the guard) needs bumping so the chain actually picks it up.

Parent finding: F-2026-17758 — the fork has ~14 months of upstream drift; if this security commit was missed, others were too.

Cherry-picks the security-relevant part of upstream cosmos/evm 264aa70 (cosmos#1176).
@0xNilesh
0xNilesh merged commit a220b4f into audit-fixes Aug 24, 2026
12 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant