GitHub Security Advisory: GHSA-6447-269v-g68m
Package: github.com/mezo-org/mezod (GO)
Severity: HIGH (CVSS 0.0)
CWE: CWE-662: Improper Synchronization
Vulnerable versions: <= 7.0.0
Patched in: 8.0.0

Summary: Mezo: ERC-20 bridgeOut burn can be erased by a stale StateDB overwrite leading to full L1 bridge drain

Details:
**Note: the fixed version of the validator client has been deployed for some time.**

### Impact

Potential full drain of L1 bridge without changing bridged balance on Mezo.

## Brief/Intro

A malicious user can steal all ERC-20 tokens locked in the L1 bridge by repeatedly calling the `bridgeOut` precompile from a crafted contract. The precompile's ERC-20 burn executes in an **inner StateDB** that commits to a cache context, but the **outer StateDB** still holds stale pre-burn storage slots in its `dirtyStorage`. When the outer transaction commits, it overwrites the inner burn with stale values -- **restoring the attacker's balance and allowance** while the `AssetsUnlocked` event has already been persisted to the bridge store. The Ethereum sidecar observes this event and attests the unlock on L1, releasing real tokens to the attacker. The attacker keeps their Mezo balance intact and can repeat the drain every block.

## Vulnerability Details

Mezo's EVM uses a dual-context state architecture for precompile calls. When a precompile is invoked, `Contract.Run()` calls `stateDB.CacheContext()` which creates a `cachedCtx` branched from the base `ctx`. Cosmos-side state changes (like ERC-20 burns) happen on `cachedCtx`, while standard EVM storage operations (SLOAD/SSTORE) operate on the outer StateDB's `dirtyStorage` backed by `baseCtx`.

The `bridgeOut` precompile handles two token types with fundamentally different mechanisms:

- **BTC (`burnBitcoin`)**: Burns via `x/bank` (Cosmos native), then explicitly records a `journal.SubBalance` entry. `syncJournalEntries()` propagates this to the outer StateDB, keeping both contexts in sync.
- **ERC-20 (`burnERC20`)**: Burns via `ExecuteContractCall`, which creates an **entirely new inner StateDB** operating on `cachedCtx`. This inner StateDB executes `burnFrom` (decreasing balance, supply, and allowance slots), commits its changes to `cachedCtx`, and is discarded. **No journal entry is created.** The outer StateDB is never informed of these storage changes.

This creates a critical asymmetry. After `burnERC20` returns, the outer StateDB's `dirtyStorage` still contains the **pre-burn allowance** (written by `approve` earlier in the same transaction), and has **never loaded** the balance or supply slots. When the attacker triggers a subsequent `transfer(sink, 1)` in the same transaction, the outer StateDB performs a `GetCommittedState` on the balance slot. This reads from `baseCtx` -- which holds the **stale pre-burn value** because `cachedCtx` changes haven't been flushed to `baseCtx` yet. The stale balance minus 1 is written to `dirtyStorage`.

At end-of-transaction, `StateDB.Commit()` executes in this order:

```go
func (s *StateDB) Commit() error {
    if s.flushCache != nil {
        s.flushCache()    // Step 1: flush cachedCtx → baseCtx (inner burn lands)
    }
    return s.commit(s.ctx) // Step 2: write dirtyStorage → baseCtx (stale values overwrite)
}
```

Step 1 flushes the inner burn into `baseCtx`.