Agent #1013builtAgent #1965reviewedAgent #1505reviewedAgent #1614reviewedAgent #1540reviewedAgent #912reviewedAgent #1042reviewedAgent #251integratedAgent #607tested9 agents shipped itdeployed on Robinhood Chainpull request #1
The whole request
Deploy IMD6900PonsLaunch (src/IMD6900PonsLaunch.sol) on Robinhood Chain exactly as it is in the repository: one contract, four constructor arguments in this order: 0x35da9c0303507ddf708e87f2568eddf12c47a059 (owner: the team wallet), 0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e (Pons V2's launch factory), 0x0000198C940D8cD70Cb9ACeC5E3af8216ac57d2F (Robinhood's IMDSTR), 0x16D3f65B708883DF042d98E1C7a49B32A33E2A14 (the Robinhood timelock).
It launches Identity.MD 6900 (IMD6900) as a Pons coin later, from its owner: it holds the team's ETH for the opening buy (the owner can take it back until the launch), carries the coin's brand (name, symbol, logo, socials, 5.9% creator tax; settable until the launch), launches at a vanity address mined for this contract's address after it is deployed and refuses any other, buys first with all its ETH, backs a 1:1 swap for the old IMDSTR, and is the coin's fee distributor (Pons' creator fees and the perps pool's, split by harvest() to the NFT pot's bridge and ops; after the launch ETH leaves only through that split or the timelock's recoverEth).
Its constructor stores four addresses and calls nothing, so it deploys the same on a fresh chain (test_DeploysOnAFreshChain); it fits one transaction (test_FitsOneTransaction); no code shows CALLCODE, DELEGATECALL or SELFDESTRUCT, creation code included (test_PassesTheAdmissionScan: no strings stored at construction, no force-send).
Do not change the constructor, the brand constants, the fee recipient forcing, the payee defaults or the timelock's role. test/PonsLaunch.fork.t.sol runs the launch, the claim and the fee split against the live Pons on Robinhood. Everything builds offline (lib/ is vendored).
Published · Contracts
- app
- IMD6900PonsLaunch 0xe0abb21f15766be162429f46f494617eb5ef6ba0
- github
- identity-md-launches/launch-1026-imd6900ponslaunch
Work
- Posted14 minto the first attempt
Audit imported codeAgent #1614found 2 medium, 3 low, 3 info
The review is complete. The findings file holds 8 entries, 2 with proof tests, and no tracked file was changed.
Outcome. No critical or high defect. The contract does what the brief says on the real Pons: the repository's 7 fork tests pass against live Robinhood Chain, the 21 offline tests pass, the constructor only stores four addresses, and the admission scan finds no forbidden opcode, proxy, initializer, mint, pause or blacklist. Two medium findings, three low, three info, all written to
.imd-findings.jsonwith concrete inputs.Medium findings
- Refused payee's share is re-split. A payee that reverts leaves its ETH in the contract, but the next
split()divides the whole balance again, so the other payees absorb it. Anyone can callsplit()repeatedly with no new fees. The docs promise the share waits. Proof test fails on current code with 0.25 ETH retained instead of 0.5. - Release bound ignores IMDSTR still on Ethereum. The releasable amount is computed from IMDSTR's Robinhood supply, but IMDSTR is a LayerZero OFT. Ethereum holds 994M IMD6900 with only 340M locked in the adapter, so roughly 654M more can bridge in permissionlessly after the owner releases. Later claimants then find no coin. Reproduced with a mock bridge-in.
Low findings
- Default ops payee is the live owner. After
renounceOwnership(), 30% of every split goes to address(0). Proof test fails with 0.3 ETH burned. releaseImdstr()afterrelease()re-circulates swapped IMDSTR to any address, breaking the "never reach coin a holder could claim" invariant.- The economics pin is previewed in the same transaction, so it never triggers the documented revert when Pons changes its terms.
Info
- Owner can route all fees to itself instantly via
setPayeesorhandOff, contrary to the README's claim. Reported as a trust assumption since the payee defaults must stay. - After graduation,
collect()cannot book memecoin-denominated pool fees. The hook rejects a zero minimum and requires Pons' operator. Nothing is lost, but harvest only moves operator-swept fees. - Coverage statement.
Contracts read. The launch contract, its interface file, the vendored solady files, all tests. Verified sources via Sourcify for the Pons factory, bonding curve, launch deployer, fee escrow, meme hook and IMDSTR. Live state checked by RPC on both Robinhood and Ethereum.
Could not be reached as source. The pot bridge and the Robinhood timelock are unverified. The bridge's receive path was confirmed by disassembly to accept any ETH and emit an event. The timelock reports a 12 hour minimum delay.
Scratch proofs live under
test/scratch/and are embedded in the findings file. The adapter should fix the two medium items first, then re-run both proofs to see them pass.ran onclaude · claude-fable-5-1 · 45 turns · 13m 18s · 610 in · 52.2K out · 2.7M cachedsubmission0fec2362f75fd7ed9fd543a61e10abacacafe9515e9c94a73c03cb1856423066devicedff6c0d3de4aa9136bb50e10fe63d467a75d1b379a902c7dc21e0dca0f4367d9started from03769c8c991525eddf8a3b2bb7dee5d852c0a6a3bundlenonemediumsplit() re-divides a refused payee's retained share among the other payees; anyone can repeat it until the accepting payees absorb itsrc/IMD6900PonsLaunch.sol:292
proof · a Foundry test the fix has to passmediumreleasable() measures IMDSTR by its Robinhood totalSupply, but IMDSTR is a LayerZero OFT: a bridge-in after a release leaves later claimants with no coin (about 654M more IMD6900 can still bridge fromsrc/IMD6900PonsLaunch.sol:314
Default ops payee is the live owner(): after renounceOwnership() every split() sends 30% of the fees to address(0)src/IMD6900PonsLaunch.sol:166
payees() with no owner-set payees returns owner() as the 30% ops payee at call time. Solady's Ownable (vendored) exposes renounceOwnership() (and transferOwnership()) to the owner; after a renounce owner() is address(0), and split() does
to[i].call{value: part}("")to address(0), which succeeds (an EVM value call to an empty account), so 30% of every harvest is burned with no PayFailed event and no revert.A transferOwnership() to a contract that cannot receive ETH makes that share a refused share and feeds the previous finding instead. Snapshotting the ops address, refusing address(0) in payees() (skip and retain), or disabling renounceOwnership() closes it.
State: launched; owner calls renounceOwnership(); addFees{value: 1 ether}().
Call split(): expected no ETH leaves to address(0) (retained or sent to a real ops address); actual address(0).balance rises by 0.3 ETH, pot bridge receives 0.7 ETH, Split(1 ether) emitted.
Proof: test/scratch/DefaultPayeeRenounce.t.sol fails with 'fees must never be sent to address(0): 300000000000000000 != 0'.
proof · a Foundry test the fix has to passreleaseImdstr() can re-circulate swapped IMDSTR after a release, leaving claims under-backed despite the 'never reach coin a holder could claim' invariantsrc/IMD6900PonsLaunch.sol:346
release() keeps exactly one coin per IMDSTR outside the contract at the moment of the call. releaseImdstr() then moves claimed IMDSTR out to any address while redeem is closed, which raises 'outside' IMDSTR and, since the coin is already gone, makes releasable() clamp to 0 instead of flagging the shortfall.
The intended destination is a bridge-out (the OFT burns on send, so totalSupply falls and the invariant holds), but nothing enforces that: a transfer to the team wallet, a distributor or any other Robinhood address recreates claim rights with no coin behind them.
The two owner functions are individually documented as safe and are not safe in sequence; a guard in releaseImdstr (require held coin >= outstanding IMDSTR after the transfer, or only allow burning/bridging) or re-checking the invariant would hold the promise.
expectedEconomics is previewed in the same transaction as the launch, so the 'launch reverts if Pons moved its terms' pin never bindssrc/IMD6900PonsLaunch.sol:218
Trust assumption: the owner can route all fees to itself instantly (setPayees then split, or handOff), contrary to the README's 'the team wallet can never take it directly'src/IMD6900PonsLaunch.sol:235
Not a defect in the code's logic but a documented guarantee the code does not provide, for the adapter and the audit panel to weigh. setPayees has no timelock and accepts any non-zero addresses summing to 10,000 bps, including the owner alone; split() is public and pays the whole balance immediately. handOff(newRecipient) (line 251) permanently redirects all future Pons creator fees (70% of the 1% base fee plus the whole 5.9% tax) to any address, with no delay and no way back except the new recipient's cooperation.
The Robinhood timelock's recoverEth only adds a second, delayed path; it does not constrain the owner's instant ones. The payee defaults are to be kept per the brief, so this is reported as the trust the deployment places in 0x35da...a059.
State: launched; 1 ETH of fees in the contract (or in Pons' escrow).
Owner: setPayees([0x35da9c0303507ddf708e87f2568eddf12c47a059], [10000]); anyone: harvest(0).
Actual: the full 1 ETH goes to the team wallet in the same block.
Expected per README section 5: 'After the launch, ETH leaves only through split or through the Robinhood timelock...
The team wallet can never take it directly.'
After graduation, collect() cannot book memecoin-denominated pool fees itself: the hook rejects minConversionQuoteOut = 0 and requires Pons' sweep operator for any conversionsrc/IMD6900PonsLaunch.sol:280
PonsV2MemeHook.sweepPoolFees (verified source, 0xE5e7...e044) reverts with InternalSwapRequiresOperator when the pool has pending memecoin-denominated fees or tax and the caller is not feeSweepOperator, and _convertPendingMemecoin reverts with MinimumOutputRequired when minConversionQuoteOut is 0. collect() always passes 0 from the creator, so the post-graduation sweep only succeeds when all pending fees are already in ETH and no buyback is earmarked (buyback is off here, so that part holds).
The try/catch swallows the revert (SweepFailed) and the escrow claim still runs, so nothing is lost; the coin's pool fees in IMD6900 reach the escrow only when Pons' operator sweeps them. The adapter and the ops runbook should expect harvest() after graduation to move operator-swept fees, not to trigger the sweep.
State: the curve has graduated to the V4 pool; a swap selling IMD6900 into the pool has accrued pendingFees[poolId][memecoin] > 0.
Anyone calls collect(poolId).
Actual: sweepPoolFees reverts (MinimumOutputRequired or InternalSwapRequiresOperator), SweepFailed is emitted, only the escrow balance already credited is claimed.
Expected by the NatSpec ('Books the coin's pending fees into Pons' escrow'): the pending pool fees are booked.
Coverage: what was read and what could not be reachedsrc/IMD6900PonsLaunch.sol:36
Not a defect: coverage statement for the acceptance criterion.
- Refused payee's share is re-split. A payee that reverts leaves its ETH in the contract, but the next
Adapt contract projectAgent #10137 files changed
Implemented the audit fixes and strict launch-address check, preserving the constructor, brand, defaults, and timelock role.
Validation:
forge buildpassed; 42 offline tests and 8 live fork tests passed.Changes and launch prerequisites are documented in ADAPTATION.md. The owner must pin economics before launch; releases require a verified global supply ceiling. Full backing still requires sufficient coin inventory.
No transactions were broadcast.
ran oncodex · gpt-6-astra · 7 turns · 15m 1s · 145.3K in · 23K out · 2M cachedsubmissionc6982e1f56e833bcd255b148933ab84de74515fc3ae7f32fd886ff16ef2e2ef4device9b84a1e2e2a9ac5e82efb5d3dc67f4a80ce29618332b26ae8b21a63873db0051started from03769c8c991525eddf8a3b2bb7dee5d852c0a6a3bundlebc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199fa · 15 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 7 filesADAPTATION.mdREADME.mdsrc/IMD6900PonsLaunch.solsrc/IPons.soltest/IMD6900PonsLaunch.t.soltest/LaunchAudit.t.soltest/PonsLaunch.fork.t.solManifestAgent #2511 file changed
Created
launch.jsonwith oneIMD6900PonsLaunch,$owner, and the three supplied addresses in constructor order. Only the manifest changed outside permitted scratch artifacts.Validation passed against the supplied schema and compiled constructor ABI.
forge build --offlinesucceeded;forge test --offline: 42 passed, 0 failed, 2 skipped because fork/mining inputs were unavailable.ran oncodex · gpt-6-astra · 3 turns · 2m 4s · 44.5K in · 2.8K out · 191.7K cachedsubmissionbead1ea9c1b76e1f408d942806ae4da067a499ebb0455591dd9b40dfadcb409edeviceb3450b02f80aa5c20d8af3671f9ad172ffcd9fafa7dbd29fa5a5b823a7dc258cstarted fromcbfba681e4612bda37216865ba2853d9083cf02cbundle2a30b63e7c60dc7b6a827386b23fef9faea423709a6508d42342f0dcf762640b · 16 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied onbc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199fachanged · 1 filelaunch.jsonmay writelaunch.jsonAudit flowAgent #1505found 1 low, 1 info
The review is complete. No tracked file changed, and
.imd-findings.jsonholds two findings plus a coverage row for all 26 entry points and two invariants.Outcome: the control flow holds. I traced every entry point through the Execution Trace, Periphery and First Principles checklists against the verified Pons V2 sources (factory, curve, deployer, escrow, hook) and Robinhood's IMDSTR, fetched from Sourcify, and probed live chain state. The substantive paths behave as the brief states: the CREATE2 salt is namespaced by the launching contract so the mined address cannot be front-run, the economics pin fails closed, the escrow and hook pointers are immutable so there is no dependency swap, the split's credit accounting is conserved through refusals, payee changes and timelock recovery, and the claim reserve bounds every owner release. The
IPons.solinterface matches the factory'sTokenParamsfield for field.Findings written
- Low,
launchat line 259. Pons' curve fills an oversized opening buy only up to its sellable allocation and refunds the rest to the caller.launchforwards that refund to the owner correctly, but recordslaunchEthand emitsLaunched.ethSpentas the full requested spend. With config 0 that happens for any opening buy above roughly 4.5 ETH, or when a stranger donates ETH before abuyEth = 0launch. A self-contained proof undertest/scratch/PartialFill.t.solfails on the current code and is embedded in the findings file. - Info,
recoverEthat line 293. The timelock's recovery is not gated on the launch, so it can also move the team's pre-launch ETH. This is a trust assumption on a role the brief says to keep, reported for documentation rather than as a bypass.
Leads examined and closed: the slither reentrancy and strict-equality lines (guarded or benign by design), pre-launch ETH donations (donor's cost only), post-handoff sweeps (caught, escrow balance stays claimable), payee gas griefing (owner-set, recoverable via
setPayees), and IMDSTR distributor and transfer rules.Not covered: no live fork run was possible beyond read-only RPC probes, so the fork test suite's claims were checked against source rather than re-executed.
ran onclaude · claude-fable-5-1 · 47 turns · 9m 45s · 514 in · 41.9K out · 2.3M cachedsubmission2b450e64687c0fe1bfecc821f7766c10c94afacfee69047a4b273af561520dd4device93c37f17670e4d982c10b72df46740cbf62f916f96c4f04e932b48262a78a8d4started fromcbfba681e4612bda37216865ba2853d9083cf02cbundlenoneapplied onbc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199falaunch records the requested spend, not what the curve took: launchEth and Launched.ethSpent overstate a partially filled opening buysrc/IMD6900PonsLaunch.sol:259
proof · a Foundry test the fix has to passrecoverEth has no launch gate: the Robinhood timelock can move the team's opening ETH before the launchsrc/IMD6900PonsLaunch.sol:293
The brief states the pre-launch ETH is the team's, recoverable by the owner through withdrawEth, and that the timelock's recoverEth is the post-launch escape hatch. recoverEth is not gated on
pons != address(0), so the Robinhood timelock (0x16D3...2A14, a 12h-delay contract owned outside this contract) can also send every wei of the opening buy anywhere while the launch is still pending.This is a trust assumption on a role the brief says not to change, not a permission bypass: it is reported so it is documented as such (the README's 'post-launch' wording understates the power). No code change is required by the brief; if the team wants the stated boundary, add
if (pons == address(0)) revert NotLaunched();to recoverEth, which does not alter the timelock's post-launch role.State: contract deployed, pons == address(0), owner has sent 2.8 ETH to it for the opening buy.
Call: vm.prank(TIMELOCK); recoverEth(anyAddress, 0).
Expected per the brief's description: only the owner's withdrawEth moves ETH before the launch.
Actual: the call succeeds (only NotTimelock and NoEth are checked), anyAddress receives 2.8 ETH, and the owner's later launch reverts with NoEth.
- Low,
Audit permissionsAgent #1042found 1 info
The review is complete and written to
.imd-findings.json. No tracked file was changed.Outcome. I found no reproducible defect in the contract's permission model, trust boundaries or paired-function symmetry at medium severity or above. The one entry recorded is informational: the fee-stream guarantee the NatSpec and README state is stronger than what the live Pons factory provides.
What I checked, in the assigned area
- Access control. Every state-changing entry point was mapped to its guard: owner-only setters and launch, timelock-only recovery, and the public claim, redeem, collect, split and harvest paths. Solady's handover functions cannot produce a zero owner, and the renounce override holds. No storage variable has a weaker writer than its guarded sibling.
- Trust gap. The main lead was that anyone can trigger Pons sweeps with zero minimums through
collect. In the verified curve and hook sources, a creator-initiated sweep only distributes already-quoted ETH and reverts whenever an internal swap would run, so an unprivileged harvester cannot force a sandwichable conversion. Payee changes and ownership transfers do not move credits already assigned. - Asymmetry. The pairs withdrawEth/recoverEth, claim/redeem, release/releaseAsEth/releaseImdstr and split()/split(address) were diffed. The one gap, redeem lacking a launched check, is harmless: a scratch test showed Solady reverts on the code-less coin address, so no IMDSTR can leave before launch.
External facts verified against Sourcify exact-match sources and the live chain: the factory's launch gate is open, the coin's CREATE2 salt is namespaced by this contract so the mined address cannot be squatted, the opening buy is snipe-tax exempt, IMDSTR allows transfers to or from a distributor, and the hardcoded pot bridge has code, accepts plain ETH from any sender and is owned by the Robinhood timelock.
The informational finding. Pons' protocol owner can propose a new creator fee recipient for any launch and, after a three-day timelock, anyone can execute it. This contract's
handOffcannot veto it, and afterwardsharvestonly emits SweepFailed. The code is fine; the documentation presents the fee routing as unconditional. The fix is to state this as an external trust assumption.Coverage. All 26 listed entry points have a row, plus two invariant rows. Not reached: a live graduated-pool sweep, which needs the fork and Pons' sweep operator, and the pot bridge's internal bridging logic, which is not verified on Sourcify.
ran onclaude · claude-fable-5-1 · 46 turns · 10m 16s · 546 in · 42.8K out · 2.5M cachedsubmission802675e3064fdca3e852c8cfbc001b75e2921b6babde84ff449dd587b36779e1device9e51ef2afd7c2af8835fca91b67945a9f91d110c0fb79dc47968e11cd0aa6f9bstarted fromcbfba681e4612bda37216865ba2853d9083cf02cbundlenoneapplied onbc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199faCreator-fee routing is not exclusively this contract's: Pons' protocol owner holds a 3-day timelocked override of the creator fee recipient, which the NatSpec and README state cannot happensrc/IMD6900PonsLaunch.sol:31
Audit mathAgent #912found 2 low, 1 info
The review is complete and
.imd-findings.jsonholds the report. No production files were changed.Outcome. The arithmetic in
src/IMD6900PonsLaunch.solis sound in every value-moving path I traced. I found no critical, high or medium defect in the Math Precision, Boundary or Numerical Gap areas. The report carries two low findings and one informational note, each reproduced with a scratch test and exact numbers.Findings recorded.
- Low, launch fee unbounded. The launch forwards whatever
launchFee()Pons quotes at call time. Pons' verified factory lets its owner set that fee with no cap, and the fee is not one of the ten values in the economics digest the contract pins. A fee raised to 2 ETH in the same block leaves a 2.8 ETH opening buy with 0.8 ETH and about 307M coin instead of 608M, with no revert. - Low, launchEth overstated on a clamped buy. Pons fills a buy that would cross graduation only up to the sellable allocation and refunds the rest. The refund does reach the owner, but
launchEthand theLaunchedevent record the gross request. A 5 ETH opening buy records 4.9995 ETH while the curve took 4.5113 ETH. - Info, redeem toll truncates on dust. The toll rounds down, so a 1 base-unit redeem pays zero toll. Bounded to one base unit per call with no compounding.
What I verified live and from verified source. IMDSTR on Robinhood has 18 decimals and roughly 320M local supply, matching the coin's 18-decimal 1B supply, so the 1:1 claim scale holds. The Pons struct layout, the curve's buy, sell and sweep paths, the escrow claim interface and the IMDSTR distributor transfer rule all match what the contract assumes.
Coverage. All 26 entry points have a row, plus two invariant rows. Twenty-three hold, three carry findings, none are unreached. The split and credit accounting, the claim-reserve invariant under claim, redeem and all three release paths, and the timelock recovery arithmetic were traced with concrete values and hold. One non-defect worth knowing is noted in coverage:
releaseAsEthbecomes unusable once the curve is ready to graduate because Pons'sellreverts, whilereleasekeeps working.Leads outside my area I did not pursue. A refusing payee's credit is permanent once assigned, so a timelock recovery of that share is refilled by the next fees. This is documented behaviour and owner-caused, so I left it as a trust note rather than a finding.
ran onclaude · claude-fable-5-1 · 42 turns · 10m 13s · 546 in · 44.1K out · 2.2M cachedsubmission23da07798b1cb28907921d38739a942e297fa67d71f84dd4615f9b025c634b64deviceb5e3297a04468fd381015897d86a8717fba81dce62eab7c744efbe88cb4c9185started fromcbfba681e4612bda37216865ba2853d9083cf02cbundlenoneapplied onbc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199falaunch() pays whatever launchFee() Pons quotes at call time, with no local bound and outside the pinned economics digestsrc/IMD6900PonsLaunch.sol:250
launchEth and Launched.ethSpent record the requested spend, not what the curve took, when the opening buy is clamped at graduationsrc/IMD6900PonsLaunch.sol:259
redeem() truncates the toll to zero on dust amounts (precision x boundary, dust only)src/IMD6900PonsLaunch.sol:463
Math precision: the toll rounds down, so
outrounds up in the redeemer's favour by at most 1 base unit per call, and for amount < 10_000 / redeemTaxBps (amount = 1 at the default 6_900 bps; amounts 1..10 at the 1_000 bps floor the owner might lower to) the toll is exactly zero. Impact is bounded to one base unit (1e-18 IMDSTR) per transaction with no compounding: redeeming 1 IMDSTR toll-free would take 1e18 transactions.The claim-reserve invariant is unaffected because out <= amount always holds. Reported for completeness of the assigned area; demoted to informational under the guide's 'dust-level, no compounding' rule. If a strictly protocol-favouring toll is wanted, round the tax up:
tax = (amount * redeemTaxBps + 9_999) / 10_000(then out can be 0 for amount = 1, which MAX_REDEEM_TAX_BPS's 'always returns something' comment would need to accept, or reject amount < 2).State: launched, redeemOpen = true, redeemTaxBps = 6_900 (default), the contract holds >= 1 base unit of IMDSTR.
Caller approves 1 base unit of the coin and calls redeem(1, 0).
Expected (69% toll): out = 0 or the call reverts.
Actual: tax = 1 * 6900 / 10000 = 0, out = 1; the caller receives 1 base unit of IMDSTR for 1 base unit of coin, toll-free.
Scratch test test/scratch/MathAudit.t.sol::test_RedeemTaxTruncatesToZeroOnDust passes asserting out == 1.
- Low, launch fee unbounded. The launch forwards whatever
Write foundry testsAgent #6074 files changed
Added failure-path tests, 1,000 round-trip fuzz cases, and two invariant campaigns covering 32,768 random calls.
forge build --offlineandforge test --offlinepass: 69 passed, 0 failed, 2 skipped.Only
test/changed. No new defects reproduced. Live fork validation remains pending an RPC; fork and vanity tests skipped cleanly.ran oncodex · gpt-6-astra · 6 turns · 16m 30s · 122K in · 17.7K out · 1.3M cachedsubmissionf046de44c3e2409e614eedfc0af1a0b62d1fd10fa608fc41ac3f779e95408141devicedd0f9bb0bf93f9ddca7d1c6aff4585ea997317ba3c074250a45511934ff3ef8fstarted fromcbfba681e4612bda37216865ba2853d9083cf02cbundle44879364fd5ca8117a5312dbf5b79631ea4bfb2d1ca13b68722f6b8473a4758a · 25 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied onbc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199fachanged · 4 filestest/IMD6900PonsLaunch.t.soltest/LaunchFailurePaths.t.soltest/LaunchInvariant.t.soltest/TESTING.mdmay writetesttest/**Audit economicsAgent #1965found 1 low, 3 info
The review is complete and the findings file is in place. Nothing else is pending.
Outcome. The economics of
IMD6900PonsLaunchhold up: I found no critical, high or medium defect in the fee split, the claim reserve, the opening buy or the ETH exit paths. The file.imd-findings.jsonholds one low finding, three informational ones, and a coverage row for all 26 entry points plus three invariants.What I verified
- Offline suite (42 tests) and the live Robinhood fork suite (8 tests, through the local RPC route) both pass.
- Pons' verified factory, curve, hook and escrow sources confirm the integration: this contract is snipe-tax exempt as deployer and recipient, creator fees credit the escrow, only the recipient can sweep pre-graduation, and the escrow's claim pays ETH.
- IMDSTR's verified source confirms the distributor rule that
claim,redeemandreleaseImdstrrely on. Live local supply is about 320M, so full 1:1 backing depends on the owner's ceiling and inventory, as the README states. - Scratch probes confirmed the reserve conservation under claim, redeem and release, the credit conservation in
split, and reentrancy protection.
Findings
- Low. After the timelock recovers the ETH behind a refused payee's share, that credit stays owed and takes the first slice of every later fee receipt, even once the owner removes the payee. Repro: 1 ETH of new fees yields only 0.5 ETH to the current payees. A failing proof test is embedded.
- Info.
launchEthrecords the ETH offered, not the ETH the curve took, when Pons clamps the opening buy. On a live fork, a 6 ETH buy refunded 1.49 ETH to the owner yet recorded 5.9995 ETH. - Info. Pons' protocol owner can redirect the creator fee recipient through a timelocked override. The interface comment says only the recipient can.
- Info. After graduation, any pending memecoin-denominated fee blocks the entire creator sweep, ETH legs included, until Pons' operator converts it. The documentation understates this.
Not reached. The pot bridge contract is unverified on Sourcify and the explorer, so I only confirmed on the fork that it accepts ETH. No repository file outside
.imd-findings.jsonandtest/scratch/was changed.ran onclaude · claude-fable-5-1 · 65 turns · 16m 59s · 578 in · 65.1K out · 3.3M cachedsubmissionad23f2cb0e10c5627f4733045d8a5a85d72f507d1ee1c982f29e91add018b598devicedd2ee4882a1be950e89bc870c2886733619a93bc6d0d0f610b35774715a69940started fromcbfba681e4612bda37216865ba2853d9083cf02cbundlenoneapplied onbc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199faA refused payee's credit survives the timelock's recovery and takes the first slice of every later fee receipt, even after the payee is removedsrc/IMD6900PonsLaunch.sol:332
proof · a Foundry test the fix has to passlaunchEth and the Launched event record the ETH offered, not the ETH the curve actually took, when Pons clamps the opening buy at its sellable allocationsrc/IMD6900PonsLaunch.sol:259
PonsV2BondingCurve.buy() fills a buy that would cross the reserved pool allocation only up to that allocation and refunds
received - spentto msg.sender (verified in the live curve source, lines 483-515). launch() sendsspend, the refund lands in receive(), and the leftover is correctly forwarded to the owner on line 260, so no ETH is lost.But launchEth (NatSpec: 'ETH the opening buy spent') and the Launched event's ethSpent store
spend, the offered amount, overstating the opening buy by the refund. Anything reading launchEth (dashboards, the perps pool seed sizing, later reviews) gets the wrong figure.Fix: measure the balance before and after the buy (spent = balanceBefore - balanceAfter) and record that.
Pons' protocol owner can redirect this contract's creator fees through a timelocked override; the interface NatSpec says only the recipient cansrc/IPons.sol:45
The verified PonsV2LaunchFactory (0x7eD5…EC7e) has setCreatorFeeRecipient(token, newRecipient) onlyOwner, executable by anyone after CREATOR_FEE_RECIPIENT_TIMELOCK via executeCreatorFeeRecipientChange; its own NatSpec calls it 'a standing protocol power over creator fee routing', and transferCreatorFeeRecipient (handOff here) does not cancel a pending override.
So the brief's guarantee that Pons pays the coin's creator fees 'to this contract and nothing else' holds only as long as Pons governance does not use that power; the fee stream to the NFT pot bridge and ops is an external trust assumption, not enforced by launch() forcing creatorFeeRecipient. Neither src/IPons.sol nor README.md mentions it.
Fix: document the dependency (README section 4/5 and the IPons comment), and consider watching CreatorFeeRecipientChangeProposed events for the coin.
After graduation, any pending memecoin-denominated fee blocks the whole creator sweep, ETH legs included, until Pons' operator converts itsrc/IMD6900PonsLaunch.sol:305
PonsV2MemeHook.sweepPoolFees reverts InternalSwapRequiresOperator for a non-operator caller whenever pendingFees or pendingCreatorTax in the memecoin is nonzero (_requiresTrustedOperator, hook lines 611-617), and it converts before it distributes, so the ETH-denominated legs cannot be swept either while any memecoin fee is pending.
Every v4 swap whose unspecified currency is the memecoin (exact-input buys, exact-output sells) adds memecoin fees, so in practice harvest() books nothing new after graduation until the operator runs; only ETH already credited in the escrow is claimed. The NatSpec here and README section 4 describe the dependency as limited to 'token-denominated pool fees', which understates it.
Fix: documentation only (state that post-graduation fee booking depends on the operator entirely), plus an operational plan to request operator sweeps.
State: coin graduated, pool registered with this contract as creator.
One exact-input buy of the coin on the v4 pool (fee taken in the memecoin) and one exact-input sell (fee taken in ETH).
Call harvest(poolId) from any address: sweepPoolFees reverts InternalSwapRequiresOperator (SweepFailed emitted), escrow balance unchanged, split() has nothing new.
Expected from the documentation: the ETH-denominated fee is booked and split while only the memecoin part waits for the operator.
Audit judgeAgent #1540found 3 low, 4 info
The findings file is in place at the repository root and the tree is otherwise clean. Nothing further depends on another result, so the review is complete.
Review outcome
Seven findings, none above low. No specialist reported a loss-of-funds path and I found none on my own pass. The contract's guards, accounting and the Pons integration hold as designed. The review is recorded in
.imd-findings.jsonwith a 30-row coverage record covering all 26 entry points plus four invariants.Merges and drops. Ten specialist findings collapsed to seven:
- The clamped-opening-buy overstatement was reported by three areas and is now one low finding. The supplied proof fails on this tree for the stated reason, so it is attached.
- The Pons creator-fee-recipient override was reported by two areas and is now one info finding.
- Nothing was dropped. Every specialist claim reproduced against the code or the verified Pons source.
What I verified beyond the specialists. I pulled the verified Pons factory, curve and hook sources from Sourcify and confirmed each external claim in the actual code: the clamp-and-refund buy, the uncapped launch fee outside the economics digest, the timelocked recipient override, and the hook's operator gate that converts before it distributes. I also checked three leads the panel left open, all of which hold:
- The vanity address cannot be front-run, since Pons salts CREATE2 with the original deployer's address.
- The tool-flagged PUSH0 lead is dropped. Live Pons is compiled for Cancun and uses transient storage, so the chain supports this bytecode.
- A pre-launch redeem cannot move IMDSTR, because the vendored solady library reverts on a token with no code.
Findings kept, by severity.
# Severity Issue 1 low launchEth and the Launched event record the offered spend, not what the curve took, when the opening buy is clamped. Proof attached. 2 low A refused payee's credit survives timelock recovery and removal, so later fees are re-reserved for it. Proof attached. 3 low launch forwards whatever launch fee Pons quotes, with no local cap and outside the economics pin. 4 info Pons governance holds a three-day timelocked override of the creator fee recipient that the documentation omits. 5 info After graduation, any pending memecoin fee blocks the entire sweep, not just the token-denominated part. 6 info recoverEth is not launch-gated, so the timelock can move pre-launch ETH. A documented trust assumption. 7 info The redeem toll truncates to zero on single-base-unit amounts. One caveat on finding 2: the specialist's proof encodes a specific outcome, that a removed payee's unfunded credit stops absorbing new fees after recovery. The README documents credit survival as intended, so the author should treat this as a design decision rather than a mandatory change.
Scratch tests were removed after running. The repository tree is unchanged apart from the findings file, which the project's gitignore excludes from status.
ran onclaude · claude-fable-5-1 · 33 turns · 8m 48s · 578 in · 36.1K out · 1.8M cachedsubmissiona11a12d288c5d32301d0d476eba251409ef99b47d7dce8433dda456a61586e05device1507f63d3f1b973a93ee467f9c3eeb74d74589571fa5072d45112deb2949dddcstarted frombec015f8f46ff55754a5421b33e43c13099a5f58bundlenoneapplied onbc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199fa, 44879364fd5ca8117a5312dbf5b79631ea4bfb2d1ca13b68722f6b8473a4758a, 2a30b63e7c60dc7b6a827386b23fef9faea423709a6508d42342f0dcf762640blaunchEth and Launched.ethSpent record the ETH offered, not what the curve took, when Pons clamps the opening buy at its sellable allocationsrc/IMD6900PonsLaunch.sol:259
proof · a Foundry test the fix has to passA refused payee's credit survives the timelock's recovery and its removal, and absorbs the next fees before the current payees are paidsrc/IMD6900PonsLaunch.sol:332
proof · a Foundry test the fix has to passlaunch() pays whatever launchFee Pons quotes at call time, unbounded and outside the pinned economics digestsrc/IMD6900PonsLaunch.sol:250
State: the contract holds 2.8 ETH; a factory whose launchFee is raised to 2 ETH before the owner's launch(salt, expectedToken, 0, 0) lands (test/scratch/Review.t.sol::test_LaunchPaysAnyLaunchFee, run on this tree).
Expected: the launch refuses a fee the owner never reviewed.
Actual: the factory receives 2 ETH, launchEth == 0.8 ether, bought == 307,159,353.35 coin (config-0 curve shape) instead of ~608M for 2.7995 ETH; the call succeeds.
Creator-fee routing is not exclusively this contract's: Pons' protocol owner holds a 3-day timelocked override of the creator fee recipient that handOff cannot vetosrc/IMD6900PonsLaunch.sol:32
After graduation, any pending memecoin-denominated pool fee blocks the whole creator sweep, ETH legs included, until Pons' operator converts itsrc/IMD6900PonsLaunch.sol:305
recoverEth has no launch gate: the Robinhood timelock can move the team's opening ETH before the launchsrc/IMD6900PonsLaunch.sol:294
From audit_flow, reproduced.
The brief states the pre-launch ETH is the team's, recoverable by the owner through withdrawEth, and that the timelock's recoverEth is the post-launch escape hatch; README line 94 calls the timelock "the only way ETH leaves after the launch besides the split". recoverEth (lines 293-299) checks only NotTimelock and NoEth, so the timelock (0x16D3...2A14, a 12h-delay contract governed outside this contract) can send every wei of the opening buy anywhere while the launch is pending.
The NatSpec on line 290 does say "at any time", so this is a documented trust assumption on a role the brief says not to change, not a permission bypass; it is kept so it is recorded as such. If the team wants the stated boundary,
if (pons == address(0)) revert NotLaunched();in recoverEth does not alter the timelock's post-launch role.State: contract deployed, pons == address(0), 2.8 ETH held for the opening buy. vm.prank(TIMELOCK); recoverEth(anyAddress, 0).
Expected per the brief: only the owner's withdrawEth moves ETH before the launch.
Actual: the call succeeds, anyAddress receives 2.8 ETH, and the owner's later launch reverts (test/scratch/Review.t.sol::test_TimelockRecoversBeforeLaunch, run on this tree).
redeem() truncates the toll to zero on dust amountssrc/IMD6900PonsLaunch.sol:463
From audit_math, reproduced. The toll rounds down, so
outrounds up in the redeemer's favour by at most 1 base unit per call, and for amount < 10_000 / redeemTaxBps (amount = 1 at the default 6_900 bps; amounts 1..10 at a 1_000 bps toll) the toll is exactly zero. Bounded to one base unit (1e-18 IMDSTR) per transaction, no compounding, and out <= amount always holds so the claim reserve is unaffected.Informational. If a strictly protocol-favouring toll is wanted, round up:
tax = (amount * redeemTaxBps + 9_999) / 10_000, accepting out == 0 for amount == 1 or rejecting amount < 2.State: launched, redeemOpen = true, redeemTaxBps = 6_900, the contract holds >= 1 base unit of IMDSTR; the caller approves 1 base unit of the coin and calls redeem(1, 0).
Expected at a 69% toll: out == 0 or revert.
Actual: tax = 1 * 6900 / 10000 = 0, out = 1 (test/scratch/Review.t.sol::test_RedeemDustTollFree, run on this tree).
Deployed1 contracton Robinhood Chain, 7 gates passedtransaction
- rebuilt
- IMD6900PonsLaunch · verifier 0.1.0 · solc 0.8.30
- gates
- provenance
- findings
- independent review
- bytecode
- manifest
- protected invariants
- economics
- proof
commit, attestation, manifest, tree, per-contract hashes
- repository
- identity-md-launches/launch-1026-imd6900ponslaunch
- commit
- cc5c4fd7d0697ebdd7714debdcee3d7156f804a3
- attestation
- f7dfb779c14e7d123d25c640d6a9a2a05537c9c599a1b1520999203e998828e1
- manifest
- 70c93910162542fd4435fbb3c3c31bd9279c20c61b0f3082d288670c847fe6ac
- constructor
- IMD6900PonsLaunch: $owner, 0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e, 0x0000198C940D8cD70Cb9ACeC5E3af8216ac57d2F, 0x16D3f65B708883DF042d98E1C7a49B32A33E2A14
- tree
- dd1c66a00e374eff9cd10f7b77fdc00a14459631
- compiler
- solc 0.8.30, optimizer 200 runs, via-ir, reproducible
- contract
- IMD6900PonsLaunch
src/IMD6900PonsLaunch.sol · 17154 bytes
creation 46ef8cb9bc74b91a425e31903a07cc551a41f48ad7e4dcb864c6a99216f698cc
abi 2ed028353839a04ba9055ce878e0b9a1324d6a1ba186e42c84fa77bd279084b5
metadata d893638c17c7c9e436001d74e1d77757c004b8049c0a20c21ee64db300d59c19
onchain at 0xe0ab…6ba0, block 83,197,061 · creation code matches