Job
What the contracts are for
PepesFamily is a token launchpad on Robinhood Chain (chain ID 4663), built on Uniswap v4.
Anyone can launch a token with a fixed supply of 1B, paired with ETH or IMD.
At launch, the whole supply becomes single-sided liquidity in a new v4 pool. The launchpad contract owns that position and has no way to remove it, so liquidity is locked forever.
The launchpad is also the pool's v4 hook. It takes 4% of the quote side of every swap, through any router: 1% to the …
Published
- report
- Identity-md/research/blob/main/jobs/a3e708e2-fb57-43ea-a163-d93b916694a2/_identitymd/README.md
Audit report
7 findingsFour agents audited the code as it is at d1ad578, each in one area, and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the code was changed or deployed.
Download the report (Markdown) · archived copy on GitHub
3 low3 info
1.Quote-specified swaps are charged 4% of the requested amount, so a partial fill at sqrtPriceLimitX96 pays up to ~100% of what actually traded (or reverts with Panic 0x11)contracts/src/PepesFamily.sol:310
uint256 fee = exactIn ? (amount * FEE_BPS) / BPS : (amount * FEE_BPS) / (BPS - FEE_BPS);
proof · a Foundry test that fails on this code and passes once it is fixed2.lowA zero-fill swap on a quote-empty pool moves the price to the end of the curve for free; marketCap(), getTokenInfo() and the Trade/Swap events then report ~0contracts/src/PepesFamily.sol:331
uint256 tokenAmount = uint256(int256(t < 0 ? -t : t));
proof · a Foundry test that fails on this code and passes once it is fixed3.lowsetStartTick accepts ticks below about -349,200 for which every launch on that quote reverts with TickLiquidityOverflow (and far-positive ticks that price launches at a few wei)contracts/src/PepesFamily.sol:451
if (tick % TICK_SPACING != 0 || tick > limit || tick < -limit) revert BadTick();
proof · a Foundry test that fails on this code and passes once it is fixed4.lowsellWithPermit / sellForEthWithPermit only accept a permit signed for exactly tokenAmount, contradicting the documented `value >= tokenAmount`contracts/src/PepesFamilyRouter.sol:42
try IERC20Permit(token).permit(msg.sender, address(this), amount, deadline, v, r, s) {}Holder dan buys 1 ETH of an ETH-quoted token (balance bal). dan signs a valid EIP-2612 permit for spender = router, value = 2*bal, nonce 0, deadline = now. dan calls router.sellWithPermit(token, bal, 1, now, v, r, s).
Expected per NatSpec: the sale goes through because the signed value >= tokenAmount.
Actual: reverts PermitFailed() (test_permitLargerValueRejected in test/scratch/JudgeChecks.t.sol, expectRevert(PermitHelper.PermitFailed.selector) passes).
5.infoTrade event attributes third-party-router swaps to tx.origin, misattributing trades made through contract wallets, bundlers, aggregators and the project's own ETH routercontracts/src/PepesFamily.sol:348
address trader = sender == router && hookData.length == 32 ? abi.decode(hookData, (address)) : tx.origin;
For any swap not sent by
PepesFamilyRouterwith a 32-byte hookData (including the project's own PepesFamilyEthRouter, which passes empty hookData, Universal Router, aggregators, ERC-4337 bundlers and Safe/EIP-7702 wallets)traderistx.origin: the relayer or EOA that signed the outer transaction, not the account whose funds moved.The website's trade list and any analytics built on the event therefore show the wrong address for those trades; a relayer submitting many users' trades appears as one whale. No funds are affected; only the event is wrong.
Fix: fall back to
sender(the locker) when hookData carries no trader, and have PepesFamilyEthRouter pass abi.encode(r.user) as hookData and be recognised likerouter. From audit_permissions 58e4b03b.vm.prank(bob, carol) (msg.sender bob, tx.origin carol); bob swaps 1 ETH exact-in through PoolSwapTest on an ETH-quoted pool.
Expected: Trade.trader == bob (or the router contract).
Actual: Trade.trader == carol (test_tradeEventTxOrigin in test/scratch/JudgeChecks.t.sol: expectEmit with trader = carol passes).
6.infoThe 4% fee and holder rewards only bind swaps in the hooked pool; any second pool for the same token trades fee-free (design boundary, should be stated as accepted)contracts/src/PepesFamily.sol:521
function beforeInitialize(address, PoolKey calldata, uint160) external pure returns (bytes4) {beforeInitializeonly blocks pool keys whose hook is PepesFamily; PadToken is a plain ERC-20 with no transfer hooks, so holders can supply it as liquidity to any other venue: a v4 pool with a different fee/tickSpacing/hook, or a v2/v3 pool. Swaps there pay nothing to the protocol or to holders, and once such a pool has depth aggregators will route around the 4%.AUDIT.md invariant 5.2 is stated for launched pools only, so this is a design boundary rather than a code bug, but the docs advertise '4% of every swap, through any router' and this caps those economics. No code fix is possible without changing the token (transfer-level fees), which the design rejects; recommend listing it under section 8 as accepted behaviour. From audit_economics 48737709.
7.infov1 router allowance exemption confirmed safe: no path lets the v1 router move tokens from anyone but its own msg.sender (GoPlus honeypot flag is a false positive)contracts/src/v1/PadTokenV1.sol:105
if (msg.sender != router) {
Work
- posted3 minto the first attempt
- reviewed
#1548Audit permissionsCodexrefusedRefused by Codex's safety filterretried on #1299 (Claude)
This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.
ran oncodex · gpt-6-astra · 2 turns · 2m 29ssubmission2a021dcc0a1a5fff1624358a645384babdd8fe3cee05bc0962ade477c4eb7399device35c52a5b502e847cda633d436a25cd57d809a4ea7935560acc2b18eccfd592acstarted fromd1ad57886d66570471fa11e23cf2b62f3e5aa7e8bundlenonechanged · 0 filesnothing - reviewed
#47Audit flowCodexrefusedRefused by Codex's safety filterretried on #1850 (Claude)
This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.
ran oncodex · gpt-6-astra · 3 turns · 3m 33ssubmission771af6025dbabc3f0abfabc5c819285dd5271e8e8c414f6ca372ce50a5e40759device3f6a9bdd601cb99f6ed43e548c54969af8f5a70edeae432aa541d955a4078cdfstarted fromd1ad57886d66570471fa11e23cf2b62f3e5aa7e8bundlenonechanged · 0 filesnothing - reviewed
#1120Audit mathCodex2 findings · 1 medium
Wrote two validated findings to .imd-findings.json:
- Medium: Partial fills overcharge swap fees.
- Low: Accepted extreme starting ticks prevent new launches.
All 38 existing tests passed. Targeted tests reproduced both defects. Temporary tests were removed; production files remain unchanged.
ran oncodex · gpt-6-astra · 5 turns · 10m 6s · 94.2K in · 8.9K out · 1.4M cachedsubmission361c61d81fa7d5a1d5f2b217dc037ebb524f111bd12ae5123dfc2e08e388594cdevice0256823ae36e790079c99eb46fcdaa245ca2fdeeb213e0b3a63c8e85afb04046started fromd1ad57886d66570471fa11e23cf2b62f3e5aa7e8bundlenonechanged · 0 filesnothingPartial fills retain the fee on the requested quote amount and overcharge traderscontracts/src/PepesFamily.sol:310
When quote is the specified currency, beforeSwap charges the fee on params.amountSpecified and afterSwap reuses that fee without reconciling the executed quote delta. A price limit can substantially reduce execution while the entire fee is still charged, violating the promised 4% of actual gross quote. Exact-output sells have the same root cause: when actual pool quote output is below the precharged fee, poolQuote - fee at line 350 underflows and the swap reverts.
The effect is confined to the affected swap; this does not demonstrate unbacked claims or a PoolManager drain. Calculate the final fee from executed quote and reconcile/refund the unused reservation. If the specified-side hook interface cannot support that reconciliation, explicitly rejecting partial fills is a fallback with a documented loss of partial-fill functionality.
Accepted start ticks can exceed the per-tick liquidity cap and disable new launchescontracts/src/PepesFamily.sol:451
_setStartTick validates tick spacing and TickMath range but does not validate the liquidity required for the fixed 1e27 supply against PoolManager maxLiquidityPerTick. Consequently it accepts configurations under which every new launch for that quote reverts. This affects future launches only and requires an owner configuration change; existing pools are unaffected.
Validate the calculated liquidity for both currency orderings against the per-tick cap before accepting a starting tick.
- reviewed
#2Audit economicsClaude5 findings · 1 medium
The review is complete. The findings file is written at the repository root with five entries, every quoted snippet matches its source line, and no tracked file was changed. The only new files are my scratch tests under
contracts/test/scratch/, which the task discards.What I found, ranked
- Medium: partial fills in the quote-specified swap modes pay 4% of the requested amount, not of what traded.
beforeSwapcomputes and mints the fee fromamountSpecifiedbefore the pool runs. When a price limit stops the swap early, the fee stays. Measured: an exact-in buy of 100 ETH limited 0.01% below spot moved 0.00025 ETH into the pool and cost the trader 4.0002 ETH, a 99.99% effective fee. An exact-out sell at a limit kept 0.0127 ETH for the seller against 0.0833 ETH of fees. This breaks the brief's invariant 2. The sign flip the brief asks about cannot happen, but only because theTradeevent'spoolQuote - feeunderflows and reverts the swap when the fee exceeds the output. A front-runner can induce the partial fill for any trader whose router passes a tight price limit, and holders collect the fee. The project's own routers and the Universal Router use MIN/MAX limits and are unaffected. The proof test in the findings fails on the current code. - Low: a swap with nothing to trade moves the pool price to the curve end for free. With no quote in the pool (every fresh launch, any fully-exited token) a sell-direction swap by an address holding nothing succeeds, exchanges nothing, and leaves
slot0at the extreme price.marketCapdrops from 1.53 ETH to 0, and the website's market cap, price card and chart read that value. The next real buy trades correctly, so no funds are at risk. - Info: the 4% fee only binds the hooked pool. A no-hook v4 pool for the same token trades fee-free; reproduced. A design limitation worth stating in the docs.
- Info:
setStartTickaccepts ticks below about -349,200 that make every launch on that quote revert with a tick-liquidity overflow. Owner-only and reversible, answering the brief's question on extreme ticks. - Info: the v1 router allowance exemption is safe. The only
transferFromthe v1 router issues uses its own caller as the source, and only the PoolManager can reach that path with data the router itself encoded. The GoPlus honeypot flag is a false positive.
Areas checked in depth with no defect found
- Hook delta signs in all four modes and both currency orders, including the transient fee slot handoff and the claim mint that nets the hook's credit to zero.
flushandcollectProtocolFeesinside a foreign unlock: burn plus take nets to zero for the pad, the pad only burns claims it minted, and no caller'ssync/settlewindow is reachable from the hook, so the PoolManager cannot be drained and claim solvency holds.- Launch liquidity rounding: owed tokens never exceed supply minus the buffer in either orientation.
- Reward accounting: corrections and per-share overflow need roughly 1.7e11 ETH of distributed fees to trigger, the exclusion set is static, claim reentrancy is guarded, and payouts cannot exceed what was distributed.
- Permit domain, nonce, expiry and front-run tolerance, router
msg.valuehandling and refunds, and the two-hop ETH route's leftover math.
The brief's accepted behaviours (dividend sniping, fee timing on third-party routers, early fees waiting) were confirmed as described and not reported.
ran onclaude · claude-fable-5-1 · 40 turns · 19m 22s · 610 in · 74K out · 2.7M cachedsubmission35e81b1f74ab316acb96b42dbc65cc9d6e051895660a3b9538639ceea3a79263device468e82a89b9bfe188ef46b055e2ea554bde626cc162630c20effb0fcc7e13fedstarted fromd1ad57886d66570471fa11e23cf2b62f3e5aa7e8bundlenonechanged · 0 filesnothingQuote-specified swaps pay 4% of the requested amount, so a partial fill is charged up to ~100% of what actually tradedcontracts/src/PepesFamily.sol:310
proof · a Foundry test the fix has to passA swap with nothing to trade moves the pool price to the end of the curve for free; marketCap, getTokenInfo and the Trade event then report ~0contracts/src/PepesFamily.sol:331
The 4% fee and holder rewards only bind swaps in the hooked pool; any second pool for the same token trades fee-freecontracts/src/PepesFamily.sol:30
PadToken is a plain ERC-20 with no transfer hooks, so holders can supply it as liquidity to any other venue: a v4 pool with a different fee/tickSpacing/hook (beforeInitialize only blocks keys whose hook is PepesFamily), or a v2/v3 pool. Swaps there pay nothing to the protocol or to holders, and once such a pool has depth, aggregators will route around the 4%.
The brief states invariant 2 for launched pools only, so this is a design boundary rather than a code bug, but it caps the economics the docs advertise ("4% of every swap") and should be stated as an accepted limitation. No code fix is possible without changing the token (transfer-level fees), which the design rejects.
ETH-paired launch; bob buys 5 ETH through the router.
Anyone initialises PoolKey{currency0: ETH, currency1: token, fee: 3000, tickSpacing: 60, hooks: 0} at the current price (succeeds: no hook is consulted) and bob adds full-range liquidity with PoolModifyLiquidityTest (2 ETH + tokens). carol swaps 0.5 ETH exact-in in that pool through PoolSwapTest.
Expected per the docs: 0.02 ETH of fees.
Actual: carol receives tokens, pendingProtocolFees(ETH) and pendingHolderFees(token) are unchanged.
setStartTick accepts ticks beyond about -349,200 for which every launch on that quote revertscontracts/src/PepesFamily.sol:451
owner calls setStartTick(ETH, -400000) (accepted: multiple of 200, within +-887000). Any pad.launch(..., ETH) then reverts (TickLiquidityOverflow from PoolManager.modifyLiquidity). setStartTick(ETH, -340000) followed by the same launch succeeds.
v1 router allowance exemption: no path lets the v1 router move tokens from anyone but its own msg.sender (GoPlus honeypot flag is a false positive)contracts/src/v1/PadTokenV1.sol:105
With the v1 token: carol (holding nothing) calls v1 router.sell(token, bobBalance, 0, deadline): the router calls token.transferFrom(carol, poolManager, bobBalance) and reverts with InsufficientBalance; bob's balance is unchanged.
Calling router.unlockCallback(abi.encode(SwapData{user: bob, ...})) directly reverts with NotPoolManager.
There is no other external function on the router that reaches transferFrom.
- Medium: partial fills in the quote-specified swap modes pay 4% of the requested amount, not of what traded.
- reviewed
#1299Audit permissionsClaude5 findings · 3 low
The findings file is in place and the tree is otherwise clean. Review complete.
Outcome: no critical, high or medium defects found. The findings file holds three low findings and two informational notes, each with a concrete input, and one carries a Foundry proof that fails on the current code.
Findings written to
.imd-findings.json:- Low, hook fee on partial fills. For exact-in buys and exact-out sells the fee is computed on the requested amount in beforeSwap. A trader-chosen price limit yields a 96% effective fee on an exact-in buy and 25% on an exact-out sell. When the delivered quote is below the fee, afterSwap reverts with an arithmetic underflow, which is what prevents the delta from flipping sign. Impact stays with the trader's own swap, as the brief suspected. Proof test included.
- Low, start tick bounds. The owner's setStartTick accepts ticks at which launches revert (ETH ticks at or below -360000 overflow the liquidity cast) or price the launch at a few wei. Owner-only, so a trust assumption, but it is effectively a per-quote pause.
- Low, permit value mismatch. The routers pass exactly tokenAmount to permit, so a signature for a larger value fails and the fallback demands an existing allowance. The NatSpec promises
value >= tokenAmount. - Info, Trade event trader. Non-official routers attribute trades to tx.origin, which misattributes bundler and contract-wallet trades.
- Info, v1 approval shortcut. Confirmed safe: the v1 router only pulls from its own caller and makes no calls to attacker-chosen contracts. The GoPlus honeypot flag is a false positive.
Checked and found sound: every hook callback and unlockCallback is PoolManager-only; launchFor is router-only; the hook pool key cannot be initialised or given liquidity by anyone else; mint and burn of ERC-6909 claims net to zero for the launchpad, so flush and collectProtocolFees inside a foreign unlock cannot drain the PoolManager (a locker calling flush between its own sync and settle only shortchanges itself); the transient fee slot cannot leak between swaps; claim reentrancy is guarded and state is settled before the ETH send; reward payouts stay bounded by accountedBalance; routers only spend msg.sender's assets; permit domain, nonce and deadline handling are correct. Web rendering escapes creator metadata and whitelists URLs.
Limits: no fork tests were run (offline). No stateful invariant fuzzing was added. The scratch tests were removed after use; the proof source lives in the findings file.
ran onclaude · claude-fable-5-1 · 42 turns · 21m 0s · 898 in · 81.4K out · 4.4M cachedsubmission04218bbe3bfab6a117b2b8ab168eaa52f269f0d2687ea723a3f0588b9ae0bd48device98b4506bef931d13cb913bf855d8360596e9aceab2a590f8f5b165a483e76a95started fromd1ad57886d66570471fa11e23cf2b62f3e5aa7e8bundlenonechanged · 0 filesnothingHook fee is computed on the requested amount, so partially filled quote-specified swaps pay far more than 4% (or revert)contracts/src/PepesFamily.sol:310
proof · a Foundry test the fix has to passsetStartTick bounds admit ticks for which _addLaunchLiquidity overflows int128 or the per-tick liquidity cap, bricking launches for that quotecontracts/src/PepesFamily.sol:451
With the test deployment (ETH start tick for a 1.5 ETH market cap), owner calls
setStartTick(address(0), -(TickMath.maxUsableTick(200) - 200))=setStartTick(ETH, -887000): accepted.Then any
pad.launch("X","X","",address(0))reverts (observed in scratch testtest_extremeStartTick_bricksLaunch).Scanning ticks: -340000 still launches, -360000 and below revert (
test_startTickThreshold).Expected:
setStartTickrejects every tick at which a launch cannot succeed.sellWithPermit / sellForEthWithPermit require a permit signed for exactly tokenAmount, contradicting the documented `value >= tokenAmount`; a larger-value permit is rejected with PermitFailedcontracts/src/PepesFamilyRouter.sol:42
Holder dan buys 1 ETH of an ETH-quoted token (balance
bal). dan signs a valid EIP-2612 permit for spender = router, value = 2*bal, nonce 0, deadline = now. dan callsrouter.sellWithPermit(token, bal, 1, now, v, r, s).Actual: reverts
PermitFailed()(scratch testtest_permitSignedForLargerValueIsRejected).Expected per NatSpec: the sale goes through because the signed value is >= tokenAmount.
Trade event attributes third-party-router swaps to tx.origin, misattributing trades made through contract wallets, bundlers or aggregatorscontracts/src/PepesFamily.sol:348
For any swap not sent by
PepesFamilyRouter(including the project's ownPepesFamilyEthRouter, Universal Router, aggregators, ERC-4337 bundlers, Safe/EIP-7702 wallets),traderistx.origin, which is the bundler, relayer or EOA that signed the outer transaction, not the account whose funds moved. The website's trade list and any analytics built on the event therefore show the wrong address for those trades; a relayer that submits many users' trades appears as one whale.No funds are affected; only the event is wrong.
Fix: use
sender(the locker) when the hookData does not carry a trader, and havePepesFamilyEthRouterpassabi.encode(r.user)as hookData and be recognised likerouteris.Swap through v4-core's PoolSwapTest from an EOA-prank (test) or from any contract wallet: the emitted
Trade.traderis the forge default sender / the EOA that started the transaction (observed in the scratch trace:trader: DefaultSender 0x1804c8AB...for a swap made bybobthrough PoolSwapTest). Expected: the account that paid/received the quote (bob, or at least the router contract).v1 router allowance exemption confirmed safe: PadTokenV1.transferFrom can only be exercised by the v1 router on its own msg.sender (GoPlus honeypot flag is a false positive)contracts/src/v1/PadTokenV1.sol:105
- reviewed
#1850Audit flowClaude3 findings · 3 low
Review complete. The findings file is at
.imd-findings.jsonin the repository root with three substantiated Low findings, each carrying a Foundry proof undercontracts/test/scratch/that fails on the current code. No source or configuration files were changed.Findings
- Fee on partial fills (
contracts/src/PepesFamily.sol:310). When the quote is the specified currency,beforeSwapcharges 4% of the requested amount before the pool runs. A swap stopped by its own price limit still pays the full fee. Proof: an exact-in buy of 10 ETH with the limit one unit below spot paid 0.4 ETH and received zero tokens. The exact-out sell variant reverts with a wrapped Panic 0x11 inafterSwapwhen the fill is smaller than the fee. Hook accounting stays balanced and only the trader is affected, which confirms the brief's expectation. Suggested fix is to reject shortfalls in the quote-specified branch ofafterSwap. - Zero-cost price displacement (
contracts/src/PepesFamily.sol:350). On any pool holding no quote, a 1 wei sell from an account with no tokens fills nothing but walks the price to the tick limit. The tick moved from 203000 to 887271,marketCap()dropped to 0, and a zero-amount Trade event was emitted. Trading recovers on the next buy, so this is a display and chart griefing vector only. Suggested fix is to revert zero-fill swaps inafterSwap. setStartTickdead zone (contracts/src/PepesFamily.sol:451). The bound accepts ticks down to -887000, but below about -349200 the launch liquidity exceedsmaxLiquidityPerTickand every launch reverts withTickLiquidityOverflow. Owner-only and reversible. Suggested fix is a tighter bound or a liquidity check inside_setStartTick.
Checked and found sound
- Hook delta signs for all four swap modes in both currency orders, the transient
FEE_SLOThandoff, and claim minting that settles the hook's credit. flushandcollectProtocolFeesinside a foreign unlock. Burn plus take nets to zero for the launchpad, does not touch synced reserves, and is always backed by minted claims.- Launch liquidity rounding in both orientations stays within the supply buffer.
- Holder reward math, correction bounds, exclusion handling, and claim reentrancy. Payouts cannot exceed what was distributed.
- EIP-2612 domain, nonce consumption, and the permit front-run tolerance. The routers only ever pull from
msg.sender, and the two-swap ETH route nets its IMD legs correctly. - The v1 router allowance exemption. The router's
transferFromtarget is always its own caller andunlockCallbackis reachable only through the router's own unlock, so the GoPlus honeypot flag is a false positive.
Coverage notes
No critical, high, or medium defects were found. The 31 unit tests pass. Fork tests and the website were not run or reviewed. Formal invariant fuzzing was not performed.
ran onclaude · claude-fable-5-1 · 32 turns · 20m 50s · 418 in · 82.5K out · 1.8M cachedsubmission2ab9cd5223ddcd25bf490bc86f933df709953e72de575e337f28a179e0346baedevice03f15d1296244279ebdd0e54df271264fe51f911902957fe042ff85c368f0173started fromd1ad57886d66570471fa11e23cf2b62f3e5aa7e8bundlenonechanged · 0 filesnothingHook charges 4% of the requested amount, not the filled amount, when a quote-specified swap stops at its price limitcontracts/src/PepesFamily.sol:310
proof · a Foundry test the fix has to passZero-fill swap on a quote-empty pool moves the price to the tick limit for free; marketCap() then reports 0contracts/src/PepesFamily.sol:350
proof · a Foundry test the fix has to passsetStartTick accepts ticks for which every launch reverts with TickLiquidityOverflowcontracts/src/PepesFamily.sol:451
proof · a Foundry test the fix has to pass
- Fee on partial fills (
- reviewed
#1731Audit judgeClaude7 findings · 1 medium
afterAudit math, Audit permissions, Audit economics, Audit flowReview complete. The findings are in
.imd-findings.jsonat the repository root. Only that file andcontracts/test/scratch/were added; no source or config files changed.What I did. I rebuilt the project (31 unit tests pass), ran all four specialist proofs from
test/scratch/(all five tests fail on the current code for the stated reasons), and wrote my own scratch test to reproduce the remaining claims: the exact-out partial-fill panic, the start-tick brick on both quotes and orientations, the permit value mismatch, the tx.origin attribution, the hookless second pool, and the v1 allowance exemption. I also read the v1 router at commit a549093 to confirm the exemption analysis, and checked that the web page escapes creator metadata.Result: 15 specialist findings merged into 7, none dropped as non-reproducing.
Severity Finding Proof medium Quote-specified swaps pay 4% of the requested amount, so a partial fill at a price limit pays up to ~100% of what traded, or reverts with Panic 0x11 (merged from 4 specialists) yes low Zero-fill swap on a quote-empty pool moves the price to the curve end for free; marketCap reports 0 (merged from 2) yes low setStartTick accepts ticks below about -349,200 that brick every launch on that quote (merged from 4) yes low sellWithPermit only accepts a permit signed for exactly tokenAmount, contradicting the NatSpec no info Trade event attributes third-party swaps to tx.origin no info A second, hookless pool trades fee-free (design boundary to document) no info v1 router exemption confirmed safe; GoPlus flag is a false positive (merged from 2) no Judgment calls worth knowing. I rated the partial-fill issue medium rather than low because it directly breaks the stated 4% invariant with real user loss, and a front-runner can force it on anyone whose router uses a price limit as slippage protection. The Panic at line 350 is the only thing stopping an exact-out seller's quote delta from going negative, so the description warns that the fix must not simply make that subtraction saturating. The start-tick finding is owner-only and reversible, and I say so, but kept it at low because the brief explicitly asked whether an extreme tick can brick launches and describes the bound as protective.
Questions in the brief that produced no finding. Calling flush or collectProtocolFees inside a foreign unlock cannot drain the PoolManager: the burn and take net to zero for the launchpad and the claims are always backed by real currency. The FEE_SLOT handoff is safe because nothing can run between beforeSwap and afterSwap of the same swap. The routers only spend the caller's funds, and the holder-reward arithmetic cannot overflow at any reachable balance.
ran onclaude · claude-fable-5-1 · 30 turns · 9m 23s · 290 in · 40.2K out · 1.1M cachedsubmission78c90a43bb866b9041081865ac7bbee72d7b65c14bb46fc8fbef02c14730b2e1device3c7630b22a73c1fb36d7cccb511d3c400a92c46f4065d9046a3f71b9ce3aa6bestarted fromd1ad57886d66570471fa11e23cf2b62f3e5aa7e8bundlenonechanged · 0 filesnothingQuote-specified swaps are charged 4% of the requested amount, so a partial fill at sqrtPriceLimitX96 pays up to ~100% of what actually traded (or reverts with Panic 0x11)contracts/src/PepesFamily.sol:310
proof · a Foundry test the fix has to passA zero-fill swap on a quote-empty pool moves the price to the end of the curve for free; marketCap(), getTokenInfo() and the Trade/Swap events then report ~0contracts/src/PepesFamily.sol:331
proof · a Foundry test the fix has to passsetStartTick accepts ticks below about -349,200 for which every launch on that quote reverts with TickLiquidityOverflow (and far-positive ticks that price launches at a few wei)contracts/src/PepesFamily.sol:451
proof · a Foundry test the fix has to passsellWithPermit / sellForEthWithPermit only accept a permit signed for exactly tokenAmount, contradicting the documented `value >= tokenAmount`contracts/src/PepesFamilyRouter.sol:42
Holder dan buys 1 ETH of an ETH-quoted token (balance bal). dan signs a valid EIP-2612 permit for spender = router, value = 2*bal, nonce 0, deadline = now. dan calls router.sellWithPermit(token, bal, 1, now, v, r, s).
Expected per NatSpec: the sale goes through because the signed value >= tokenAmount.
Actual: reverts PermitFailed() (test_permitLargerValueRejected in test/scratch/JudgeChecks.t.sol, expectRevert(PermitHelper.PermitFailed.selector) passes).
Trade event attributes third-party-router swaps to tx.origin, misattributing trades made through contract wallets, bundlers, aggregators and the project's own ETH routercontracts/src/PepesFamily.sol:348
For any swap not sent by
PepesFamilyRouterwith a 32-byte hookData (including the project's own PepesFamilyEthRouter, which passes empty hookData, Universal Router, aggregators, ERC-4337 bundlers and Safe/EIP-7702 wallets)traderistx.origin: the relayer or EOA that signed the outer transaction, not the account whose funds moved.The website's trade list and any analytics built on the event therefore show the wrong address for those trades; a relayer submitting many users' trades appears as one whale. No funds are affected; only the event is wrong.
Fix: fall back to
sender(the locker) when hookData carries no trader, and have PepesFamilyEthRouter pass abi.encode(r.user) as hookData and be recognised likerouter. From audit_permissions 58e4b03b.vm.prank(bob, carol) (msg.sender bob, tx.origin carol); bob swaps 1 ETH exact-in through PoolSwapTest on an ETH-quoted pool.
Expected: Trade.trader == bob (or the router contract).
Actual: Trade.trader == carol (test_tradeEventTxOrigin in test/scratch/JudgeChecks.t.sol: expectEmit with trader = carol passes).
The 4% fee and holder rewards only bind swaps in the hooked pool; any second pool for the same token trades fee-free (design boundary, should be stated as accepted)contracts/src/PepesFamily.sol:521
beforeInitializeonly blocks pool keys whose hook is PepesFamily; PadToken is a plain ERC-20 with no transfer hooks, so holders can supply it as liquidity to any other venue: a v4 pool with a different fee/tickSpacing/hook, or a v2/v3 pool. Swaps there pay nothing to the protocol or to holders, and once such a pool has depth aggregators will route around the 4%.AUDIT.md invariant 5.2 is stated for launched pools only, so this is a design boundary rather than a code bug, but the docs advertise '4% of every swap, through any router' and this caps those economics. No code fix is possible without changing the token (transfer-level fees), which the design rejects; recommend listing it under section 8 as accepted behaviour. From audit_economics 48737709.
v1 router allowance exemption confirmed safe: no path lets the v1 router move tokens from anyone but its own msg.sender (GoPlus honeypot flag is a false positive)contracts/src/v1/PadTokenV1.sol:105
- publishedaudit report
- onchain