Root Cause
The vulnerability lived in the x/evm staking precompile shared by all chains running the Cosmos Labs EVM module. The exploit chained at least two bugs:
- Underflow in post-delegation balance write-back — after delegating an amount exceeding the account's spendable balance, the EVM-side mirror balance was decremented without an underflow guard, wrapping to approximately 2^256.
- Missing overflow guard on the EVM value-transfer credit path — a second defect (confirmed by Cosmos Labs' emergency patch) allowed the attacker to translate the phantom balance into real token transfers.
// Simplified pseudocode of the vulnerable write-back pattern in the staking precompile:
func (p *StakingPrecompile) Delegate(ctx sdk.Context, evm *vm.EVM, caller common.Address, validator string, amount *big.Int) error {
spendable := p.bankKeeper.SpendableCoins(ctx, callerAcc)
// BUG: no underflow check — if amount > spendable, uint256 wraps on subtraction
p.setEVMBalance(ctx, caller, p.getEVMBalance(ctx, caller).Sub(amount)) // underflow
return p.stakingKeeper.Delegate(ctx, callerAcc, validator, amount)
}
The attacker set up conditions by pre-computing the address a contract would deploy to (via CREATE/CREATE2), converting that address into a Cosmos vesting account before deploying, then deploying the exploit contract to that address. The contract inherited vesting status (spendable balance ≈ 0, total balance > 0), creating maximum underflow headroom when delegating.
Attack Steps
| Step | Action |
|---|---|
| 1 | Compute the address the exploit contract will deploy to (via CREATE/CREATE2) |
| 2 | Convert target address to a Cosmos vesting account (spendable ≈ 0, total > 0) |
| 3 | Deploy exploit contract to that pre-computed address — inherits vesting status |
| 4 | Call delegate(validator, spendable_balance + 1 wei) via staking precompile |
| 5 | EVM-side balance write-back underflows → wraps to ~2^256 |
| 6 | Second bug (missing overflow guard on credit path) allows converting phantom balance to real tokens |
| 7 | Drain victim accounts (no new token supply created — funds redirected from existing holders) |
| 8 | Repeat across 18 separate wallet targets (KiiChain alone) |
| 9 | Bridge stolen tokens to BNB Chain via Hyperlane; sell on DEXes |
Impact
- Chains affected: MANTRA (~$3.6M, first target
Aug 20), TAC ($7.5M,Aug 22), KiiChain ($9.7M nominal, ~Aug 22), plus at least three additional Cosmos EVM chains - KiiChain losses: 148,326,583.15 KII drained; ~80.7M KII frozen when validators halted at block 9,355,723; ~64.6M KII bridged to BNB Chain and sold; ~3M KII sent to KuCoin
- Cosmos Labs response: Emergency halt advisory issued to all chains running the shared
cosmos/evmmodule; emergency patch released - No new token supply was created — the attack redirected tokens from real holders
Lessons for Auditors
- Precompile arithmetic in Go/Rust needs explicit bounds checks — Solidity 0.8+ reverts on overflow, but precompiles written in Go or Rust bypass this; every arithmetic operation on balances in native precompile code needs an explicit guard.
- Vesting account × EVM module interaction is a high-risk surface — the dual-accounting split between Cosmos-side vesting constraints and EVM-side balance mirrors is hard to keep consistent; every code path that modifies one side must atomically update the other.
- Shared modules are shared attack surfaces — a bug in a shared module immediately affects every downstream chain; downstream chains must run their own security reviews rather than relying on upstream audits.
- Test
delegate(amount > spendable)on vesting accounts explicitly — this edge case should be a mandatory test vector in any EVM module test suite. - Cross-chain bridge monitoring is critical for early detection — large anomalous outflows via Hyperlane/IBC should trigger on-chain alerts before significant funds exit.