Skip to content

fix(security): port cosmos/evm statedb balance hardening to the v36 line - #31

Open
morde08 wants to merge 4 commits into
release/v35from
sec/v36-evm-balance-fixes
Open

fix(security): port cosmos/evm statedb balance hardening to the v36 line#31
morde08 wants to merge 4 commits into
release/v35from
sec/v36-evm-balance-fixes

Conversation

@morde08

@morde08 morde08 commented Aug 21, 2026

Copy link
Copy Markdown

Summary

Ports the three cosmos/evm balance-hardening fixes onto the branch that ZetaChain v36 (current mainnet) pins, plus the adaptations needed to make them correct and non-breaking on this fork.

Base is release/v35, whose tip f0addce1 is exactly what node's release/v36 pins in go.mod. (The v37 line pins release/v37@515bbca4 — a companion PR will be needed there.)

Context — we are not exposed to the live exploit

The exploit is now public: pushchain/push-chain-evm#40 (Hacken F-2026-18201), and MANTRA confirmed the staking precompile as the vector.

The real trigger is a vesting account delegating locked coins through the staking precompile: spendable S < delegated D, the balance handler mirrors coin_spent(D), SubBalance wraps to ~2^256, and SetBalance reconciliation mints (or drains a victim).

ZetaChain blocks that precondition already. app/ante/vesting.go rejects MsgCreateVestingAccount, MsgCreatePermanentLockedAccount and MsgCreatePeriodicVestingAccount with ErrUnauthorized, wired into all three ante chains (app/ante/handler_options.go:43,74,103), present in release/v36, and added in cosmos#923 on 2023-08-11 — before mainnet launch, for an unrelated reason (contract address bricking). In cosmos-sdk, vesting accounts are the only source of non-zero LockedCoins, and we add no custom account types. That is the same mitigation Push is adding in their companion PR cosmos#315.

Two secondary reasons the other halves are inert for us:

  • ParseAmount denom handlingcmd/zetacored/root.go:175-179 sets Denom == ExtendedDenom at Decimals: 18, the 18-decimal conversion factor is 1e0 (identity), and precisebank is not wired into app/app.go. This is the half that hit MANTRA (6-decimal OM, factor 1e12).
  • Module-account mintblockedReceivingModAcc (app/app.go:186-194) already blocks the bonded/not-bonded pools, distribution, gov, evm and feemarket from receiving.

So this is defense-in-depth, not an emergency. It should ride the normal release train rather than force an out-of-band upgrade. Note that the safety argument rests on the ante-handler block — if vesting-account creation is ever re-enabled, or a locked-coin account type is added, we become exposed and this patch becomes load-bearing.

What's included

Upstream Fix Applied as
#1190 (backport of cosmos#1187) locked-balance snapshot on statedb account cherry-pick
3524ebc module-account guard cherry-pick, narrowed (see below)
#1253 (backport of cosmos#1176) SubBalance underflow panic + denom-aware ParseAmount cherry-pick

Our fork descends from v1.0.0-rc2 (June 2025); v0.6.2/v0.7.2 are Aug 2026 tags on divergent maintenance branches, so there is no clean upgrade path — these had to be ported. Unrelated hunks bundled into the upstream PRs (README.md, tests/solidity dependency bumps) are deliberately not taken.

Three adaptations (final commit)

1. The ParseAmount fix landed on dead code. This fork carries duplicate unexported parseAmount/parseHexAddress in balance_handler.go, while cosmos#1253 patches only the exported ParseAmount in utils.go. The live path was left unpatched by the cherry-pick — a silent no-op. Deduplicated onto the patched exported helpers, converging with upstream.

2. utils.go would not compile. The patched ParseAmount body uses big.Int, but the import hunk did not apply against our import block. Added "math/big".

3. The module-account guard had to be narrowed — please review this closely. Upstream rejects module accounts unconditionally at the top of SetBalanceWithLocked, before computing the delta. That breaks any EVM call made from a module account, and x/fungible issues those pervasively — ZRC20 deploys, gas-pool swaps, system-contract calls (x/fungible/keeper/evm.go:96,205,258,289,313,337). Each makes the module account dirty in the statedb and reaches SetBalance with delta == 0, so the unconditional guard would brick the omnichain core. This was caught by TestCallEVMWithData/Case_deploy failing only with the patch applied.

The guard now fires only when delta != 0. A zero delta mints and burns nothing, so the security property is identical — no mint or burn can be reconciled into a module account — while module-initiated EVM calls keep working. The divergence is commented at the call site, and TestSetBalanceRejectsModuleAccounts gains an explicit mint-direction case (the exploit direction), which upstream did not cover.

Verification

go build ./...                                    # clean
gofmt                                             # clean
go vet -tags=test ./x/vm/... ./precompiles/... ./x/precisebank/...   # clean
go test -tags=test ./x/vm/... ./precompiles/... ./x/precisebank/... ./x/erc20/...   # 16 packages ok
go test -tags=test ./evmd/tests/integration/precompiles/staking/... # ok

New upstream security tests all pass, including TestSetBalanceRejectsModuleAccounts (all four arms), TestSetBalanceWithLocked, TestGetAccountLocked and TestSubBalanceUnderflowPanics.

Pre-existing failures, unchanged: TestCallEVMWithData/{Case_pass_with_empty_data,Case_pass_with_unknown_method} fail identically on the unpatched base commit f0addce1 — verified by running the full TestKeeperTestSuite on both and diffing the failure sets, which are identical. go vet ./... also reports pre-existing unreachable code in generated *.pulsar.go.

Follow-ups

  • Companion PR onto release/v37 for the v37 line (515bbca4).
  • Bump the cosmos/evm replace directive in node's go.mod once this merges — the patch does nothing until the pin moves.
  • Worth raising the module-account guard's incompatibility with module-initiated EVM calls upstream; it likely affects other chains that call the EVM from a module account.

🤖 Generated with Claude Code

mergify Bot and others added 4 commits August 21, 2026 07:15
…smos#1187) (cosmos#1190)

* fix(statedb): snapshot locked balance on statedb account  (cosmos#1187)

* snapshot locked balance on statedb account creation to calculate spendable + locked bank balance after locked balance changes via precompile

* lint formatting

* modify x/erc20 registering code hash to not silently drop existing accounts locked balacne

* update comments

* Update x/vm/keeper/statedb.go

Co-authored-by: Vlad J <vladjdk@gmail.com>

---------

Co-authored-by: Vlad J <vladjdk@gmail.com>
(cherry picked from commit 008c171)

* fix conflicts

* bump sol

---------

Co-authored-by: mattac21 <matt@cosmoslabs.io>
Co-authored-by: Vlad <vladjdk@gmail.com>
(cherry picked from commit ee96860)
* fix

* tests

---------

Co-authored-by: Eric Warehime <eric.warehime@gmail.com>
(cherry picked from commit 3524ebc)
…s#1176) (cosmos#1253)

* fix: harden statedb balance and event amount handling (cosmos#1176)

* fix: harden statedb balance and event amount handling

Guard StateDB balance subtraction against underflow and make precompile balance-event parsing denom-aware for base vs extended denom paths. Also add regression tests and document that only 18-decimal EVM gas-token chains are supported.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(vm): enforce 18-decimal coin configuration

Reject non-18 decimal EVM coin configs in both runtime and test configurators, align affected tests, and temporarily exclude precisebank packages from root unit-test targets until precisebank removal lands.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: remove obsolete precisebank test package filters

Now that contrib/x/precisebank is removed on main, package selection no longer needs explicit exclusions and can rely on the standard simulation/e2e filters.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: fix formatter ordering in scaling tests

Apply golangci formatter output for scaling tests so gci/gofumpt checks pass in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: align integration suites with 18-decimal-only config

Remove non-18-decimal integration cases and fee checks that now fail by design under enforced 18-decimal EVM coin configuration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 264aa70)

# Conflicts:
#	README.md
#	x/vm/statedb/state_object.go
#	x/vm/types/denom_config.go
#	x/vm/wrappers/feemarket_test.go

* fix conflicts

* fix formatting

* remove the requirement that the EVM coin be configured with 18 decimals

* sum the base and extended denom amounts when parsing balance change events

---------

Co-authored-by: Alex | Cosmos Labs <alex@cosmoslabs.io>
Co-authored-by: Matt Acciai <matt@cosmoslabs.io>
(cherry picked from commit 82b3ef6)
Three follow-ups needed to make the cherry-picked upstream fixes correct
and non-breaking on this fork:

1. precompiles/common: the fork carried duplicate unexported copies of
   parseAmount/parseHexAddress in balance_handler.go, and cosmos#1253 patched
   only the exported ParseAmount in utils.go. The live path therefore
   stayed unpatched. Deduplicated onto the patched exported helpers.

2. precompiles/common/utils.go: the patched ParseAmount body uses
   big.Int but the import hunk did not apply; added "math/big".

3. x/vm/keeper: narrow the module-account guard to fire only on a
   non-zero delta. Upstream rejects module accounts unconditionally,
   which breaks any EVM call made from a module account -- ZetaChain's
   x/fungible issues those for ZRC20 deploys, gas-pool swaps and system
   contract calls, and they reach SetBalance with delta == 0. A zero
   delta mints and burns nothing, so the security property is unchanged.
   Covers the mint direction with a new test case.

Verified: TestCallEVMWithData failures are identical to the unpatched
base commit (pre-existing); all new upstream security tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants