Molt EVM — Weak onlySpawnerToken Access Control Enables Unlimited Mint

On 2026-03-08 at 20:36 UTC (Base block 43093167), an attacker exploited a trivially bypassable onlySpawnerToken modifier on the MoltEVM token contract (0x225da3d879d379ff6510c1cc27ac8535353f501f) to mint 100,000,000 mEVM tokens at zero cost. The modifier requires only that the caller is a contract whose initialized() function returns true — any attacker-deployed stub contract satisfies both conditions. The attacker sold 99M mEVM through the mEVM-WETH liquidity pool, draining approximately 50.36 WETH (~$101K) from LPs. An additional ~$26K was attributed to the secondary impact of the resulting price collapse.


Root Cause

Vulnerable Contract

  • Name: MoltEVM
  • Address: 0x225da3d879d379ff6510c1cc27ac8535353f501f
  • Chain: Base
  • Proxy: No
  • Source type: Verified (Basescan)

Vulnerable Function

  • Function: mintFromSpawner
  • Signature: mintFromSpawner(address account, uint256 amount)
  • Selector: 0xc5623f54
  • File: MoltEVM.sol (Basescan lines 2288–2296, modifier at lines 1419–1423)

Vulnerable Code

// MoltEVM.sol — verified source, Basescan lines 1419–1423 and 2288–2296

interface ISpawnerToken {
    function initialized() external view returns (bool);
}

modifier onlySpawnerToken() {
    require(
        isContract(_msgSender()) &&                          // <-- VULNERABILITY: any contract passes
        ISpawnerToken(_msgSender()).initialized() == true,  // <-- VULNERABILITY: trivially satisfied — no whitelist
        "Not a spawner token"
    );
    _;
}

function mintFromSpawner(address account, uint256 amount)
    external
    onlySpawnerToken
    returns (bool)
{
    _mint(account, amount);
    _spawnerMinted(account, amount);
    return true;
}

Why It’s Vulnerable

Expected behavior: mintFromSpawner should only be callable by a specific, trusted spawner contract — one whitelisted by the protocol deployer and stored in contract state (e.g., an address public spawnerToken slot checked against msg.sender).

Actual behavior: The modifier validates only two conditions: (1) msg.sender is a contract (passes isContract()) and (2) msg.sender.initialized() returns true. Both conditions are trivially satisfied by any minimal attacker contract. There is no stored spawner address, no role assignment, no ownership check, and no one-time initialization guard. Any deployed contract that returns true from initialized() can call mintFromSpawner and mint an arbitrary quantity of mEVM tokens.

Impact: Because mintFromSpawner accepts an arbitrary amount parameter and mints directly via _mint(), a single call mints unbounded tokens. The attacker passed 100,000,000 × 10¹⁸ (100M mEVM with 18 decimals), inflating the total supply catastrophically and enabling a full LP drain via a subsequent swap.

Normal flow vs Attack flow:

  • Normal: Legitimate spawner contract (built into Molt EVM system, initialized() = true for operational reasons) calls mintFromSpawner with bounded amounts as part of the protocol’s token emission schedule.
  • Attack: Attacker deploys a stub contract with a one-liner initialized() external pure returns (bool) { return true; }, calls mintFromSpawner with 100M tokens, modifier passes, unlimited supply injected into the LP.

Attack Execution

High-Level Flow

  1. Attacker (0x0fcca397b0e3ca31520e070f5fd2f89ce60320d2) pre-deploys AttackHelper (0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b) — a minimal contract implementing initialized() → true and minting/swap logic.
  2. Attacker calls AttackHelper with entry method labeled “RugPull” on Basescan, passing MoltEVM as the target.
  3. AttackHelper calls MoltEVM.mintFromSpawner(AttackHelper, 100_000_000 mEVM).
  4. MoltEVM’s onlySpawnerToken modifier calls back AttackHelper.initialized() via STATICCALL — receives true, passes the guard.
  5. MoltEVM mints 100,000,000 mEVM to AttackHelper.
  6. AttackHelper sends 99,000,000 mEVM to the mEVM-WETH LP (0x064c9fbed2cce0fdc3600777492bc0413b2cf95e), triggering a swap.
  7. LP sends 50.361893 WETH to AttackHelper as swap proceeds (constant-product AMM drained by the massive imbalance).
  8. AttackHelper retains 1,000,000 mEVM unsold and self-destructs, taking the WETH with it.

Detailed Call Trace

Note: Raw RPC tracing was unavailable; trace_callTracer.json is a manual reconstruction. The call flow below is derived from Basescan token transfer evidence (funds_flow.json, explorer_evidence.md) and the verified MoltEVM source. All selector values are verified with cast sig.

[CALL] 0x0fcca397b0e3ca31520e070f5fd2f89ce60320d2 (AttackerEOA)
    → 0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b (AttackHelper)
    method: <unresolved, Basescan label "RugPull">
    value: 0 ETH

  [CALL] 0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b (AttackHelper)
      → 0x225da3d879d379ff6510c1cc27ac8535353f501f (MoltEVM)
      method: mintFromSpawner(address,uint256) [0xc5623f54]
      args: account=0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b, amount=100000000000000000000000000
      value: 0 ETH

    [STATICCALL] 0x225da3d879d379ff6510c1cc27ac8535353f501f (MoltEVM)
        → 0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b (AttackHelper)
        method: initialized() [0x158ef93e]
        ← returns: true (0x0000...0001)

    ← mintFromSpawner executes: _mint(0x06a4..., 100_000_000e18)
      ↳ Transfer event: 0x0000...0000 → 0x06a4... 100,000,000 mEVM

  [CALL] 0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b (AttackHelper)
      → 0x064c9fbed2cce0fdc3600777492bc0413b2cf95e (mEVM-WETH LP)
      transfer: 99,000,000 mEVM (swap input)

  [CALL] 0x064c9fbed2cce0fdc3600777492bc0413b2cf95e (mEVM-WETH LP)
      → 0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b (AttackHelper)
      transfer: 50.361893264908023429 WETH (swap output)

Selectors verified:

  • mintFromSpawner(address,uint256)0xc5623f54
  • initialized()0x158ef93e

Financial Impact

ItemAmountNotes
mEVM minted100,000,000 mEVMFrom address(0), zero cost
mEVM sold99,000,000 mEVMSent to LP in same tx
WETH drained from LP50.361893 WETH~$101K at ~$2,008/ETH
mEVM retained1,000,000 mEVMUnsold remainder in AttackHelper
Secondary impact~$26KLP token value destruction, price collapse

Primary realized gain (tx 0xca64...): 50.361893 WETH ≈ $101K, received by AttackHelper (0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b).

Who lost: Liquidity providers in the mEVM-WETH pool (0x064c9fbed2cce0fdc3600777492bc0413b2cf95e) lost ~50.36 WETH. The LP absorbed 99M artificial mEVM tokens and was left with near-zero effective value.

Protocol solvency: The MoltEVM token supply was permanently inflated. A rescue transaction (0x10b7ec56..., block 43094802) minted an enormous additional quantity of mEVM to 0x000...dead as a mitigation attempt, further confirming the minting path was still open post-exploit. $2K was recovered and returned to the deployer.

No flash loan was used — the exploit required no borrowed capital. The only cost was deployment gas and the tx gas fee.


Evidence

ArtifactValue
Attack tx0xca64f5dc107afb6eb71d612bb7156aa218aaab07b961e0cb83892727e941630a
Block43093167, 2026-03-08 20:36:37 UTC
Receipt status0x1 (success)
mEVM mint Transfer logfrom=0x0000...0000, to=0x06a4..., amount=100,000,000 mEVM
WETH swap Transfer logfrom=0x064c...(LP), to=0x06a4..., amount=50.361893 WETH
Selector mintFromSpawnercast sig "mintFromSpawner(address,uint256)"0xc5623f54
Selector initializedcast sig "initialized()"0x158ef93e
Vulnerable modifierMoltEVM.sol lines 1419–1423 (verified Basescan)
AttackHelper sourceSelf-destructed; no bytecode recoverable at latest block