Reproduced Exploit

Blackhole — inverted `setRouter` zero-address check bricks pool launches

1. setRouter(address _router) is meant to let the owner update the DEX router used when a GenesisPool launches and adds liquidity. 2. The require is inverted: require(_router == address(0), "ZA") — so the only accepted value is the zero address. 3. A valid non-zero router always reverts "ZA". The o…

May 2025Otheraccess-control3 min read

Chain

Other

Category

access-control

Date

May 2025

Source

AuditVault

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, Balances (native / ERC-20 / NFT), 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. Reproduction of a public audit finding curated by AuditVault — the original finding: 58333-h-01-router-address-validation-logic-error-prevents-valid-ro. Standalone Foundry PoC and full write-up: 58333-h-01-router-address-validation-logic-error-prevents-valid-ro_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/broken-logic · vuln/logic/wrong-condition · vuln/liveness/admin-brick

Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only forge-std — no fork, no RPC, no anvil_state. Full trace: output.txt. PoC: test/58333-h-01-router-address-validation-logic-error-prevents-valid-ro_exp.sol.

AuditVault taxonomy: severity/high · sector/dex · sector/governance · platform/code4rena · access-roles · proxy-initialization · logic/wrong-condition · data-corruption/state-manipulation · known-pattern · privileged-tx · add-check · blast-radius/single-pool


Key info#

ImpactHIGH — owner can only set the DEX router to address(0), never a functional address; zero-router launches revert and lock GenesisPool liquidity
ProtocolBlackhole (Audit 507) — GenesisPoolManager
Vulnerable codeGenesisPoolManager.setRouterrequire(_router == address(0), "ZA") (GenesisPoolManager.sol#L314)
Bug classInverted zero-address check (broken admin setter)
FindingCode4rena 2025-05-blackhole · #58333 · reporter AvantGard
Reportcode4rena.com/reports/2025-05-blackhole
SourceAuditVault
StatusAudit finding — mitigated in Blackhole mitigation contest. Reproduced as a standalone local PoC.
Compiler^0.8.24 (PoC)

TL;DR#

  1. setRouter(address _router) is meant to let the owner update the DEX router used when a GenesisPool launches and adds liquidity.
  2. The require is inverted: require(_router == address(0), "ZA") — so the only accepted value is the zero address.
  3. A valid non-zero router always reverts "ZA". The owner can only clear the router.
  4. _launchPool passes the stored router into IGenesisPool.launch. With router == address(0), launch reverts and depositors' native/funding tokens stay locked in the pool.
  5. Fix: require(_router != address(0), "ZA").

The vulnerable code#

SOLIDITY
function setRouter(address _router) external onlyOwner {
    require(_router == address(0), "ZA"); // @> VULN: inverted check
    // FIX: require(_router != address(0), "ZA");
    router = _router;
}

Root cause#

A classic inverted null-check. The developer almost certainly meant "reject the zero address" (!=) but wrote "require the zero address" (==). The setter becomes a one-way clear function with no path to assign a working router after deploy (or after a mistaken clear).

Preconditions#

  • Caller is the owner (privileged setter — in scope for high because core launch path depends on it).
  • A GenesisPool reaches the launch stage with liquidity already deposited.

Attack walkthrough#

  1. GenesisPool is funded with 1 ETH of liquidity awaiting launch.
  2. Owner tries setRouter(newValidRouter) → reverts "ZA".
  3. Owner calls setRouter(address(0)) → succeeds; router is cleared.
  4. Owner calls launchPool → pool.launch reverts "Router is address(0)".
  5. 1 ETH remains locked; pool never launches.

Diagrams#

flowchart TD A["Owner calls setRouter(newRouter)"] --> B{"require(_router == address(0))"} B -->|"newRouter != 0"| C["Revert ZA"] B -->|"_router == 0"| D["router = address(0)"] D --> E["launchPool uses router"] E --> F["GenesisPool.launch(address(0)) reverts"] F --> G["Liquidity stays locked in pool"]

Impact#

Admin cannot repair a misconfigured or upgraded router. Clearing the router (the only successful setRouter path) bricks every subsequent pool launch and can permanently lock depositors' funds until a contract upgrade fixes the setter.

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

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.