DEV Community

ohmygod
ohmygod

Posted on

Inherited Poison: How SagaEVM Lost $7M to an Ethermint Precompile Bug It Didn't Write

In January 2026, an attacker minted $7 million worth of Saga Dollar stablecoins out of thin air — not by finding a bug in Saga's own code, but by exploiting a vulnerability in Ethermint's EVM precompile that Saga inherited when it forked the codebase.

The SagaEVM hack is one of the clearest demonstrations of a growing threat in the modular blockchain era: inherited vulnerabilities in forked infrastructure code. If you're building on Ethermint, Cosmos SDK, or any forked EVM implementation, this article is a post-mortem you can't afford to skip.


What Are EVM Precompiles (And Why They're Dangerous)?

Precompiled contracts are special addresses in the EVM that execute native code instead of EVM bytecode. In vanilla Ethereum, precompiles handle computationally expensive operations like elliptic curve math (ecRecover at 0x01, ecAdd at 0x06, etc.).

Ethermint — the EVM implementation for Cosmos SDK chains — extends this concept with custom precompiles that bridge EVM execution to Cosmos-native functionality:

Standard EVM Precompiles (0x01-0x0a):
  ecRecover, SHA-256, RIPEMD-160, identity, modexp, etc.

Ethermint Custom Precompiles:
  Staking precompile    → EVM contracts can delegate/undelegate
  Distribution precompile → EVM contracts can claim rewards
  IBC precompile        → EVM contracts can send IBC messages  ← THIS ONE
  Governance precompile → EVM contracts can vote on proposals
Enter fullscreen mode Exit fullscreen mode

The IBC precompile is the critical one. It allows EVM smart contracts to interact with the Inter-Blockchain Communication protocol — Cosmos's cross-chain messaging layer. In theory, this enables powerful cross-chain DeFi. In practice, it created a trust boundary violation that cost Saga $7 million.


The Attack: Fake IBC Messages, Real Money

Step 1: The Trust Assumption

Saga's stablecoin minting logic worked like this:

1. User deposits collateral on Chain A
2. IBC message sent: "User deposited X collateral on Chain A"
3. SagaEVM receives IBC message via IBC precompile
4. SagaEVM validates message → mints equivalent Saga Dollars
5. User receives stablecoins on SagaEVM
Enter fullscreen mode Exit fullscreen mode

The critical assumption: IBC messages arriving through the IBC precompile are legitimate cross-chain transfers. This assumption was wrong.

Step 2: The Precompile Bypass

The attacker deployed a helper contract on SagaEVM that directly invoked the IBC precompile. The vulnerability was in how the precompile validated the source of IBC messages:

// Simplified illustration of the vulnerable pattern
// The IBC precompile accepted messages from local EVM contracts
// without verifying they originated from an actual cross-chain relay

interface IIBCPrecompile {
    function sendPacket(
        string calldata sourcePort,
        string calldata sourceChannel,
        bytes calldata data,
        uint64 timeoutHeight,
        uint64 timeoutTimestamp
    ) external returns (uint64 sequence);
}

contract Exploit {
    IIBCPrecompile constant IBC = IIBCPrecompile(0x...); // precompile address

    function attack() external {
        // Craft a fake IBC packet that looks like a collateral deposit
        bytes memory fakeDepositMsg = abi.encode(
            // ... encoded to match the expected collateral deposit format
            attackerAddress,
            collateralAmount,  // Claimed: millions in collateral
            // Actual collateral deposited: zero
        );

        // The precompile processes this as if it came from a real IBC relay
        IBC.sendPacket(
            "transfer",
            "channel-0",
            fakeDepositMsg,
            0,
            block.timestamp + 3600
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The core issue: the IBC precompile didn't distinguish between:

  1. Legitimate IBC packets relayed by the IBC module from external chains
  2. Locally crafted packets submitted by EVM contracts on the same chain

This is analogous to a web application trusting X-Forwarded-For headers without verifying they came from a trusted proxy.

Step 3: Mint, Swap, Bridge, Vanish

With the ability to forge IBC deposit messages:

1. Attacker deploys exploit contract on SagaEVM
2. Exploit sends fake IBC message: "10M USDC deposited as collateral"
3. SagaEVM minting logic trusts the IBC precompile → mints 10M Saga Dollars
4. Attacker swaps Saga Dollars for real assets in protocol pools
5. Bridges ~$7M to Ethereum mainnet
6. Deposits into Tornado Cash
Enter fullscreen mode Exit fullscreen mode

The entire attack required no interaction with external chains. Everything happened locally on SagaEVM, exploiting the trust boundary between the EVM execution environment and the Cosmos IBC module.


The Deeper Problem: Forked Code, Inherited Risk

SagaEVM didn't write this vulnerability. It inherited it from Ethermint. This is the same pattern that has burned dozens of DeFi projects:

The Fork Dependency Chain

Ethermint (maintained by Evmos/Cosmos Labs)
    └── SagaEVM (forked Ethermint)
    └── Canto (forked Ethermint)
    └── Cronos (forked Ethermint)
    └── Kava EVM (uses Ethermint)
    └── Injective (uses Ethermint)
    └── Sei (partial Ethermint patterns)
    └── dozens of smaller chains...
Enter fullscreen mode Exit fullscreen mode

When a vulnerability exists in Ethermint's precompile code, every downstream fork is potentially affected. Cosmos Labs confirmed after the Saga hack that this class of vulnerability could impact multiple Ethermint-based chains.

Historical Ethermint Precompile Vulnerabilities

The SagaEVM hack isn't the first time Ethermint precompiles have been exploited:

1. Staking Precompile Infinite Mint (June 2024)

An attacker could interact with the staking precompile's delegate function from within a try/catch block. If the EVM transaction reverted, the EVM state changes (token deduction) were rolled back, but the Cosmos staking state (delegation) persisted. Result: infinite delegation without spending tokens.

// The 2024 staking precompile exploit pattern
contract StakingExploit {
    function exploit() external {
        try stakingPrecompile.delegate(validator, amount) {
            // Success: both EVM and Cosmos state updated
        } catch {
            // EVM state reverted (tokens restored)
            // BUT Cosmos staking state NOT reverted (delegation persists)
            // Net result: free delegation
        }
        // Repeat for infinite staking rewards
    }
}
Enter fullscreen mode Exit fullscreen mode

This revealed a fundamental design flaw: EVM state rollbacks don't cascade to Cosmos module state changes made through precompiles.

2. Ante Handler Bypass (Multiple Instances)

Ethermint's ante handlers (transaction preprocessors) could be bypassed by wrapping MsgEthereumTx inside Cosmos Authz messages, skipping EVM gas fee deductions entirely. Attackers could execute EVM transactions without paying gas.

3. Cross-Chain Replay (CVE-2021-25835)

Early Ethermint versions shared the same chainIDEpoch as Ethereum, allowing signed Ethereum transactions to be replayed on Ethermint chains.

The Pattern

Every one of these vulnerabilities exploits the impedance mismatch between EVM execution and Cosmos SDK state management. The EVM assumes it owns all state transitions. Cosmos SDK assumes its module system is the authority. Precompiles sit at the boundary and, when implemented incorrectly, create gaps where neither system fully validates the other.


Defensive Patterns for Ethermint-Based Chains

1. Precompile Input Validation: Trust Nothing

Every precompile that bridges EVM to Cosmos functionality must validate the caller context:

// Go pseudocode for a hardened IBC precompile
func (p *IBCPrecompile) Run(evm *vm.EVM, contract *vm.Contract, input []byte) ([]byte, error) {
    // CRITICAL: Verify the caller context
    // Only the IBC relayer module should trigger packet processing
    if !p.isCalledFromIBCModule(evm) {
        return nil, errors.New("unauthorized: IBC packets must originate from IBC module")
    }

    // Validate packet structure matches expected format
    packet, err := p.parseAndValidatePacket(input)
    if err != nil {
        return nil, fmt.Errorf("invalid packet: %w", err)
    }

    // Verify the source chain and channel are registered
    if !p.isRegisteredChannel(packet.SourceChannel) {
        return nil, errors.New("unregistered source channel")
    }

    // Cross-reference with actual IBC module state
    if !p.verifyPacketCommitment(packet) {
        return nil, errors.New("packet commitment not found in IBC store")
    }

    // ... proceed with validated packet
}
Enter fullscreen mode Exit fullscreen mode

2. State Consistency Checks Across Boundaries

After any precompile execution that modifies both EVM and Cosmos state, implement post-execution consistency checks:

func (k Keeper) PostPrecompileHook(ctx sdk.Context, evmTxHash common.Hash) error {
    // Compare EVM-side balance changes with Cosmos-side balance changes
    evmChanges := k.getEVMBalanceChanges(evmTxHash)
    cosmosChanges := k.getCosmosBalanceChanges(ctx)

    for account, evmDelta := range evmChanges {
        cosmosDelta, exists := cosmosChanges[account]
        if !exists || !evmDelta.Equal(cosmosDelta) {
            // State inconsistency detected — revert everything
            return fmt.Errorf(
                "state mismatch for %s: EVM=%s, Cosmos=%s",
                account, evmDelta, cosmosDelta,
            )
        }
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

3. Fork Audit: Don't Just Audit Your Code

If you're building on Ethermint (or any fork), your security audit must cover:

□ Your application-specific code
□ All custom precompiles you've added
□ The Ethermint precompile implementations you inherited
□ The Cosmos SDK module interactions triggered by precompiles
□ The state management boundary between EVM and Cosmos
□ Ante handler chain for both Cosmos and EVM transaction types
□ IBC packet validation for any cross-chain functionality
□ Upgrade and migration logic (state can be corrupted during upgrades)
Enter fullscreen mode Exit fullscreen mode

Most projects only audit items 1-2 and assume the rest is "upstream's problem."

4. Differential Testing Against Upstream

Maintain automated tests that verify your fork's behavior matches upstream Ethermint for all precompile operations:

# Differential testing framework sketch
def test_ibc_precompile_rejects_local_packets():
    """
    Verify that the IBC precompile rejects packets 
    crafted by local EVM contracts (not relayed from external chains).
    """
    exploit_contract = deploy_contract(IBC_EXPLOIT_BYTECODE)

    with pytest.raises(EVMRevertError, match="unauthorized"):
        exploit_contract.functions.sendFakeIBCPacket(
            source_port="transfer",
            source_channel="channel-0",
            fake_deposit_data=encode_fake_deposit(1_000_000),
        ).transact()

    # Verify no tokens were minted
    assert stablecoin.balanceOf(attacker) == 0

def test_staking_precompile_reverts_cosmos_state_on_evm_revert():
    """
    Verify that if an EVM transaction reverts after calling
    the staking precompile, the Cosmos delegation is also reverted.
    """
    initial_delegation = get_cosmos_delegation(validator, delegator)

    # This transaction should revert in the EVM
    with pytest.raises(EVMRevertError):
        exploit_contract.functions.delegateAndRevert(
            validator, amount
        ).transact()

    # Cosmos delegation must NOT have increased
    assert get_cosmos_delegation(validator, delegator) == initial_delegation
Enter fullscreen mode Exit fullscreen mode

For Auditors: The Precompile Checklist

If you're auditing an Ethermint-based chain, add these to your methodology:

State Boundary Analysis:

  • [ ] Map every precompile that modifies Cosmos module state
  • [ ] Verify EVM revert cascades to Cosmos state changes
  • [ ] Check for state read inconsistencies (EVM reads stale Cosmos state)
  • [ ] Test try/catch patterns around precompile calls

Access Control:

  • [ ] Verify precompiles check caller context (module vs. external contract)
  • [ ] Test if precompiles can be called via delegatecall
  • [ ] Check if precompile addresses can be called from arbitrary contracts
  • [ ] Verify ante handlers can't be bypassed via Authz or nested messages

IBC-Specific:

  • [ ] Test packet forgery from local EVM contracts
  • [ ] Verify packet commitment proofs are checked
  • [ ] Test channel and port authorization
  • [ ] Check timeout and acknowledgement handling for state consistency

Economic Impact:

  • [ ] Model the maximum extractable value if each precompile is compromised
  • [ ] Identify minting/burning paths accessible through precompiles
  • [ ] Check for oracle manipulation vectors via precompile state changes

Key Takeaways

  1. Forked code is inherited attack surface. SagaEVM's $7M loss came from code it didn't write. If you fork infrastructure, you own its vulnerabilities.

  2. EVM precompiles are trust boundary violations by design. They bridge two state machines with different assumptions. Every precompile is a potential state inconsistency.

  3. The Cosmos-EVM impedance mismatch is a systemic risk. The 2024 staking exploit, the ante handler bypasses, and the 2026 IBC exploit all stem from the same root cause: EVM and Cosmos SDK don't agree on who controls state.

  4. Audit the fork, not just the diff. Most teams audit only their custom code and assume upstream is safe. The SagaEVM hack proves this assumption can cost millions.

  5. Differential testing is non-negotiable for forks. Automated tests that verify your fork's security properties against known attack patterns are the minimum viable defense.

The modular blockchain thesis promises composability and rapid development. But every module boundary, every precompile, every forked codebase is a potential trust boundary violation waiting to be discovered. The $7 million SagaEVM exploit won't be the last of its kind — but with rigorous precompile security, it could be the last one that catches you off guard.


Auditing a Cosmos/Ethermint-based chain? The precompile boundary is where the bugs hide. More security research at dev.to/ohmygod and DreamWork Security.

Top comments (0)