fix(evm): stop mirroring wasm bank events into EVM StateDB (wasm precompile accounting defect) - #220
Closed
mateeullahmalik wants to merge 1 commit into
Closed
mateeullahmalik wants to merge 1 commit into
mateeullahmalik wants to merge 1 commit into
Conversation
…ompile) The wasm precompile installed the upstream BalanceHandler, which replays native x/bank balance events into the EVM StateDB. For events emitted by a CosmWasm contract this is doubly broken: 1. common.BytesToAddress silently truncates the 32-byte wasm contract address. SubBalance on the zero-balance truncated alias wraps uint256 (x/vm/statedb has no insufficient-balance guard), and the commit-time SetBalance reconciliation mints ~2^256 extended-denom units into the alias account. 2. The matching coin_received replay credits the recipient in the StateDB journal on top of the native bank credit; reconciliation mints the amount a second time (observed on local chain: fixture sends 111 ulume, recipient gets 222, plus a giant alias mint and supply inflation). Filtering only non-20-byte addresses is insufficient (case 2 persists), so the precompile now installs no balance handler: wasm bank events are native movements owned by x/bank and are never mirrored. Regression tests cover the production wiring helper, the alias wrap, the recipient double-apply, the defect demonstration of the pre-fix wiring, and the legitimate EVM handler path used by the other (unchanged) precompiles. Consensus-affecting fix: chains that already executed the vulnerable path need a coordinated upgrade; previously minted balances are not healed by this change.
mateeullahmalik
force-pushed
the
zee/wasm-evm-accounting-fix
branch
from
September 13, 2026 23:54
e28407d to
5d40441
Compare
Contributor
Author
|
Closing to handle through private security process. |
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.
Fix: EVM→Wasm precompile accounting defect (unbounded mint)
Behavior change
The wasm precompile (
0x0000000000000000000000000000000000000903) no longer installs the upstreamBalanceHandlerFactory. Bank balance events emitted by a CosmWasm contract are native x/bank movements of the contract's own funds and are never mirrored into the EVM StateDB. No other precompile is touched; the ABI, execute/query semantics, sender identity, reentrancy guard, and gas metering are unchanged.Root cause
precompiles/wasm/wasm.gowiredcmn.NewBalanceHandlerFactory(bankKeeper), so every wasm execute replayed the native bank events into the EVM StateDB (cosmos/evm v0.6.0 precompiles/common/balance_handler.go):common.BytesToAddress, silently truncating the 32-byte wasm contract address.stateDB.SubBalanceon the zero-balance truncated alias wraps (x/vm/statedb/state_object.go:121has no insufficient-balance guard). At commit,keeper.SetBalancereconciles the wrapped target and callsMintAmountToAccount— minting2^256 - amountextended units into the alias account.coin_receivedreplay credits the recipient in the StateDB journal on top of the native bank credit.SetBalancereconciles the journal target against a bank view that does not include the in-flight native transfer and mints the amount a second time.Observed end state on a real signed local chain (three validators, unmodified binary): fixture sends 111 ulume from the wasm contract → contract −111, recipient +222, truncated alias +115792089237316195423570985008687907853269984665640564039457583896 ulume (integer part of
(2^256−111e12)/1e12, fractional913129639936), with matchingcoinbase/mint events in the committed tx results and total-supply inflation.Filtering only non-20-byte addresses was evaluated and is insufficient: case 2 persists for 20-byte recipients (proven RED by the regression test). The correct minimal fix is to not mirror at all: 20-byte EVM account events can never be produced by a wasm execute through this precompile (non-payable; submessages dispatch from the 32-byte contract address), so no legitimate behavior is lost.
Regression tests (RED→GREEN)
precompiles/wasm/balance_test.go— in-process, no devnet:TestWasmBalanceReplayDoesNotMintOrDoubleApply— builds the replay through the production wiring helper; asserts no handler is installed, the truncated alias stays zero, and the recipient is not double-credited. A defect-demonstration subtest pins the vulnerable pre-fix wiring (alias wraps to ~2^256, recipient double-credited) so the assertions cannot pass vacuously. RED on unfixed code (verified by reverting the wiring:does_not_mutate_the_truncated_alias...FAILS with the giant wrapped balance).TestWasmBalanceReplayKeepsEVMAccountBehavior— guards the legitimate upstream handler path still used by the other (unchanged) precompiles.Verified:
go test ./precompiles/wasm/ ./app/evm/— PASSgo test -tags='integration test' ./tests/integration/evm/precompiles/ -run 'TestPrecompilesSuite/WasmPrecompile'— 13/13 PASS (live node, 76s)golangci-lint run precompiles/wasm/...— 0 issuesgo build ./...— PASS;gofmtcleango test ./app/...—app/evm,app/openrpc,app/upgrades/*PASS;apppackage has a pre-existingTestEVMMempoolDisabledWhenMaxTxsIsNegativepanic (chainConfig double-set) that reproduces identically on the unmodified base commit (verified viagit stash).Risks
Rollback
Revert this single commit; restore the factory wiring. No state migration is required for the revert itself (the mirroring is a per-tx computation), but any chain that executed the fixed code and then reverts would re-introduce the defect — so rollback should only ever mean "upgrade handler not yet activated."
Migration
No store/key migration. The change is purely behavioral (per-transaction computation). Upgrade handler / coordinated halt is required (out of scope for this PR; flagged for release planning).
Affected versions / commits
The vulnerable wiring was introduced with the Cosmos EVM integration (#108, commit f3083eb) and is present unchanged in
v1.20.0,v1.20.1,v1.20.1-hotfix,v1.20.2,v1.20.3and current master (3debe2dd; zero diff inprecompiles/,app/evm.go,app/evm/precompiles.go,app/wasm_evm_plugin.gobetween v1.20.3 and this base). Mainnet (lumera-mainnet-1) currently runs v1.12.0 (verified read-only via/abci_infoand/app/versionat height ~6.9M), which predates PR #108 and does not contain the EVM stack — the vulnerable path is NOT present on current mainnet. It is present on every v1.20.x release candidate/release line and any testnet/devnet running them.Observability
No new metrics. The unit regression tests are the primary guard; the defect-demonstration subtest fails if the vulnerable wiring is ever restored.
Evidence archive (local chain reproduction)
work/circuit-closure-20260911/final-pass/runtime/evidence/(committed tx results, events, balances, supply) andwork/circuit-closure-20260911/goal1-evm-wasm-accounting/DOUBLE-APPLY-ANALYSIS.md(full mechanism trace).