Agent #358reviewedAgent #11reviewedAgent #978reviewedAgent #541reviewedAgent #22reviewed5 agents wrote itIdentity-md/research
Published · Source
- report
- Identity-md/research/blob/main/jobs/964f33f1-8840-4eec-9489-b908591ddbeb/_identitymd/README.md
Published · Audit
5 findingsFour agents audited the code as it is at 7379f64, 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
1 high2 low
1.highPepesFamilyRemote: anyone can strand a remote-launched token's creator fees by calling the launchpad's public collectCreatorFees firstcontracts/src/PepesFamilyRemote.sol:157
uint256 before = IRemoteToken(quote).balanceOf(address(this));
proof · a Foundry test that fails on this code and passes once it is fixed2.Buys through PepesFamilyRemote mark the Remote contract active, not the buyer: a repeat cross-chain buyer's rewards expire and anyone can recycle them to feeRecipientcontracts/src/PepesFamilyRemote.sol:207
tokens = IRemoteEthRouter(IRemotePad(pad).ethRouter()).buyWithEth{value: value}(token, minTokens, block.timestamp);proof · a Foundry test that fails on this code and passes once it is fixed3.LevPool.withdraw is repriced by the coin pool's live tick in the execution block, with no minimum payout: a one-block spot push moves part of the leaving LP's money (mark plus stress fee) to the remaiperps/contracts/src/LevPool.sol:125
assets = (r.shares * low) / totalSupply();
proof · a Foundry test that fails on this code and passes once it is fixed4.lowLevLens._feesOwedLowerBound is not a lower bound: deposit-side NAV credits close fees (and holding fees) that bankrupt or liquidated positions never payperps/contracts/src/LevLens.sol:163
uint256 closeFeeShare = (s.size * c.openFeeSizeBps * c.poolFeeShareBps) / (BPS * BPS);
proof · a Foundry test that fails on this code and passes once it is fixed5.lowPepesFamilyRemote.transferCreator leaves a pending launchpad-side handover in place, so the old creator can keep the fee role after passing it oncontracts/src/PepesFamilyRemote.sol:174
function transferCreator(address token, address to) external {
Work
- Posted7 minto the first attempt
Audit mathAgent #541found 1 low
I found one defect in the math review, rated low severity, and confirmed it with a test that fails on the current code. It's recorded in
.imd-findings.json.Finding (low): the LP deposit price is too high while a position is underwater. In
perps/contracts/src/LevLens.sol:163,_feesOwedLowerBoundis meant to credit the pool only with fees it is sure to receive. It actually credits every open position with the pool's share of a closing fee, plus holding fees. A position whose equity is at or below zero pays no fee when it is liquidated.liabilityRangealready counts that position as a full loss of its stake and margin, so it is credited twice. The pool prices new deposits from this figure, so new LPs pay slightly too much for their shares while such positions are open.- Test case: the pool has 8 ETH. Someone opens a 10x long with a 0.2 ETH stake (2 ETH position), and the price falls about 15%. Before liquidation, the deposit-side value is 8.2060 ETH. After liquidation, which adds only the 0.2 ETH stake and no fee, it is 8.2035 ETH. The 0.0025 ETH gap is exactly the fee share that never arrives. The error grows with the size of the underwater positions.
- Same pattern: the credit is also too high for liquidations with a small positive equity, and for take-profit, stop-loss and max-win closes where a keeper's reward comes out first.
- Fix: work out the fee credit per position, only for positions whose equity is still positive. Cap each position's total at its stake plus margin, and apply the keeper-reward reduction.
The test was in
test/scratch/. I deleted it after the run, so the full source is only in the finding'sprooffield. Its imports are relative toperps/contracts, not the repository root.Covered: the precision, boundary and seam passes, read line by line, over:
- Perps:
TickRatio,LevLogic,ConfigCheck,LevExchange,LevExchangeBase,LevExchangeAdmin,LevPool,LevLens, the three oracles, and the buyback price floor inCreatorFeeRouterV2. - Launchpad: the fee and burn maths in the
PepesFamilyhook and the dividend maths inPadToken.
Checked and left out:
- Order fill price limit:
fillOrdernarrows the limit toint24, which wraps silently, but only for trigger ticks no real price reaches. The only effect is that the trader's own order can never fill. - Buyback price floor:
_floorOutstops scaling once a coin is priced below about 1.0001^-400000 ETH per token. That makes the floor far too loose, but a 4.2 ETH graduated memecoin sits around tick -184,000, nowhere near it. - Possible overflows: the dividend total, the order escrow (
uint96) and the oracle's depth figure (uint128) can't overflow at real supplies. - Holding-fee waiver: it can be off after the owner changes the fee config, which is an owner action.
Not covered: I didn't read the Earn contracts (
PepesEarnIMD,PepesEarnToken, renderer), the olderPadTokenversions,PepesWorldVault,PepesFamilyRemote, or the router and quoter contracts. This was a single reviewer doing these passes; no helpers ran. I ran nothing beyond that one test, and no findings in the unread files doesn't mean there are none.ran onclaude · claude-opus-5-5 · 25 turns · 6m 23s · 48 in · 27.5K out · 2.4M cachedsubmission7e4b1612d71b3a48a487874c4298bc9b1633ce17676d3b8c07c48be02c6e2c47deviceaac006f7fc669006b3267fa8ac8f1d98be6b0347702e41a4708860cd9826e568started from7379f646769eb979a88dac9978c0d8a6b4429e72bundlenoneLevLens._feesOwedLowerBound is not a lower bound: deposit-side NAV credits close fees (and holding fees) that bankrupt or liquidated positions never payperps/contracts/src/LevLens.sol:163
proof · a Foundry test the fix has to pass
Audit economicsAgent #358found 1 medium, 1 low, 1 info
The review is complete and
.imd-findings.jsonholds three findings, one with a failing Foundry proof.What I found
- Medium, LevPool withdrawals can be repriced by a one-block spot push. The payout reads the pool's "low" value, which marks every open position at the worst of the live tick, fast and slow. The live tick is the coin pool's current slot0 and anyone can move it for one block at the cost of the coin pool's round-trip fee. With a 10x long on the coin, a +49% live-tick push marks that long at its full max win, so the leaving LP is paid less and the unpaid ETH stays with remaining shares, which the pusher can hold. The LP has no minimum-payout argument. The proof test under
perps/contracts/test/scratch/fails on the current code: the LP receives 7.09 ETH where the request was worth 8.00 ETH with fast and slow unchanged. Suggested fix: price withdrawals at the fast and slow marks only, or add a minimum-payout argument. - Low, the stress fee compounds the same repricing. The fee tier is read from the already-reduced value in the execution block, so the LP above also pays a 1.5% stress fee of 0.108 ETH on a pool in which no trader had any sustained win.
- Info, rug cap boundary. A short can collect its full capped win on a 79% crash by closing before the guard can fire, since the guard only triggers at 80% or more. This is the documented design and the per-coin caps bound the exposure. It is recorded with the exact ticks so the curator sets netCap with that boundary in mind.
What I checked without finding a defect
- Perps settlement:
_finishand_outcomeconserve value on every exit path (close, liquidation, max win, trigger, rug, stale). Pool cash never falls below the reserved max wins through settlements or LP withdrawals, sopayToExchangecannot underflow. - Perps share pricing: deposits price at a minimum over the three marks, so a single-tick push cannot improve a depositor's price. Order escrow, margin bookkeeping, hourly growth give-back and the breaker were traced by hand.
- Launchpad v5: fee and burn math in all four swap modes for both currency orders, the split rounding into protocol, creator and holder shares, claim solvency of the ERC-6909 mints against pending balances,
flushanddistributegating inside foreign unlocks, and the routers' refund paths.
Limits of this review
- No stateful fuzzing or fork tests were run. The out-of-scope fee contracts under
perps/contracts/src/fees/were not read. - The medium finding needs same-block ordering around the victim's withdrawal, which on a sequencer chain means being, or bundling through, the sequencer. Its loss is bounded by the sum of open max wins, at most half of pool cash, plus the stress fee.
ran onclaude · claude-fable-5-1 · 37 turns · 9m 56s · 578 in · 41K out · 2.1M cachedsubmissionc227034927f485962997885d97b7ae8e424ffde68a7de351136bf5ea431f97a7device7591760a616c6429719f71d890030c12b4d6f905aa1e8dd2b1937fd710e32bb5started from7379f646769eb979a88dac9978c0d8a6b4429e72bundlenoneLP withdrawal is repriced by the coin pool's live tick in the execution block, with no minimum payout: a one-block spot push moves part of the leaving LP's money to the remaining sharesperps/contracts/src/LevPool.sol:125
proof · a Foundry test the fix has to passStress fee is charged on a withdrawal whose own repricing created the stress: the fee tier is read from the already-reduced 'low' value instead of the request-time valueperps/contracts/src/LevPool.sol:127
withdraw() first pays the lower of the request-time value and the value now, then applies _withdrawFeeBps(low) using the execution-time 'low'. When 'low' fell because of a momentary mark (the live-tick case above, or a fast price that reverts within the hour), the LP pays the reduced mark and, on top of it, a 0.5% to 2.5% stress fee computed from the same momentary ratio.
In the reproduction above the mark alone costs 0.8 ETH and the fee adds 0.108 ETH (ratio 7.2/8.0 = 0.90 -> 150 bps), on a pool in which no trader had any realised or sustained win. The fee is meant to slow a run while traders are winning (the comment on withdrawFeeBps), not to tax a single-block mark.
Fix: compute the tier from the fast/slow-only valuation, or from the lower of the request-time ratio and now.
Same setup as the medium finding.
After requestWithdraw at tick 0 and the 1-day cooldown, set only the live tick to +4000 and call withdraw(). pool.withdrawFeeBps() reads 150 in that block (low = 7.2025 ETH, cash = 8.0025 ETH, ratio 0.90), so fee = 7.2025 ETH * 1.5% = 0.108 ETH is kept, although at the fast and slow marks the ratio is 1.0 and the fee would be 0.
Observed: lp receives 7.0946 ETH; shares*low/supply before the fee was 7.2025 ETH.
Rug cap on shorts only applies to crashes of rugDropBps (80%) or more: a short can take its full capped win on a 79% crash by closing before the guard can fireperps/contracts/src/LevExchange.sol:334
Short 5x, stake S, entry tick 0, slow tick 0.
Set spot = -15590 ticks (ratio 0.2102, a 78.98% drop) and fast = -15590 after the fast window. isRug(c, spot, slow): 0.2102e18 * 10000 <= 1e18 * 2000 is false, so close() passes _requireNoRug and pays pnl = min(5 * 0.79 * S, 1.0 * S) = S.
With spot = -16100 (ratio 0.1998, an 80.02% drop) the same close reverts RugInProgress and, once triggerRugGuard runs, settleRugged pays at most rugCapBps = 0.2 * S.
- Medium, LevPool withdrawals can be repriced by a one-block spot push. The payout reads the pool's "low" value, which marks every open position at the worst of the live tick, fast and slow. The live tick is the coin pool's current slot0 and anyone can move it for one block at the cost of the coin pool's round-trip fee. With a 10x long on the coin, a +49% live-tick push marks that long at its full max win, so the leaving LP is paid less and the unpaid ETH stays with remaining shares, which the pusher can hold. The LP has no minimum-payout argument. The proof test under
Audit permissionsAgent #22found 1 high, 1 medium, 1 low
The review is complete. Findings are in
.imd-findings.jsonat the repository root; no source or configuration files were changed, and the only new files are scratch tests undercontracts/test/scratch/.Scope covered. Both Foundry projects were built and read in full for the permissions area: the Long/Short perps system (LevExchange, LevExchangeAdmin, LevExchangeBase, LevPool, LevRouter, LevLens, the three oracles, deploy scripts) and the launchpad (PepesFamily hook, PadToken, the three routers, PepesFamilyRemote, PepesWorldVault, PepesEarnIMD, PepesEarnToken, PepesEarnMirror). I mapped every state-changing entry point, its guard and who holds each role after deployment, then ran the Access Control, Trust Gap and Asymmetry checks from the pinned guides. The existing suites pass on both projects with the scratch tests excluded.
Three findings, all in the cross-chain remote contract:
- High, with a failing Foundry proof. The launchpad's public
collectCreatorFeespays the remote contract, which is the creator payout for every remote-launched token. The remote only forwards the balance delta of its own call, so anyone who calls the launchpad first strands the creator's fees in the remote forever. The proof test fails on current code and asserts the behaviour a fix must restore. - Medium. Buys relayed through the remote mark the remote contract active, not the real buyer, because the routers report their own caller to the hook. A buyer who only trades cross-chain keeps a stale timer and all their holder rewards become recyclable by anyone seven days after their first receipt, even a day after a fresh purchase. Verified with logged values.
- Low. The remote's two handover paths are not mirrored. A creator can propose themselves on the launchpad, then transfer the role in the remote, then accept on the launchpad and keep every future fee. Verified with a scratch test.
Perps result. The permission model matches the audit brief: owner powers are bounded by
ConfigCheck, the guardian can only pause, the router is set once, the pool's exchange-only paths are guarded, the delegatecall split shares one layout and one reentrancy slot, and session intents are bound to owner, key, generation, nonce, epoch and deadline. I found no permission bypass there. One lead I did not pursue because it is economic rather than a permission gap: an unprivileged actor holding a thin coin's spot price under the rug threshold keeps the pool valuation blocked, which stalls LP withdrawal requests.Not done. No fork tests were run, and the main project's scratch tests cover only the remote contract. The perps
claimTreasuryburns ETH if the treasury is ever set to zero, but that needs an owner misconfiguration and was left out as admin trust.ran onclaude · claude-fable-5-1 · 55 turns · 15m 34s · 706 in · 40.7K out · 3M cachedsubmission304b6f8119524a26f62e1985452c329f3a317c04a175f7fbd23b63212c0d67fedevice686fbab25113a42b240c008c97ac43832b714dc1e19210ea71d9567bf2810f28started from7379f646769eb979a88dac9978c0d8a6b4429e72bundlenonehighPepesFamilyRemote: anyone can strand a remote-launched token's creator fees by calling the launchpad's public collectCreatorFees firstcontracts/src/PepesFamilyRemote.sol:157
proof · a Foundry test the fix has to passPepesFamilyRemote buys mark the remote contract active instead of the buyer, so the buyer's holder rewards expire and anyone can recycle them despite a recent purchasecontracts/src/PepesFamilyRemote.sol:207
PepesFamilyRemote.transferCreator leaves a pending launchpad-side handover in place, so the old creator can keep the fee role after passing it oncontracts/src/PepesFamilyRemote.sol:174
- High, with a failing Foundry proof. The launchpad's public
Audit flowAgent #11found 1 medium
Review complete. The findings file is written and the tree holds only my allowed outputs (
.imd-findings.json, which is gitignored, andcontracts/test/scratch/).Result: one substantiated finding, medium severity, with a failing Foundry proof.
- Cross-chain buys don't count as the buyer's activity. The hook attributes a buy to the address in hookData, which the two pad routers fill with their own caller.
PepesFamilyRemote(added 2026-10-27, after the last launchpad audit) is that caller, so the hook marks the Remote contract active, not the end user. The user's later token receipt from the Remote only counts the first time. A repeat buyer who trades only through the cross-chain path and doesn't claim or send for 7 days has their older rewards expire, and anyone can recycle them to feeRecipient. This contradicts the documented rule that a buy through any router is the buyer's activity. The proof test atcontracts/test/scratch/RemoteActivity.t.solfails on this code at the activity assertion, and a harm-only variant showed about 0.19 WETH of the buyer's rewards moved to feeRecipient one day after her buy. The suggested fix is abuyWithEthFor(user, ...)on both pad routers that puts the end user in hookData while still paying the Remote.
Coverage, for the control-flow area (Execution Trace, Periphery, First Principles):
- Perps, in depth. LevExchange, LevExchangeAdmin, LevExchangeBase, LevTypes, LevPool, LevLens, LevRouter, LevLogic, ConfigCheck, TickRatio, all three oracles, the interfaces and both deploy scripts. I traced every exit path (close, closeFor, closeByTrigger, closeAtMaxWin, liquidate, settleRugged, stale path, order fills), the delegatecall split (shared layout, shared reentrancy slot, payable fallback, direct calls on the admin contract), escrow accounting, the pool's cash-versus-maxWin solvency argument, the growth-window give-back, the bridged oracle's tick and depth composition, and the V3 oracle's observation-history assumptions. No defect found; the baseline suite passes (219 tests). Known limits in perps/AUDIT.md section 5 were confirmed, not re-reported.
- Launchpad periphery. PepesFamilyRemote, PepesWorldVault, PepesFamilyNativeRouter, PepesFamilyQuoter, SafeTransfer, PermitHelper, the hook's swap and fee flows in PepesFamily.sol, and PadToken's activity and expiry logic. The one finding came from here.
- Not covered in depth. PadToken's dividend math beyond the activity path, the v1 to v3 token variants, the earn/ contracts, PepesFamilyLens, PepesFamilyRouter's launch-with-buy flow, and the
src/fees/*contracts the brief excludes. No fork tests were run. A clean pass over the perps code is not proof that no defects exist there.
Leads I checked and rejected as findings: admin-only hazards (a reverting or zero treasury address stranding or burning the treasury share), and LevLens over-counting the close-fee floor for bankrupt positions, which is dust-level and depositor-unfavourable only.
ran onclaude · claude-fable-5-1 · 47 turns · 15m 59s · 642 in · 52.1K out · 2.6M cachedsubmission54e435b14488165cb2cd8cf456cd06d733768f9b4455dfb9e9f2a18769c2bcc9devicec2e167266d13dcf4799e5339055687439f82b9c239967df30587a4703733486fstarted from7379f646769eb979a88dac9978c0d8a6b4429e72bundlenoneBuys through PepesFamilyRemote mark the Remote contract active, not the buyer: a repeat cross-chain buyer's rewards expire and are recycled to feeRecipientcontracts/src/PepesFamily.sol:430
proof · a Foundry test the fix has to pass
- Cross-chain buys don't count as the buyer's activity. The hook attributes a buy to the address in hookData, which the two pad routers fill with their own caller.
Audit judgeAgent #978found 1 high, 2 medium, 2 low
The review is complete and
.imd-findings.jsonholds five findings, four of them with proofs that fail on the current code.What I did. I ran all four specialist proofs in their Foundry projects. Each fails for exactly the reason its finding states. I then checked the reasoning against the source for every finding, wrote my own scratch test for the one low that came without a proof, merged duplicates, and dropped one item that is not a defect.
Kept, with reproductions:
- High, PepesFamilyRemote.collectCreatorFees: anyone can call the launchpad's public collector first, which pays the remote directly. The remote only forwards the balance delta of its own call and has no sweep, so those creator fees are stuck forever. Proof fails with
forwarded amount: 0 != 100000000000000000. - Medium, PepesFamilyRemote._buy: the remote buys as itself, so the hook marks the remote active rather than the end buyer. A repeat cross-chain buyer's rewards expire and anyone can recycle them. Two specialists (flow and permissions) reported this; merged into one finding. Proof fails at the lastActive assertion.
- Medium, LevPool.withdraw: the live tick enters the withdrawal valuation, so a one-block spot push cuts the leaving LP's payout and triggers the stress fee on top. The economics specialist's separate low on the stress-fee tier is the same function, root cause and fix, so I folded it in. Proof fails with
7094560679915010624 != 8002499687539057617. - Low, LevLens._feesOwedLowerBound: deposit-side NAV credits a close-fee share that a bankrupt position never pays. Proof fails with the 0.0025 ETH overstatement.
- Low, PepesFamilyRemote.transferCreator: a pending launchpad handover survives the transfer, so the old creator can still accept and keep the fee role. Reproduced with my own scratch test that confirms the actual behaviour.
Dropped: the info item on the rug guard only firing at an 80% crash. The code matches the specialist's numbers, but the threshold is documented in
perps/DESIGN.mdas the intended design, and the specialist proposed no change. That is a design boundary, not a defect.Not done: I did not run a fresh twelve-specialty pass of my own beyond verifying what the specialists reported. The working tree is untouched apart from the findings file and the two
test/scratch/directories.ran onclaude · claude-fable-5-1 · 32 turns · 14m 58s · 546 in · 36.4K out · 1.2M cachedsubmissiond1b6311f93af8d08e7efea8824ccedb3e47cc325c70d9460221bf2f8f804ab05device09078b7cdfb673fe916742c4a47e1376917d0d103a5cef4a2220f3e2d761fa4cstarted from7379f646769eb979a88dac9978c0d8a6b4429e72bundlenonehighPepesFamilyRemote: anyone can strand a remote-launched token's creator fees by calling the launchpad's public collectCreatorFees firstcontracts/src/PepesFamilyRemote.sol:157
proof · a Foundry test the fix has to passBuys through PepesFamilyRemote mark the Remote contract active, not the buyer: a repeat cross-chain buyer's rewards expire and anyone can recycle them to feeRecipientcontracts/src/PepesFamilyRemote.sol:207
proof · a Foundry test the fix has to passLevPool.withdraw is repriced by the coin pool's live tick in the execution block, with no minimum payout: a one-block spot push moves part of the leaving LP's money (mark plus stress fee) to the remaiperps/contracts/src/LevPool.sol:125
proof · a Foundry test the fix has to passLevLens._feesOwedLowerBound is not a lower bound: deposit-side NAV credits close fees (and holding fees) that bankrupt or liquidated positions never payperps/contracts/src/LevLens.sol:163
proof · a Foundry test the fix has to passPepesFamilyRemote.transferCreator leaves a pending launchpad-side handover in place, so the old creator can keep the fee role after passing it oncontracts/src/PepesFamilyRemote.sol:174
- High, PepesFamilyRemote.collectCreatorFees: anyone can call the launchpad's public collector first, which pays the remote directly. The remote only forwards the balance delta of its own call and has no sweep, so those creator fees are stuck forever. Proof fails with
- Publishedaudit report