## Cosmos EVM Vulnerability Patterns

> Vulnerability patterns specific to Cosmos EVM chains (evmOS, Ethermint, Initia, Berachain, Sei, etc.). These chains run two execution runtimes (EVM + Cosmos SDK) with independent state that must stay in sync. This is the most frequently discovered vulnerability class in Cosmos EVM chains — 7+ findings across multiple chains, $7M+ in real losses. Referenced by [SKILL.md](../SKILL.md).

### 1 EVM REVERT DOES NOT ROLLBACK COSMOS STATE

**Description**: Precompiles bridge EVM and Cosmos state machines. When a Solidity `revert`, `try/catch`, or out-of-gas (OOG) occurs, EVM state rolls back automatically, but Cosmos keeper writes made during the precompile call persist. An attacker wraps a precompile call in `try/catch`, triggers a revert after the Cosmos-side effect executes, and gets the EVM-side effect undone while keeping the Cosmos-side effect. Every Cosmos EVM chain is vulnerable until it implements snapshot-based atomic precompile wrappers.

**Detection Patterns**:
```solidity
// VULNERABLE: Solidity try/catch around precompile call
// EVM state rolls back on revert, but Cosmos delegation persists
contract InfiniteDelegate {
    IStaking constant STAKING = IStaking(0x...1003);

    function exploit(address validator, uint256 amount) external {
        try this._delegate(validator, amount) {} catch {}
        // EVM: tokens refunded (revert undid the transfer)
        // Cosmos: delegation still exists (keeper write persisted)
        // Net effect: delegation increased without balance decreasing
    }

    function _delegate(address validator, uint256 amount) external {
        STAKING.delegate(validator, amount);
        revert();  // EVM state rolls back, Cosmos state does NOT
    }
}
```

**What to Check**:
- [ ] ALL precompile `Run()` methods wrapped in atomic snapshot/revert (no partial execution on OOG or error)
- [ ] EVM `revert` / `try/catch` / OOG propagates rollback to Cosmos keeper state — test with Solidity `try/catch` around every precompile call
- [ ] IBC operations triggered via precompiles are atomic with the EVM transaction

**References**:
- [EVM staking precompile infinite delegation](https://forum.cosmos.network/t/critical-evm-precompiled-contract-bug-allowing-unlimited-token-mint/14060) (Finding 7)
- [Saga $7M exploit](https://advisories.gitlab.com/pkg/golang/github.com/cosmos/evm/GHSA-54gx-3cgr-7mfm/) (Finding 5)

---

### 2 STATEDB.COMMIT() DURING PRECOMPILE EXECUTION

**Description**: Certain precompile calls (e.g., `delegationTotalRewards`) trigger `StateDB.Commit()`, writing state to permanent storage mid-execution. A subsequent `revert` only undoes cached in-memory state — permanent storage retains the committed state. This creates orphaned contracts with withdrawable balances. Exponential multiplication is possible: 1 ETH becomes 1,048,576 ETH in 20 loops.

**Detection Patterns**:
```go
// VULNERABLE: Precompile triggers StateDB.Commit() (Finding 6)
// Calling certain precompiles caused StateDB.Commit() to write
// state to permanent storage. A subsequent revert only undoes
// cached in-memory state, but permanent storage retains the
// committed state.
// Result: Orphaned contracts with balances are accessible and withdrawable.
```

**What to Check**:
- [ ] `StateDB.Commit()` is NEVER called during precompile execution (only at transaction finalization)
- [ ] Precompile code paths audited for any indirect `Commit()` triggers (e.g., via keeper calls that flush caches)
- [ ] StateDB journal entries cover all state modifications made by precompiles

**Grep Patterns** (use Grep tool with `glob: "*.go"` in `x/`):
- `StateDB|stateDB` — then check results for `commit` context
- `func.*Run\(.*vm\.EVM` — precompile Run methods

**References**:
- [Evmos infinite mint](https://blog.asymmetric.re/evmos-precompile-state-commit-infinite-mint/) (Finding 6)

---

### 3 PARTIAL PRECOMPILE EXECUTION (OOG)

**Description**: A precompile `Run()` performs multiple Cosmos state changes sequentially (e.g., transfer rewards then clear claimable counter). If execution runs out of gas between steps, early state changes persist while later ones are skipped. The attacker receives rewards but the claimable counter is never reset — repeat to drain the module.

**Detection Patterns**:
```go
// VULNERABLE: Precompile Run() not atomic (Finding 20)
func (p DistributionPrecompile) Run(evm *vm.EVM, input []byte) ([]byte, error) {
    // Step 1: Transfer rewards to user (succeeds)
    k.bankKeeper.SendCoins(ctx, moduleAcc, userAddr, rewards)
    // Step 2: Clear claimable rewards (runs out of gas here!)
    k.SetClaimableRewards(ctx, userAddr, sdk.Coins{})
    // User received rewards but claimable counter never reset
    // Repeat to drain distribution module
}
```

**What to Check**:
- [ ] Every precompile `Run()` is wrapped in an atomic execution wrapper (snapshot before, revert on any error including OOG)
- [ ] No multi-step state mutations without atomicity guarantees
- [ ] Gas estimation for precompile operations is accurate (underestimation enables OOG at attacker-chosen points)

**Mitigation**:
```go
// SECURE: Wrap in atomic execution
func (p DistributionPrecompile) Run(evm *vm.EVM, input []byte) ([]byte, error) {
    return p.RunAtomic(evm, input, func(ctx sdk.Context) ([]byte, error) {
        // All operations atomic — snapshot + revert on any error including OOG
    })
}
```

**References**:
- [Partial precompile state writes](https://github.com/advisories/GHSA-mjfq-3qr2-6g84) (Finding 20)

---

### 4 REVERTED EVM CALLS LEAVE COSMOS MESSAGES IN QUEUE

**Description**: When an EVM call dispatches a Cosmos message (e.g., IBC transfer) via a precompile, and the EVM call subsequently reverts, the dispatched Cosmos message remains in the execution queue. The Cosmos message executes despite the EVM revert, enabling IBC transfers (and other Cosmos-side effects) without corresponding EVM-side state changes.

**Detection Patterns**:
```go
// VULNERABLE: Initia — dispatched Cosmos messages remain in execution queue
// even when the EVM call that dispatched them reverts (Finding 26b)
// Solidity try/catch wraps a call that dispatches IBC transfer via precompile
// Revert undoes EVM state but IBC transfer message still in Cosmos execution queue
// Result: IBC transfers execute despite explicit reversion
```

**What to Check**:
- [ ] Cosmos message dispatch queue is cleared when EVM execution reverts
- [ ] All Cosmos messages dispatched from precompiles are tracked in the EVM journal and rolled back on revert
- [ ] Test: wrap every message-dispatching precompile call in Solidity `try/catch` with explicit `revert` — verify no Cosmos messages execute

**References**:
- [Initia Code4rena report](https://code4rena.com/reports/2025-02-initia-cosmos) (Finding 26b)

---

### 5 GAS LIMIT BYPASS VIA PRECOMPILE DISPATCH

**Description**: Solidity's `call{gas: X}()` restricts gas forwarded to an untrusted contract. But if that contract calls a precompile that dispatches a new Cosmos `MsgCall`, the new message gets the full parent transaction gas limit — not the restricted amount. This defeats gas-based reentrancy guards and any logic relying on gas limits to bound untrusted execution.

**Detection Patterns**:
```solidity
// VULNERABLE: Initia — COSMOS_CONTRACT precompile bypasses gas restrictions (Finding 26c)
// Solidity call{gas: X}(untrustedContract) restricts gas
// But untrusted contract calls COSMOS_CONTRACT precompile to dispatch new MsgCall
// New MsgCall gets full parent transaction gas limit, not the restricted amount
```

**What to Check**:
- [ ] Gas forwarding through precompile dispatch does not bypass caller-imposed gas limits
- [ ] Precompile-dispatched messages inherit the remaining gas of the calling frame, not the transaction-level gas limit
- [ ] Gas-based reentrancy guards are not the sole defense (combine with mutex/state guards)

**References**:
- [Initia Code4rena report](https://code4rena.com/reports/2025-02-initia-cosmos) (Finding 26c)

---

### 6 WRONG ROUTING IN MULTI-DENOM ERC20 OPERATIONS

**Description**: When processing multiple denominations in a loop, ERC20-related operations (burn, mint, community pool funding) accidentally use the full `amount` parameter (all denoms) instead of the individual `coin` being processed. If the loop contains both ERC20 and native denoms, the native denom amount is sent to the wrong destination on every iteration that hits the ERC20 path.

**Detection Patterns**:
```go
// VULNERABLE: Initia — BurnCoins sends entire amount to community pool (Finding 26a)
// When burning ERC20 denominated coins in a loop,
// the function sends the full `amount` parameter (all denoms)
// instead of just the individual `coin` being processed
func (k Keeper) BurnCoins(ctx sdk.Context, amount sdk.Coins) error {
    for _, coin := range amount {
        if k.isERC20(coin.Denom) {
            // WRONG: sends ALL coins, not just this one
            k.communityPoolKeeper.FundCommunityPool(ctx, amount, moduleAddr)
        }
    }
}
```

**What to Check**:
- [ ] Multi-denom operations in loops use the individual `coin`, not the full `amount` parameter
- [ ] ERC20/native denom branching logic tested with mixed-denom inputs
- [ ] Community pool / burn / mint amounts match the specific coin being processed

**Mitigation**:
```go
// SECURE: Use the individual coin in the loop
func (k Keeper) BurnCoins(ctx sdk.Context, amount sdk.Coins) error {
    for _, coin := range amount {
        if k.isERC20(coin.Denom) {
            k.communityPoolKeeper.FundCommunityPool(ctx, sdk.NewCoins(coin), moduleAddr)
        } else {
            k.bankKeeper.BurnCoins(ctx, moduleName, sdk.NewCoins(coin))
        }
    }
}
```

**References**:
- [Initia Code4rena report](https://code4rena.com/reports/2025-02-initia-cosmos) (Finding 26a)

---

### 7 ETHERMINT ANTE HANDLER BYPASS (NESTED MESSAGES)

**Description**: Cosmos EVM chains add custom AnteHandler decorators for EVM-specific validation (gas price floors, tx type restrictions, EVM-specific fee checks). These decorators typically only inspect the outer message. An attacker wraps a blocked EVM message inside `x/authz MsgExec` or an `x/group` proposal — the inner message skips EVM-specific ante validation entirely.

**Detection Patterns**:
```go
// VULNERABLE: AnteHandler only checks outer message type
func (d EthGasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
    for _, msg := range tx.GetMsgs() {
        // Only checks outer msg — does NOT recurse into MsgExec.Msgs!
        if ethMsg, ok := msg.(*evmtypes.MsgEthereumTx); ok {
            if err := d.validateGas(ethMsg); err != nil {
                return ctx, err
            }
        }
    }
    return next(ctx, tx, simulate)
}
```

**What to Check**:
- [ ] Custom EVM AnteHandlers recurse into `x/authz MsgExec` and `x/group` proposal inner messages
- [ ] EVM-specific fee/gas validation applies to nested EVM messages, not just top-level
- [ ] SDK `MaxUnpackAnyRecursionDepth = 10` is not sufficient — semantic nesting (authz wrapping authz) must be handled by the ante handler itself
- [ ] Integration tests submit EVM messages wrapped in `MsgExec` to verify they are validated

**Mitigation**:
```go
// SECURE: Recurse into nested messages
func (d EthGasDecorator) checkMsg(msg sdk.Msg) error {
    if ethMsg, ok := msg.(*evmtypes.MsgEthereumTx); ok {
        return d.validateGas(ethMsg)
    }
    if exec, ok := msg.(*authz.MsgExec); ok {
        for _, inner := range exec.Msgs {
            var innerMsg sdk.Msg
            if err := d.cdc.UnpackAny(inner, &innerMsg); err != nil {
                return err
            }
            if err := d.checkMsg(innerMsg); err != nil {
                return err
            }
        }
    }
    return nil
}
```

**References**:
- [Jump Crypto — Bypassing Ethermint Ante Handlers](https://jumpcrypto.com/bypassing-ethermint-ante-handlers/)

---

### 8 EVM HOOKS AND CROSS-MODULE INTERACTIONS

**Description**: Cosmos EVM chains use hooks (`PostTxProcessing`, `PreTxProcessing`) to synchronize EVM and Cosmos state after EVM transactions. The `x/evm` and `x/erc20` modules fire these hooks on every EVM transaction. Bugs in hook logic — missing error propagation, state assumptions, reentrancy via hook callbacks — break the cross-module state invariant.

**What to Check**:
- [ ] [EVM hooks](https://docs.evmos.org/protocol/modules/evm#hooks) from `x/evm` and `x/erc20` modules reviewed for correctness
- [ ] Hook errors propagate correctly (a failed hook must revert the entire EVM transaction)
- [ ] Hooks do not assume EVM state is finalized (it may still be in a cached context)
- [ ] Cross-module interactions between `x/evm`, `x/erc20`, `x/bank`, and `x/staking` do not create circular dependencies or reentrancy
- [ ] User interaction with [module special addresses](https://docs.evmos.org/protocol/module-accounts) reviewed — EVM contracts should not be able to interact with Cosmos module accounts in unexpected ways

---

### 9 ETHEREUM ADDRESS VALIDATION

**Description**: Cosmos EVM chains handle two address formats: Bech32 (`cosmos1...`) and hex (`0x...`). The `go-ethereum` function `common.HexToAddress()` does NOT validate format and silently ignores errors — it returns a zero-padded address for any invalid input. Address mapping between the two formats must be bijective and validated.

**Detection Patterns**:
```go
// VULNERABLE: HexToAddress does not verify format, ignores errors
addr := common.HexToAddress(userInput)
// "not_an_address" → 0x0000000000000000000000000000000000000000
// No error returned! Silently maps to zero address.

// VULNERABLE: Inconsistent address derivation
cosmosAddr := sdk.AccAddress(ethAddr.Bytes())  // Cosmos addr from ETH addr
ethAddr2 := common.BytesToAddress(cosmosAddr)   // Round-trip may not match
```

**What to Check**:
- [ ] NOT using `common.HexToAddress()` for user-supplied input — use a validating function instead
- [ ] Address conversion between Bech32 and hex is validated in both directions
- [ ] Zero address (`0x000...`) is explicitly rejected where it should not be valid
- [ ] EVM contract addresses derived from Cosmos module accounts are deterministic and documented

**Mitigation**:
```go
// SECURE: Validate hex address format before conversion
func ValidateHexAddress(addr string) (common.Address, error) {
    if !common.IsHexAddress(addr) {
        return common.Address{}, fmt.Errorf("invalid hex address: %s", addr)
    }
    return common.HexToAddress(addr), nil
}
```

---

### 10 CROSS-CHAIN TRANSACTION REPLAY AND CHAIN ID

**Description**: Cosmos EVM chains use an epoch-based chain ID format (`chainname_NNNN-1`) where the numeric part maps to the Ethereum chain ID used in EVM transaction signing. If the chain ID epoch is not correctly parsed or enforced, transactions signed for one chain can be replayed on another. Nonce management differs between EVM (per-account incrementing) and Cosmos (sequence numbers) — mismatches enable replay or stuck transactions.

**What to Check**:
- [ ] Chain ID epoch is correctly parsed and used as the EVM chain ID in `ethermint/types/chain_id.go`
- [ ] EVM transactions are validated against the current chain ID (not just any valid chain ID)
- [ ] Nonce management is synchronized between EVM and Cosmos account sequences
- [ ] Chain upgrades that change the chain ID epoch correctly invalidate pending EVM transactions
- [ ] `eth_chainId` RPC returns the correct value matching the Cosmos chain ID epoch

---

### SUMMARY: COSMOS EVM BUG CLASSES

| # | Pattern | Impact | Known Incident / Reference |
|---|---------|--------|---------------------------|
| 1 | EVM revert doesn't rollback Cosmos state | Fund theft (infinite delegation/mint) | Saga $7M, Finding 5/7 |
| 2 | StateDB.Commit() during precompile | Infinite mint (exponential) | Evmos, Finding 6 |
| 3 | Partial precompile execution (OOG) | Module drainage | GHSA-mjfq-3qr2-6g84, Finding 20 |
| 4 | Reverted EVM calls leave Cosmos msgs | Unauthorized IBC transfers | Initia, Finding 26b |
| 5 | Gas limit bypass via precompile dispatch | Reentrancy guard defeat | Initia, Finding 26c |
| 6 | Wrong routing in multi-denom ERC20 ops | Fund misrouting | Initia, Finding 26a |
| 7 | Ethermint ante handler bypass | Validation skip via nested msgs | Jump Crypto disclosure |
| 8 | EVM hooks cross-module interactions | State desync | Multiple chains |
| 9 | Ethereum address validation | Zero address injection | go-ethereum HexToAddress |
| 10 | Cross-chain tx replay / chain ID | Transaction replay | Chain ID epoch parsing |

---

### GREP PATTERNS (ALL PATTERNS)

Use the Grep tool with `glob: "*.go"` in `x/` for all searches below.

**Precompile implementations**:
- `func.*Run\(.*vm\.EVM` — precompile Run methods
- `precompile|Precompile` — precompile files

**StateDB usage**:
- `StateDB|stateDB` — then check results for `commit` context
- `stateDB\.(Snapshot|RevertToSnapshot)` — snapshot/revert usage

**EVM hooks**:
- `PostTxProcessing|PreTxProcessing|EvmHooks`

**ERC20 module**:
- `isERC20|IsERC20|erc20` — ERC20 detection
- `RegisterERC20|RegisterCoin|ToggleConversion` — token conversion

**Address handling**:
- `HexToAddress|IsHexAddress|BytesToAddress`

**Ante handlers**:
- `AnteHandle|AnteDecorator` — handler files
- `MsgEthereumTx|EthereumTx` — EVM message type

**Chain ID**:
- `ChainID|ParseChainID|EpochNumber`

**Solidity patterns** (use `glob: "*.sol"` in `contracts/`):
- `try|catch|revert` — revert/try-catch patterns

---

### References

- **Saga $7M exploit**: https://advisories.gitlab.com/pkg/golang/github.com/cosmos/evm/GHSA-54gx-3cgr-7mfm/
- **Evmos infinite mint**: https://blog.asymmetric.re/evmos-precompile-state-commit-infinite-mint/
- **EVM staking precompile**: https://forum.cosmos.network/t/critical-evm-precompiled-contract-bug-allowing-unlimited-token-mint/14060
- **Partial precompile state writes**: https://github.com/advisories/GHSA-mjfq-3qr2-6g84
- **Initia Code4rena**: https://code4rena.com/reports/2025-02-initia-cosmos
- **Ethermint ante handler bypass**: https://jumpcrypto.com/bypassing-ethermint-ante-handlers/
- **Evmos module accounts**: https://docs.evmos.org/protocol/module-accounts
- **Evmos EVM hooks**: https://docs.evmos.org/protocol/modules/evm#hooks
