Reproduced Exploit

ONTR Token Zero-Owner `onlyOwner` Free Mint → Pancake WETH Drain

1. ONTR ships a custom Ownable whose onlyOwner is: When ownership has been renounced (owner == 0), any caller is treated as owner.

May 2026Ethereumaccess-control5 min read

Loss

49.480100697512152261 WETH (exact wei: 49480100697512152261)

Chain

Ethereum

Category

access-control

Date

May 2026

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Original research reproduction (not from DeFiHackLabs). Standalone Foundry PoC, offline anvil_state.json, and full write-up: 2026-05-ONTR_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/missing-owner-check · vuln/access-control/broken-logic · vuln/access-control/uninitialized-owner · vuln/logic/incorrect-state-transition

Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder. The fork is served offline from the bundled anvil_state.json (local anvil replays Ethereum state at block 25193099), so no public RPC is required. Full verbose trace: output.txt. Verified vulnerable source: Token.sol, Ownable.sol, IERC20Metadata.sol (obfuscated custom ERC-20 stack verified on Etherscan as contract Token).


Key info#

Loss49.480100697512152261 WETH (exact wei: 49480100697512152261)
Vulnerable contractONTR (OpenTrade) — 0xf074865358b0dd039beee075831f8a2ae6b1f3f3
Attacker EOA0xE806B37A9F965bd9D54AaDf9560C78957550b760
Attack contract0xD7A33e89aBC1Ac5b2497D9589c81784A2BC52491 (CREATE in attack tx, nonce 0)
Victim pair0xd46D89f4675bc96328fBDEB443842cdB5Fcd83FD (PancakeSwap V2 WETH/ONTR on Ethereum)
Attack tx0x98f80eff… (entire drain in constructor)
Chain / block / dateEthereum mainnet / fork 25193099 (attack in 25193100) / ~2026-05-29
CompilerSolidity 0.8.20
Bug classCustom onlyOwner treats owner == address(0) as authorized → free ownership seize → free balance inflate
AlertSlowMist TI

Chain note. Early intake listed BSC. On-chain verification shows Ethereum: pair token0 is mainnet WETH 0xC02a…, factory is Pancake on ETH 0x1097053Fd2ea711dad45caCcc45EfF7548fCB362. Do not conflate with 2026-05-SEAToken_exp (different MetaSea protocol on Arbitrum).


TL;DR#

  1. ONTR ships a custom Ownable whose onlyOwner is:

    SOLIDITY
    require(hazeDeer == address(0) || hazeDeer == _msgSender());
    

    When ownership has been renounced (owner == 0), any caller is treated as owner.

  2. Pre-attack owner() was address(0).

  3. Attacker CREATE (nonce 0) constructor:

    • transferOwnership(this) — seize ownership
    • desertJasper(this, 1e30) — queue a hidden balance credit
    • glenFlash()ashBudbalance[this] += 1e30 without bumping totalSupply
    • transfer(pair, 1e30) + swap49.4801 WETH to the EOA

Background#

Token (ONTR / OpenTrade) is an obfuscated ERC-20 with a custom ownership / allowlist / “pending balance” subsystem nested under a fake OpenZeppelin path (openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol is not the real OZ interface — it is the attack surface).

Relevant pieces:

Symbol (obfuscated)Role
hazeDeerowner storage
hawkDeckbalances mapping
loftQuilttotalSupply
desertJasperonlyOwner: queue (recipient, amount) into moorSouth
glenFlashpermissionless: apply all queued credits via ashBud
ashBudhawkDeck[account] += amount (no supply update)

The vulnerable code#

Broken onlyOwner (root cause)#

20:23:SOURCES/ONTR/OPENZEPPELIN/CONTRACTS/ACCESS/OWNABLE.SOL
    modifier onlyOwner() {
        require(hazeDeer == address(0) || hazeDeer == _msgSender());
        _;
    }

Correct Ownable is require(owner == msg.sender). The extra hazeDeer == address(0) branch turns renounced ownership into open admin.

transferOwnership is gated only by this modifier:

25:28:SOURCES/ONTR/OPENZEPPELIN/CONTRACTS/ACCESS/OWNABLE.SOL
    function transferOwnership(address tideFlint) public virtual onlyOwner {
        require(tideFlint != address(0));
        smokeAxe(tideFlint);
    }

Free balance inflate path#

134:146:SOURCES/ONTR/OPENZEPPELIN/CONTRACTS/TOKEN/ERC20/EXTENSIONS/IERC20METADATA.SOL
    function desertJasper(address starField, uint256 meadowWood) public onlyOwner {
        require(roseBanner(_msgSender()));
        // ...
        marchTree[harborCoil] = bufferMyrtle(address(0), starField, meadowWood, block.timestamp);
        if (meadowWood > 0) {
            moorSouth.push(harborCoil);
        }
        vortexMesa.push(harborCoil);
        harborCoil++;
    }
178:189:SOURCES/ONTR/OPENZEPPELIN/CONTRACTS/TOKEN/ERC20/EXTENSIONS/IERC20METADATA.SOL
    function glenFlash() external {
        uint256 sandFern = moorSouth.length;
        for (uint256 storkWind = 0; storkWind < sandFern; storkWind++) {
            uint256 riverDawn = moorSouth[storkWind];
            bufferMyrtle storage mapleTusk = marchTree[riverDawn];
            ashBud(mapleTusk.starField, mapleTusk.meadowWood);
        }
        delete moorSouth;
    }
49:51:SOURCES/ONTR/TOKEN.SOL
    function ashBud(address depthDock, uint256 meadowWood) internal virtual override {
        hawkDeck[depthDock] += meadowWood;
    }

ashBud is not a mint: it never increments loftQuilt (totalSupply). The pair still prices the inflated ERC-20 balance as real supply when swapping.


Root cause#

Two bugs compose:

  1. Auth hole: onlyOwner succeeds for everyone after renounce.
  2. Hidden mint: owner-gated desertJasper + public glenFlash/ashBud increases spendable balance without updating totalSupply or emitting a proper mint event from address(0) via the normal deltaPlume path.

Either alone is dangerous; together they let an unprivileged CREATE drain the only liquid market (WETH/ONTR Pancake pair).


Preconditions#

  • ONTR owner() == address(0) (confirmed at block 25193099).
  • Live Pancake pair with ~50.03 WETH reserves.
  • Attacker EOA with nonce 0 (historical CREATE address) and gas.

Attack walkthrough#

Historical attack is a single CREATE — no separate attack call.

sequenceDiagram participant EOA as Attacker EOA participant AC as Attack contract (CREATE) participant T as ONTR Token participant P as Pancake WETH/ONTR participant W as WETH EOA->>AC: CREATE (nonce 0, all logic in ctor) Note over T: owner == address(0) AC->>T: transferOwnership(AC) Note over T: onlyOwner passes because owner==0 AC->>T: desertJasper(AC, 1e30) AC->>T: glenFlash() Note over T: balance[AC] += 1e30 (no totalSupply bump) AC->>T: transfer(P, 1e30) AC->>P: swap(amount0Out=WETH, to=EOA) P->>W: Transfer WETH to EOA Note over EOA: +49.480100697512152261 WETH

On-chain log highlights (attack tx 0x98f80eff…, block 25193100):

  1. OwnershipTransferred(0x0 → 0xD7A33e89…)
  2. Transfer(0xD7A33e89… → pair, 1e30) (inflated ONTR)
  3. Transfer(pair → EOA, 49480100697512152261) (WETH)
  4. Pair Sync: WETH reserve collapses from ~50.03 → ~0.55

PoC assertions (offline output.txt):

  • Attacker WETH profit = 49480100697512152261
  • totalSupply unchanged at 1e27
  • New owner = attack contract

Remediation#

  1. Fix onlyOwner: require(owner() == msg.sender) only. Never treat address(0) as authorized.
  2. If renouncing, burn admin paths: after renounce, transferOwnership, desertJasper, recover, etc. must permanently revert.
  3. No silent balance credits: any balance increase must go through a real mint that updates totalSupply and is access-controlled with a non-zero owner or a timelocked governance role.
  4. Do not leave permissionless appliers (glenFlash) sitting on top of owner-gated queues after ownership is gone.

How to reproduce#

BASH
# Offline (no RPC):
_shared/run_poc.sh 2026-05-ONTR_exp -vvvvv
# Expect: [PASS] testExploit — Attacker WETH profit 49.480100697512152261

Historical CREATE initcode is embedded in test/ONTR_exp.sol and replayed at fork block 25193099. A clean synthetic path lives in test/2026-05-ONTR.sol for the site playground.

Reference: SlowMist TI alert


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.