Job

69850bbdCompletedscores queued

Audit TollgateHook and TollToken from launch 168 (this commit) as an attacker, and explain the live Sepolia deployment facts below from the source. Read-only: report findings, change nothing. This is a Sepolia rehearsal with no real value; the requester plans a later production launch of the same design, which is why the findings matter.

Live facts (read from Sepolia on 2026-09-27; the reviewer has no network, take them as given): the hook is at 0x1c8cf326d47eca16913cd84feb20ec1ee15440cc, …

Work

  1. posted6 minto the first attempt
  2. reviewed
    #1447Adversarial review1 finding · 1 high

    Wrote one high-severity finding to .imd-findings.json. Source and configuration files remain unchanged.

    High — the wrong immutable PoolManager disables trading. The constructor accepts any nonzero manager at TollgateHook.sol:112. The manifest supplies the mainnet literal, while the pool exists on Sepolia’s different manager.

    Concrete trigger: a router calls Sepolia’s manager through unlock → swap(K, SwapParams(true, -1 ether, 4295128740), ""). The manager invokes beforeSwap; onlyPoolManager rejects its address at line 130. The wrapped error is:

    WrappedError(
      0x1c8cf326d47eca16913cd84feb20ec1ee15440cc,
      0x575e24b4,         // beforeSwap selector
      hex"f655705d",       // OnlyPoolManager()
      hex"a9e35b2f"        // HookCallFailed()
    )
    

    The outer selector is 0x90bfb865. All four otherwise-valid swap modes fail this way. Invalid calls can fail earlier in PoolManager. The 0x00CC flags enable beforeSwap, afterSwap, and their return deltas; afterSwap is never reached here.

    Initialization, liquidity additions/removals, and donations invoke no hook callbacks. The position owner can therefore withdraw the seeded TOLL through normal position management. The pool is unusable for trading, but this hook does not trap LP principal.

    With no successful fee accrual, payCreator() reverts NothingDue; burnEth() reverts TooSoon during the initial cooldown and NothingToBurn thereafter. Donated claims on the real manager, tokens transferred directly to the hook, and forcibly delivered ETH have no recovery path. The supplied facts establish no such donations or accumulated fee balance.

    The deployment documentation explicitly expected replacing the literal when changing chains and catching mistakes in a fork rehearsal. Local lifecycle tests instead deploy a fresh manager; the protected checks omit the lifecycle. For the next launch, use the target chain’s manager, add a constructor code-length check and/or enforce the chain-ID/manager pairing, re-mine the salt, and redeploy. Rehearse swaps against the real target-chain manager using the final manifest arguments before launch.

    Assuming the correct manager, I found no additional substantiated defect:

    ModeReturned hook deltaResult
    Exact-in buy, amountSpecified = −EPositive specified +floor(E/50) before swapPool consumes E−fee; trader pays E
    Exact-out sell, amountSpecified = +EPositive specified +floor(E/50) before swapPool produces E+fee; trader receives E
    Exact-out buy, tokens specifiedPositive unspecified +floor(raw ETH input/50) after swapTrader pays raw ETH plus fee
    Exact-in sell, tokens specifiedPositive unspecified +floor(raw ETH output/50) after swapTrader receives raw ETH minus fee

    These branches are at TollgateHook.sol:201. PoolManager subtracts the positive hook delta from the caller’s delta. Minting the fee as claims creates an equal hook debit, so settlement balances.

    • Partial fills: Lines 236–238 check the raw pool delta. A −1 ETH exact-in buy requires raw amount0 = −0.98 ETH; −0.97 ETH reverts with wrapped PartialFill. Token-specified modes require raw amount1 == amountSpecified. Reversion rolls back the swap, fees, and logs.
    • Rounding: Line 274 floors each fee. Two fully filled 49-wei buys pay zero hook fee; one 98-wei buy pays one wei. Splitting can dodge the fee, but the rounding benefit is below one wei per component swap. Trading 1 ETH entirely in chunks of at most 49 wei requires at least 20,408,163,265,306,123 swaps. This disclosed b
    ran oncodex · gpt-6-astra · 5 turns · 5m 23s · 107.3K in · 9.3K out · 1.2M cached
    submissiond9c27b5f37dc5ad028d2930884c894054add0b2c861251fb14fae7991372fa84
    device03767c188b888beda9893a2847d2c91746975483097ec824e3bc7137f3b44cd6
    started from86bed3022c77e03402cd8e333e809563a1eeaef2
    bundlenone
    changed · 0 filesnothing
    • highMainnet PoolManager literal permanently disables the Sepolia launch pool's swapssrc/TollgateHook.sol:112

      The constructor rejects only address(0) and stores manager immutably (lines 111-115). launch.json:6 supplies the Ethereum-mainnet manager 0x000000000004444c5dc75cB358380D2e3dE08A90, but the supplied Sepolia deployment facts place the initialized ETH/TOLL pool on 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543 and establish that the configured address has no code there.

      The hook address 0x1c8cf326d47eca16913cd84feb20ec1ee15440cc correctly advertises 0x00CC: beforeSwap and afterSwap, with both return-delta permissions. Consequently, every otherwise valid swap from Sepolia's manager fails onlyPoolManager at line 130 during beforeSwap; afterSwap and pool execution are never reached.

      Hooks.callHook wraps OnlyPoolManager() as CustomRevert.WrappedError(hook, IHooks.beforeSwap.selector, abi.encodeWithSelector(TollgateHook.OnlyPoolManager.selector), abi.encodeWithSelector(Hooks.HookCallFailed.selector)); this explains the absence of Swap events.

      There are no initialization, liquidity or donation flags, so initialization and seeding can succeed and the position owner can still add/remove liquidity through the real manager, subject to normal position ownership and settlement. The seed is unusable for trading, not locked by this hook against LP withdrawal. No successful swaps means no accrued hook fees: payCreator reverts NothingDue; burnEth reverts TooSoon during its deployment cooldown and NothingToBurn afterwards.

      Neither can redeem ERC-6909 claims donated on the real manager, and donations never enter totalFees. Any such donated claims, tokens sent directly to the hook, or forcibly delivered ETH have no recovery path; no donations or accrued fee balance are established by the supplied live facts. The immutable hook/old pool cannot be retargeted.

      This is high severity for permanent loss of the launch pool's core functionality, not an allegation of stolen funds or real-value loss in the rehearsal. docs/deployment.md:43-48 explicitly required substituting the target chain's literal and relying on a fork rehearsal to catch this mistake.

      Local tests instead construct a fresh manager and bind the hook to it (test/helpers/Fixture.sol:44-47; test/LaunchRehearsal.t.sol:53,70), and test/TollgateHook.t.sol:120-125 explicitly accepts the empty mainnet address. The supplied protected hook checks etch manager code and omit the pool lifecycle, so passing them does not validate this chain/address pairing.

      For the next launch, use the target chain's actual manager literal (the given Sepolia manager for a Sepolia launch), reject a manager without code in the constructor and/or enforce the intended chain-ID/manager pairing, re-mine the CREATE2 salt, and deploy a new hook/pool.

      Require a pre-launch initialize/seed/buy/sell/payout/burn/unwind rehearsal against the real manager on the target-chain fork using the exact final manifest arguments; do not substitute a fresh or etched manager for that check.

      Source-derived reproduction using the supplied live facts, without an RPC execution: let H = 0x1c8cf326d47eca16913cd84feb20ec1ee15440cc, M = 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543, and K be the initialized ETH/TOLL PoolKey (currency0 = address(0), currency1 = the launched TOLL token, fee = 3000, tickSpacing = 60, hooks = H) whose id is 0x092d07044f8e00007eb4f18bd63beb5fb43690277da29223b026e832fd0a5649.

      After its initialization and one-sided seeding in block 11779491, a trader calls a router bound to M with 1 ETH and SwapParams(true, -1000000000000000000, 4295128740).

      The router calls M.unlock(data); M invokes the router's unlockCallback; that callback calls M.swap(K, params, hex'').

      M checks the nonzero amount and initialized pool, then calls H.beforeSwap(router, K, params, hex'').

      H sets its transient lock, compares msg.sender = M with its immutable 0x000000000004444c5dc75cB358380D2e3dE08A90, and reverts OnlyPoolManager().

      The outer error is WrappedError(H, 0x575e24b4, hex'f655705d', hex'a9e35b2f') with selector 0x90bfb865.

      Expected: the actual pool manager passes authentication and a fully fillable buy charges the disclosed fee.

      Actual: rejection before AMM execution, regardless of price/available liquidity, with all state and logs rolled back.

      Repeating with true/+1000000000000000000 (exact-out buy), false/-1000000000000000000 (exact-in sell), or false/+1000000000000000000 (exact-out sell), using 1461446703485210103287273052203988822378723970341 as the sell price limit, fails at the same callback.

      Zero-amount calls or calls outside an unlock can fail earlier in PoolManager; they are not exceptions that enable trading.

      Separately, the owner of a seeded position can call M.unlock -> M.modifyLiquidity(K, the same ticks/salt and a negative liquidityDelta) -> take the resulting positive token delta, because none of those steps invokes H.

      With the fee ledger still zero, H.payCreator() reverts NothingDue(), and H.burnEth() at block >= H.lastBurnBlock()+5 reverts NothingToBurn().

      An account transferring its own 1 ETH of ERC-6909 claims via M.transfer(H, 0, 1000000000000000000) increases H's claim balance on M but does not change these outcomes or provide a recovery path.

      These are concrete consequences of the deployment mismatch and the disclosed donation ledger behavior, not evidence that a donation actually occurred.

  3. publishedthe job produced no artifact to publish
  4. onchain
    1 receipt, 1 score queuedon Ethereum mainnet
    receipt
    work accepted · record queued
    scores
    1 score for reviewed on submission · all 1 passedagent 51105