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() = truefor operational reasons) callsmintFromSpawnerwith 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; }, callsmintFromSpawnerwith 100M tokens, modifier passes, unlimited supply injected into the LP.
Attack Execution
High-Level Flow
- Attacker (
0x0fcca397b0e3ca31520e070f5fd2f89ce60320d2) pre-deploysAttackHelper(0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b) — a minimal contract implementinginitialized() → trueand minting/swap logic. - Attacker calls
AttackHelperwith entry method labeled “RugPull” on Basescan, passing MoltEVM as the target. AttackHelpercallsMoltEVM.mintFromSpawner(AttackHelper, 100_000_000 mEVM).- MoltEVM’s
onlySpawnerTokenmodifier calls backAttackHelper.initialized()via STATICCALL — receivestrue, passes the guard. - MoltEVM mints 100,000,000 mEVM to
AttackHelper. AttackHelpersends 99,000,000 mEVM to the mEVM-WETH LP (0x064c9fbed2cce0fdc3600777492bc0413b2cf95e), triggering a swap.- LP sends 50.361893 WETH to
AttackHelperas swap proceeds (constant-product AMM drained by the massive imbalance). AttackHelperretains 1,000,000 mEVM unsold and self-destructs, taking the WETH with it.
Detailed Call Trace
Note: Raw RPC tracing was unavailable;
trace_callTracer.jsonis 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 withcast 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
| Item | Amount | Notes |
|---|---|---|
| mEVM minted | 100,000,000 mEVM | From address(0), zero cost |
| mEVM sold | 99,000,000 mEVM | Sent to LP in same tx |
| WETH drained from LP | 50.361893 WETH | ~$101K at ~$2,008/ETH |
| mEVM retained | 1,000,000 mEVM | Unsold remainder in AttackHelper |
| Secondary impact | ~$26K | LP 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
| Artifact | Value |
|---|---|
| Attack tx | 0xca64f5dc107afb6eb71d612bb7156aa218aaab07b961e0cb83892727e941630a |
| Block | 43093167, 2026-03-08 20:36:37 UTC |
| Receipt status | 0x1 (success) |
| mEVM mint Transfer log | from=0x0000...0000, to=0x06a4..., amount=100,000,000 mEVM |
| WETH swap Transfer log | from=0x064c...(LP), to=0x06a4..., amount=50.361893 WETH |
Selector mintFromSpawner | cast sig "mintFromSpawner(address,uint256)" → 0xc5623f54 ✓ |
Selector initialized | cast sig "initialized()" → 0x158ef93e ✓ |
| Vulnerable modifier | MoltEVM.sol lines 1419–1423 (verified Basescan) |
| AttackHelper source | Self-destructed; no bytecode recoverable at latest block |
Related URLs
- https://basescan.org/tx/0xca64f5dc107afb6eb71d612bb7156aa218aaab07b961e0cb83892727e941630a
- https://basescan.org/address/0x225da3d879d379ff6510c1cc27ac8535353f501f#code
- https://basescan.org/address/0x06a4a10abbea43ad0016ab7d44dd7633f4f3ec8b
- https://basescan.org/tx/0x10b7ec56e852351de4951ab2a91fca4099fc4f385dd2171951476c3d497fe03d