{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"f0191de7-47dd-4658-8578-1f6f236538e4","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"c6982e1f56e833bcd255b148933ab84de74515fc3ae7f32fd886ff16ef2e2ef4","dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"a2cea02e8b33e91eeb784e0e5eaeb571cd62cd69e768d335d5834e24b3aa46e5","skillId":"adapt-contract-project","tools":[]},"key":"adapt_contract_project","kind":"code","role":"implement","skillHash":"a2cea02e8b33e91eeb784e0e5eaeb571cd62cd69e768d335d5834e24b3aa46e5","skillId":"adapt-contract-project","state":"accepted"},{"acceptedSubmissionHash":"ad23f2cb0e10c5627f4733045d8a5a85d72f507d1ee1c982f29e91add018b598","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"2b450e64687c0fe1bfecc821f7766c10c94afacfee69047a4b273af561520dd4","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"0fec2362f75fd7ed9fd543a61e10abacacafe9515e9c94a73c03cb1856423066","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","state":"accepted"},{"acceptedSubmissionHash":"a11a12d288c5d32301d0d476eba251409ef99b47d7dce8433dda456a61586e05","dependsOn":["adapt_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"23da07798b1cb28907921d38739a942e297fa67d71f84dd4615f9b025c634b64","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"802675e3064fdca3e852c8cfbc001b75e2921b6babde84ff449dd587b36779e1","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"bead1ea9c1b76e1f408d942806ae4da067a499ebb0455591dd9b40dfadcb409e","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"f046de44c3e2409e614eedfc0af1a0b62d1fd10fa608fc41ac3f779e95408141","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"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).","parentJobId":null,"planHash":"7c682c2b6d51debe45ffb39e3ed92fd0f0620b10e7234cfcab7b6fbb39d2beec","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"f0191de7-47dd-4658-8578-1f6f236538e4","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1026-imd6900ponslaunch"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52196","feedbackHash":"a9e4514d43156b61f327b788a2d919ef4e34b23a11ed7d53c8fd4ffeafa8d8e6","nodeKey":"adapt_contract_project","submissionHash":"c6982e1f56e833bcd255b148933ab84de74515fc3ae7f32fd886ff16ef2e2ef4","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51214","feedbackHash":"205bf898c4e5c169a10d6e71df50b10fb2466b6b38290fd441f1920efc2a2ac4","nodeKey":"audit_economics","submissionHash":"ad23f2cb0e10c5627f4733045d8a5a85d72f507d1ee1c982f29e91add018b598","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51375","feedbackHash":"96d903bce30679e8de93a50db56e716edd80620bd8ae628d0385700ceff929eb","nodeKey":"audit_flow","submissionHash":"2b450e64687c0fe1bfecc821f7766c10c94afacfee69047a4b273af561520dd4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52210","feedbackHash":"051bf2a7a319f97ae3e7267f0a231455b73fc07dec0f06f42428dc920ec9d5db","nodeKey":"audit_imported_code","submissionHash":"0fec2362f75fd7ed9fd543a61e10abacacafe9515e9c94a73c03cb1856423066","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51231","feedbackHash":"a32acc69248a03910e2129ce299297601f1d953ed45113146f958fd8548a956c","nodeKey":"audit_judge","submissionHash":"a11a12d288c5d32301d0d476eba251409ef99b47d7dce8433dda456a61586e05","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50995","feedbackHash":"4b2257f897468e9c6a8eed3deb0556da65104c2bc3d3ea109e41eec906103fa8","nodeKey":"audit_math","submissionHash":"23da07798b1cb28907921d38739a942e297fa67d71f84dd4615f9b025c634b64","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51487","feedbackHash":"881cfe0e411a0a4f977f320c3d167e5bf158598cabbf4fe83ebe07051b451d63","nodeKey":"audit_permissions","submissionHash":"802675e3064fdca3e852c8cfbc001b75e2921b6babde84ff449dd587b36779e1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52197","feedbackHash":"7a3ff78a0be804d4b8160904932215b3338cff136a70ed296d8f85493848b65d","nodeKey":"manifest","submissionHash":"bead1ea9c1b76e1f408d942806ae4da067a499ebb0455591dd9b40dfadcb409e","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51390","feedbackHash":"ad7d9de70b55ac3c3c7d6df930c197884bf89384cf002263bcb5c6cab4882eb5","nodeKey":"write_foundry_tests","submissionHash":"f046de44c3e2409e614eedfc0af1a0b62d1fd10fa608fc41ac3f779e95408141","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"6cf98978db9b77fb71238b27346f9cfca6c7d5ea5522b42054b6e59c6d5efe3d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dff6c0d3de4aa913","findings":[{"citation":"resolved","description":"split() always distributes the contract's whole balance pro rata. When a payee's call fails, its part stays in the contract (PayFailed) but is not earmarked for it. The next split() treats that retained ETH as new fees and pays it out again pro rata, so the accepting payees receive the refuser's share and the refuser's retained amount halves (at 50/50) on every call. split() is permissionless and needs no new ETH, so anyone can call it repeatedly until the refuser's share is gone. This contradicts the NatSpec ('A payee that refuses its share leaves it here for the next split') and the README ('A payee that refuses ETH keeps its share here for the next split'). With the default payees the pot bridge (0xc39e...9EDd, receive() just logs and accepts) does not refuse today, but any owner-set payee that reverts (a non-payable perps pool, a paused bridge, a contract that reverts in receive) moves its share to ops and the pot instead of waiting. The repository's own test test_ARefusingPayeeKeepsItsShareHere only calls split() once and so does not see it.","line":292,"path":"src/IMD6900PonsLaunch.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMD6900PonsLaunch} from \"src/IMD6900PonsLaunch.sol\";\nimport {PonsTokenParams} from \"src/IPons.sol\";\n\n/// @dev Minimal stand-ins for Pons: a coin, a curve that keeps no fees, an escrow with nothing to claim.\ncontract Coin {\n    mapping(address => uint256) public balanceOf;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n}\n\ncontract Escrow {\n    function balanceOf(address) external pure returns (uint256) { return 0; }\n    function claim() external pure returns (uint256) { return 0; }\n}\n\ncontract Curve {\n    Coin public immutable coin;\n    constructor(Coin c) { coin = c; }\n    function graduated() external pure returns (bool) { return false; }\n    function sweepFees(uint256) external {}\n    function buy(uint256 quoteIn, uint256, address to) external payable returns (uint256 out) {\n        require(msg.value == quoteIn, \"value\");\n        out = quoteIn * 250_000_000;\n        coin.mint(to, out);\n    }\n}\n\ncontract Factory {\n    uint256 public constant launchFee = 0.0005 ether;\n    address public immutable feeEscrow = address(new Escrow());\n    address public constant memeHook = address(0xbeef);\n    function previewLaunchEconomics(uint256, address) external pure returns (bytes32) { return bytes32(uint256(1)); }\n    function launchToken(PonsTokenParams calldata, uint256, address) external payable returns (address token, address curve) {\n        require(msg.value == launchFee, \"fee\");\n        Coin c = new Coin();\n        return (address(c), address(new Curve(c)));\n    }\n}\n\n/// @dev A payee that refuses ETH: no receive, no fallback (the pot bridge paused or replaced by a non-payable contract\n///      would behave the same)\ncontract Refuser {}\n\n/// @notice A payee that refuses its share is documented to \"keep its share here for the next split\". It does not:\n///         every later split() re-divides the retained ETH among all payees, so the accepting payees absorb the\n///         refuser's share, and anyone can call split() repeatedly to finish the job without any new fees arriving.\ncontract SplitRefusedShareTest is Test {\n    address constant OWNER = address(0xA11CE);\n\n    function test_RefusedShareIsNotRedistributedByALaterSplit() public {\n        Factory factory = new Factory();\n        IMD6900PonsLaunch l = new IMD6900PonsLaunch(OWNER, address(factory), address(new Coin()), address(0x7133));\n        vm.deal(OWNER, 1 ether);\n        vm.prank(OWNER);\n        l.launch{value: 1 ether}(bytes32(0), address(0), 0, 0);\n        assertEq(address(l).balance, 0, \"nothing left after the launch\");\n\n        address refuser = address(new Refuser());\n        address ops = makeAddr(\"ops\");\n        address[] memory to = new address[](2);\n        uint16[] memory bps = new uint16[](2);\n        (to[0], to[1], bps[0], bps[1]) = (refuser, ops, 5_000, 5_000);\n        vm.prank(OWNER);\n        l.setPayees(to, bps);\n\n        vm.deal(address(this), 1 ether);\n        l.addFees{value: 1 ether}();\n\n        l.split(); // the refuser's 0.5 ETH stays, ops gets 0.5 ETH\n        assertEq(ops.balance, 0.5 ether);\n        assertEq(address(l).balance, 0.5 ether, \"the refuser's share, kept for the next split\");\n\n        // No new fees. A second split must not hand the refuser's retained share to ops.\n        l.split();\n        assertEq(address(l).balance, 0.5 ether, \"the refuser's share must still be here\");\n        assertEq(ops.balance, 0.5 ether, \"ops must not receive the refuser's share\");\n    }\n}","reproduction":"State: launched; setPayees([refuser (contract without receive), ops], [5000, 5000]); addFees{value: 1 ether}(). Call split(): ops gets 0.5 ETH, 0.5 ETH stays (PayFailed for refuser). Call split() again with no new ETH: expected ops still 0.5 ETH and 0.5 ETH retained for the refuser; actual ops 0.75 ETH and 0.25 ETH retained. After n extra calls ops holds 1 - 0.5^n ETH. Proof: test/scratch/SplitRefusedShare.t.sol fails with 'the refuser's share must still be here: 250000000000000000 != 500000000000000000'.","severity":"medium","snippet":"        amount = address(this).balance;","title":"split() re-divides a refused payee's retained share among the other payees; anyone can repeat it until the accepting payees absorb it"},{"citation":"resolved","description":"release()/releaseAsEth() let the owner take every coin above 'one per IMDSTR outside this contract', and the NatSpec promises that this 'can never reach coin an IMDSTR holder could claim'. The bound is computed from IMDSTRRobinhood.totalSupply(), which only counts IMDSTR already minted on Robinhood. IMDSTRRobinhood (verified source, 0x0000198C...d2F) is a LayerZero OFT whose supply is minted by bridge messages from Ethereum, where IMD6900 (same address, verified on Ethereum mainnet) has totalSupply 994,033,586 and only 339,909,958 locked in the adapter 0x6bdca052...6c51 against 320,081,431 minted on Robinhood. Any IMD6900 holder on Ethereum can bridge in at any time, permissionlessly, and the new IMDSTR is claimable 1:1 here. After the owner releases down to the Robinhood supply, every bridge-in is unbacked: claims become first-come-first-served and the last holders' claim() reverts (TransferFailed) with the coin already gone to the owner. Note also that launch() does not check bought >= any IMDSTR figure (2 ETH buys ~526M on the fork; 2.8 ETH ~608M; 1B total coin supply), so a full 1:1 for the 994M on Ethereum is not reachable by design; the contract should at least not let release() rely on a supply figure that any Ethereum holder can raise afterwards, e.g. reserve against the Ethereum total (or an owner-pinned ceiling) rather than the Robinhood supply, and document the shortfall.","line":314,"path":"src/IMD6900PonsLaunch.sol","reproduction":"State (mock numbers, fork numbers in parentheses): IMDSTR on Robinhood 100M held by A (320.08M live); launch buys 250M coin (526M live). Owner release(0): takes 150M, 100M coin left. Then 100M IMDSTR bridges in to B (live: up to ~654M can). B claim(100M) succeeds and takes the coin reserved for A; A claim(100M) reverts (no coin left); releasable() is 0 and nothing can restore the backing. Expected: a release can never leave an IMDSTR holder unable to claim. Reproduced with test/scratch/ReleaseThenBridgeIn.t.sol (mock mint standing in for the OFT bridge mint), which passes on the current code, i.e. the unbacked state is reachable.","severity":"medium","snippet":"        uint256 owed = IERC20Supply(imdstr).totalSupply() - SafeTransferLib.balanceOf(imdstr, address(this));","title":"releasable() 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 from"},{"citation":"resolved","description":"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.","line":166,"path":"src/IMD6900PonsLaunch.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMD6900PonsLaunch} from \"src/IMD6900PonsLaunch.sol\";\nimport {PonsTokenParams} from \"src/IPons.sol\";\n\n/// @dev Minimal stand-ins for Pons: a coin, a curve that keeps no fees, an escrow with nothing to claim.\ncontract Coin {\n    mapping(address => uint256) public balanceOf;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n}\n\ncontract Escrow {\n    function balanceOf(address) external pure returns (uint256) { return 0; }\n    function claim() external pure returns (uint256) { return 0; }\n}\n\ncontract Curve {\n    Coin public immutable coin;\n    constructor(Coin c) { coin = c; }\n    function graduated() external pure returns (bool) { return false; }\n    function sweepFees(uint256) external {}\n    function buy(uint256 quoteIn, uint256, address to) external payable returns (uint256 out) {\n        require(msg.value == quoteIn, \"value\");\n        out = quoteIn * 250_000_000;\n        coin.mint(to, out);\n    }\n}\n\ncontract Factory {\n    uint256 public constant launchFee = 0.0005 ether;\n    address public immutable feeEscrow = address(new Escrow());\n    address public constant memeHook = address(0xbeef);\n    function previewLaunchEconomics(uint256, address) external pure returns (bytes32) { return bytes32(uint256(1)); }\n    function launchToken(PonsTokenParams calldata, uint256, address) external payable returns (address token, address curve) {\n        require(msg.value == launchFee, \"fee\");\n        Coin c = new Coin();\n        return (address(c), address(new Curve(c)));\n    }\n}\n\n/// @notice The default payees are read live: 30% to owner(). Solady's Ownable exposes renounceOwnership(), after\n///         which owner() is address(0) and every split() sends 30% of the fees to address(0), where nothing can\n///         recover it. The pot bridge still gets its 70%, so nothing reverts and nothing warns.\ncontract DefaultPayeeRenounceTest is Test {\n    address constant OWNER = address(0xA11CE);\n\n    function test_SplitNeverPaysAddressZero() public {\n        Factory factory = new Factory();\n        IMD6900PonsLaunch l = new IMD6900PonsLaunch(OWNER, address(factory), address(new Coin()), address(0x7133));\n        vm.deal(OWNER, 1 ether);\n        vm.prank(OWNER);\n        l.launch{value: 1 ether}(bytes32(0), address(0), 0, 0);\n\n        vm.prank(OWNER);\n        l.renounceOwnership();\n        (address[] memory to,) = l.payees();\n        assertEq(to[1], address(0), \"the default ops payee is now address(0)\");\n\n        vm.deal(address(this), 1 ether);\n        l.addFees{value: 1 ether}();\n        uint256 burnedBefore = address(0).balance;\n        l.split();\n        assertEq(address(0).balance - burnedBefore, 0, \"fees must never be sent to address(0)\");\n        assertEq(l.POT_BRIDGE().balance, 0.7 ether);\n    }\n}","reproduction":"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'.","severity":"low","snippet":"            (to[0], bps[0], to[1], bps[1]) = (POT_BRIDGE, POT_BPS, owner(), 10_000 - POT_BPS);","title":"Default ops payee is the live owner(): after renounceOwnership() every split() sends 30% of the fees to address(0)"},{"citation":"resolved","description":"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.","line":346,"path":"src/IMD6900PonsLaunch.sol","reproduction":"State: launched with 608M coin held, 320M IMDSTR outside. Holder claims 100M: held 508M, IMDSTR in contract 100M, outside 220M. Owner release(0): takes 288M, held 220M = outside. Owner releaseImdstr(100_000_000e18, teamWallet): outside is now 320M, held 220M. teamWallet (made a distributor, or any later holder of that IMDSTR) claim(100M) succeeds and takes coin reserved for others; the last 100M of claims revert. Expected per NatSpec lines 24-26: coin an IMDSTR holder could claim is never released.","severity":"low","snippet":"        SafeTransferLib.safeTransfer(imdstr, to, amount);","title":"releaseImdstr() can re-circulate swapped IMDSTR after a release, leaving claims under-backed despite the 'never reach coin a holder could claim' invariant"},{"citation":"resolved","description":"PonsV2LaunchFactory._launchToken only checks params.expectedEconomics when it is non-zero, and the pin is meant to be taken in an earlier transaction (factory NatSpec: 'Reading the digest and launching in separate transactions still leaves the terms free to move in between; the pin is what makes that movement revert'). launch() fills a zero expectedEconomics with factory.previewLaunchEconomics() in the same call, so the digest always equals the live one and the check is vacuous unless the owner stored a digest through setMeta. IPons.sol line 31 and the README describe a protection that the default path does not provide: Pons' owner can change the config's supply, curve fee, graduation threshold, pool fee, or the hook policy (all owner-updatable) between the team's review and the launch tx, and the coin launches on the new terms. (Changing them also moves the mined address, so NotWhereMined would catch most but not all of these: fee policy changes are not part of the token init code.)","line":218,"path":"src/IMD6900PonsLaunch.sol","reproduction":"State: owner has reviewed config 0 (1B supply, 1% fee, graduation 4.2 ETH) and mines a salt. Pons owner then calls setLaunchConfig/updates hook fee policy (e.g. protocolFeeShareBps 30% -> 50%). Owner calls launch(salt, expected, 0, min) with metaSet false (expectedEconomics 0). Expected: LaunchEconomicsMismatch revert as documented. Actual: launch() previews the new digest itself and the launch proceeds under the changed fee policy; the token address is unchanged because the policy is only in the curve's init code, not the token's.","severity":"low","snippet":"        if (p.expectedEconomics == bytes32(0)) p.expectedEconomics = factory.previewLaunchEconomics(launchConfigId, address(0));","title":"expectedEconomics is previewed in the same transaction as the launch, so the 'launch reverts if Pons moved its terms' pin never binds"},{"citation":"resolved","description":"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.","line":235,"path":"src/IMD6900PonsLaunch.sol","reproduction":"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.'","severity":"info","snippet":"    function setPayees(address[] calldata to, uint16[] calldata bps) external onlyOwner {","title":"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'"},{"citation":"resolved","description":"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.","line":280,"path":"src/IMD6900PonsLaunch.sol","reproduction":"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.","severity":"info","snippet":"            try IPonsMemeHook(factory.memeHook()).sweepPoolFees(graduatedPoolId, 0, 0) {}","title":"After graduation, collect() cannot book memecoin-denominated pool fees itself: the hook rejects minConversionQuoteOut = 0 and requires Pons' sweep operator for any conversion"},{"citation":"resolved","description":"Read in full: src/IMD6900PonsLaunch.sol, src/IPons.sol, the vendored solady Ownable, ReentrancyGuard and SafeTransferLib, all four test files, foundry.toml and the protected test. Live dependencies read from verified sources (Sourcify, chain 4663): PonsV2LaunchFactory (launch validation, snipe-tax exemption of the original deployer, transferCreatorFeeRecipient), PonsV2BondingCurve (buy with partial fill and refund, sell, sweepFees gated to the fee recipient or operator), PonsV2LaunchDeployer (CREATE2 salt = keccak(originalDeployer, salt); the brand is in the token init code), PonsV2FeeEscrow (claim pays msg.sender by call), PonsV2MemeHook (sweepPoolFees gating), IMDSTRRobinhood (LayerZero OFT; transfers need a distributor on either side; public burn). Live state checked by RPC at block 83,165,167: launchEnabled true, launchFee 0.0005 ETH, maxCreatorTaxBps 1000, snipe tax 99% over 3 s, config 0 as described, IMDSTR supply 320.08M, owner = the timelock, team wallet not a distributor; Ethereum mainnet: IMD6900 supply 994.03M, 339.91M locked in the OFT adapter. Could not be reached as source: PonsPotBridge 0xc39e...9EDd (unverified; its runtime's receive path accepts any ETH and emits an event, confirmed by disassembly of the first bytes) and the Robinhood timelock 0x16D3...2A14 (unverified; getMinDelay() returns 43,200 s). The repository's fork suite (7 tests) was run against the public RPC and passed; the offline suite (21 tests) passed. Checked and found sound: constructor stores four addresses and calls nothing; no CALLCODE/DELEGATECALL/SELFDESTRUCT, proxies, initializers, mint, pause, freeze or blacklist; reentrancy guards on every ETH/token-moving path; vanity refusal; refund of unspent ETH; redeem tax rounding (favors the user by at most 1 wei); IMDSTR distributor semantics for claim/redeem.","line":36,"path":"src/IMD6900PonsLaunch.sol","reproduction":"Not a defect: coverage statement for the acceptance criterion.","severity":"info","snippet":"contract IMD6900PonsLaunch is Ownable, ReentrancyGuard {","title":"Coverage: what was read and what could not be reached"}],"hash":"0fec2362f75fd7ed9fd543a61e10abacacafe9515e9c94a73c03cb1856423066","nodeId":"45fba7b7-8e4e-46ad-a78b-df5a7a9ec18c","outcome":"completed","summary":"The review is complete. The findings file holds 8 entries, 2 with proof tests, and no tracked file was changed.\n\n**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.json` with concrete inputs.\n\n**Medium findings**\n- **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 call `split()` 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.\n- **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.\n\n**Low findings**\n- 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.\n- `releaseImdstr()` after `release()` re-circulates swapped IMDSTR to any address, breaking the \"never reach coin a holder could claim\" invariant.\n- The economics pin is previewed in the same transaction, so it never triggers the documented revert when Pons changes its terms.\n\n**Info**\n- Owner can route all fees to itself instantly via `setPayees` or `handOff`, contrary to the README's claim. Reported as a trust assumption since the payee defaults must stay.\n- 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.\n- Coverage statement.\n\n**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.\n\n**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.\n\nScratch 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.","treeHash":null,"usage":{"cachedInputTokens":2652607,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":52215,"runtime":"claude","turns":45,"wallClockMs":798141}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b5e3297a04468fd3","findings":[{"citation":"resolved","description":"Boundary: the external read factory.launchFee() and the value-carrying call factory.launchToken{value: fee}. Assumption: the fee is the 0.0005 ETH the owner reviewed (README step 2 reviews the launch terms, and expectedEconomics is required to be pinned so that 'a changed fee policy or config reverts instead of silently accepting new terms'). Actual: Pons' verified PonsV2LaunchFactory.setLaunchFee(uint256) is onlyOwner with no cap, and _economicsDigest hashes exactly ten values (phantomQuote, graduationThreshold, supply, curveFeeBps, poolFee, tickSpacing, protocolFeeShareBps, buybackBurnBps, hookFeeBps, maxInternalPriceImpactBps); launchFee is not among them. launch() therefore forwards the live fee unconditionally, and the opening buy silently gets whatever is left. The owner's off-chain review cannot close the gap: a setLaunchFee that lands in the same block before launch() (the 'race' amplifier) is paid in full, up to the entire balance held, and the only symptom is a smaller launchBought. Everything else in launch is pinned explicitly (expectedToken, expectedEconomics); the fee is the one value-moving term left unbounded. Minimal fix that preserves the design: take a maxLaunchFee argument (or constant) and revert when factory.launchFee() exceeds it, e.g. `if (fee > maxLaunchFee) revert LaunchFeeTooHigh();`.","line":250,"path":"src/IMD6900PonsLaunch.sol","reproduction":"State: the contract holds 2.8 ETH for the opening buy (the fork-timed launch size). Pons' owner calls setLaunchFee(2 ether) before the owner's launch(salt, expectedToken, 0, 0) lands. Expected: the launch refuses a fee the owner never reviewed. Actual: launchToken is paid 2 ETH, the opening buy spends only 0.8 ETH, and the call succeeds. With Pons config 0 (1B supply, phantomQuote 1.68 ETH, 6.9% all-in fee) the buy returns ~307.2M coin instead of ~608M; launchEth = 0.8e18 instead of 2.7995e18. Scratch test test/scratch/MathAudit.t.sol::test_LaunchPaysAnyLaunchFeeTheFactoryQuotes reproduces this against a factory whose launchFee is mutable: `ETH the opening buy got: 0.8`, `coin bought: 307159353.35`, assertion `800000000000000000 != 2799500000000000000`.","severity":"low","snippet":"        uint256 fee = factory.launchFee();\n        address curve_;\n        (token, curve_) = factory.launchToken{value: fee}(p, launchConfigId, address(0));","title":"launch() pays whatever launchFee() Pons quotes at call time, with no local bound and outside the pinned economics digest"},{"citation":"resolved","description":"Boundary: the external call IPonsCurve.buy{value: spend}. Assumption: the curve consumes all of `spend` (NatSpec on launchEth: 'ETH the opening buy spent'; fork test asserts launchEth == eth - launchFee). Actual: the verified PonsV2BondingCurve.buy fills a buy that would cross the reserved allocation only up to `sellable`, re-prices it from the token side, and refunds `received - spent` to msg.sender (this contract) with CurveBuyRefunded. launch() then forwards that refund to the owner together with any other leftover, so the ETH is not lost, but it stores `spend` (the gross request) into launchEth and emits it as Launched.ethSpent. With config 0 the clamp triggers for any opening buy above ~4.51 ETH gross (4.2 ETH net of the 6.9% fee), which is inside the range the team can fund. The stored figure is the one public record of what the opening buy cost and is wrong by the refund. Minimal fix: measure the balance delta, e.g. `uint256 before = address(this).balance; bought = ...buy{value: spend}(...); launchEth = before - address(this).balance;` (or read the curve's CurveBuy spent value).","line":259,"path":"src/IMD6900PonsLaunch.sol","reproduction":"State: the contract holds 5 ETH; launch(salt, expectedToken, 0, 0). Pons config 0 (supply 1e27, phantomQuote 1.68e18, graduation 4.2e18, curve fee 100 bps + creator tax 590 bps): sellable = 1e27 - 1e27*1.68/5.88 = 714,285,714.29 coin; the net ETH needed to buy it is 4.2 ETH, grossed up 4.2/0.931 = 4.5113 ETH. Expected: launchEth = 4.5113e18 (what the curve took). Actual: launchEth = 4.9995e18 (5 ETH - 0.0005 fee); the 0.4882 ETH refund goes to the owner via the leftover sweep. Scratch test test/scratch/MathAudit.t.sol::test_LaunchEthOverstatesAClampedOpeningBuy with a curve mock implementing Pons' clamp-and-refund arithmetic logs `launchEth recorded: 4.9995`, `ETH the curve actually took: 4.511278195488721806`, `refund that went to the owner: 0.488221804511278194`, `coin bought: 714285714.29`, and fails `4999500000000000000 != 4511278195488721806`.","severity":"low","snippet":"        (launchEth, launchBought) = (spend, bought);\n        if (address(this).balance != 0) SafeTransferLib.safeTransferETH(owner(), address(this).balance);","title":"launchEth and Launched.ethSpent record the requested spend, not what the curve took, when the opening buy is clamped at graduation"},{"citation":"resolved","description":"Math precision: the toll rounds down, so `out` rounds 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).","line":463,"path":"src/IMD6900PonsLaunch.sol","reproduction":"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.","severity":"info","snippet":"        uint256 tax = (amount * redeemTaxBps) / 10_000;\n        out = amount - tax;","title":"redeem() truncates the toll to zero on dust amounts (precision x boundary, dust only)"}],"hash":"23da07798b1cb28907921d38739a942e297fa67d71f84dd4615f9b025c634b64","nodeId":"296bfddc-b406-472c-a22e-3ef200cf4118","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the report. No production files were changed.\n\n**Outcome.** The arithmetic in `src/IMD6900PonsLaunch.sol` is 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.\n\n**Findings recorded.**\n- **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.\n- **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 `launchEth` and the `Launched` event record the gross request. A 5 ETH opening buy records 4.9995 ETH while the curve took 4.5113 ETH.\n- **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.\n\n**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.\n\n**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: `releaseAsEth` becomes unusable once the curve is ready to graduate because Pons' `sell` reverts, while `release` keeps working.\n\n**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.","treeHash":null,"usage":{"cachedInputTokens":2194108,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":44068,"runtime":"claude","turns":42,"wallClockMs":612820}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"93c37f17670e4d98","findings":[{"citation":"resolved","description":"Pons' PonsV2BondingCurve.buy (verified source, robin.etherscan.io factory 0x7eD5...EC7e) does not revert when a buy would take the curve past its sellable allocation: it fills up to the allocation, charges only for what it took, refunds the rest to msg.sender (event CurveBuyRefunded) and auto-graduates. launch() sends `spend` to buy and then stores `launchEth = spend` on line 259 and emits Launched(token, curve_, spend, bought) on line 261 without measuring the balance delta. The refund lands in receive() and is forwarded to the owner on line 260, so no ETH is lost, but the public record of 'ETH the opening buy spent' (line 77-78 NatSpec) and the Launched event are wrong whenever the team's ETH exceeds the curve's capacity. With launch config 0 (graduation at 4.2 ETH real reserve, 1% fee + 5.9% creator tax) that is any opening buy above roughly 4.51 ETH net of the launch fee; a donation to the contract before launch (receive() accepts ETH from anyone) also pushes a buyEth=0 launch over it. Assumption violated (First Principles / Execution Trace: untrusted return value, value leak): the code assumes the curve consumes exactly `spend`. Fix: measure `uint256 before = address(this).balance; ... launchEth = before - address(this).balance;` (or read the curve's CurveBuy event amount) before the owner refund on line 260, and emit that value.","line":259,"path":"src/IMD6900PonsLaunch.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMD6900PonsLaunch} from \"src/IMD6900PonsLaunch.sol\";\nimport {PonsTokenParams} from \"src/IPons.sol\";\n\n/// @dev Minimal coin: whole supply minted to the curve, plain transfers.\ncontract Coin {\n    mapping(address => uint256) public balanceOf;\n    uint256 public totalSupply;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n        totalSupply += a;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract Escrow {\n    mapping(address => uint256) public balanceOf;\n\n    function claim() external returns (uint256) {\n        return 0;\n    }\n}\n\n/// @dev A Pons-like curve: a buy larger than the sellable allocation is filled up to it, charged only for what it\n///      took, and the remainder is refunded to msg.sender (PonsV2BondingCurve.buy, \"CurveBuyRefunded\").\ncontract ClampingCurve {\n    Coin public immutable token;\n    uint256 public constant SELLABLE = 100e18; // 1 ETH buys it all at 100 coin / ETH\n    bool public graduated;\n\n    constructor(Coin t) {\n        token = t;\n    }\n\n    function buy(uint256 quoteIn, uint256, address to) external payable returns (uint256 out) {\n        require(msg.value == quoteIn, \"value\");\n        uint256 spent = quoteIn;\n        out = spent * 100;\n        if (out > SELLABLE) {\n            out = SELLABLE;\n            spent = 1 ether;\n        }\n        token.transfer(to, out);\n        uint256 refund = quoteIn - spent;\n        if (refund != 0) {\n            (bool ok,) = msg.sender.call{value: refund}(\"\");\n            require(ok, \"refund\");\n        }\n        graduated = true;\n    }\n}\n\ncontract Factory {\n    uint256 public constant launchFee = 0.0005 ether;\n    address public immutable feeEscrow = address(new Escrow());\n    address public constant memeHook = address(0xbeef);\n    bytes32 public constant economics = keccak256(\"economics\");\n\n    function previewLaunchEconomics(uint256, address) external pure returns (bytes32) {\n        return economics;\n    }\n\n    function launchToken(PonsTokenParams calldata p, uint256, address)\n        external\n        payable\n        returns (address token, address curve)\n    {\n        require(msg.value == launchFee, \"fee\");\n        Coin t = new Coin{salt: p.salt}();\n        ClampingCurve c = new ClampingCurve(t);\n        t.mint(address(c), 100e18);\n        return (address(t), address(c));\n    }\n\n    function predict(bytes32 salt) external view returns (address) {\n        return address(\n            uint160(\n                uint256(\n                    keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(type(Coin).creationCode)))\n                )\n            )\n        );\n    }\n}\n\ncontract PartialFillTest is Test {\n    address constant OWNER = 0x35dA9C0303507ddf708E87F2568EdDf12c47a059;\n\n    function test_launchEthOverstatesTheOpeningBuyOnAPartialFill() public {\n        Factory f = new Factory();\n        IMD6900PonsLaunch l = new IMD6900PonsLaunch(OWNER, address(f), address(0xdead), address(0xbeef));\n        PonsTokenParams memory m = l.meta();\n        m.expectedEconomics = f.economics();\n        vm.prank(OWNER);\n        l.setMeta(m);\n        vm.deal(address(l), 3 ether + 0.0005 ether); // the team's ETH, more than the curve can sell\n        bytes32 salt = keccak256(\"s\");\n        address expected = f.predict(salt);\n        uint256 ownerBefore = OWNER.balance;\n        vm.prank(OWNER);\n        (, uint256 bought) = l.launch(salt, expected, 0, 0);\n        assertEq(bought, 100e18, \"filled only up to the sellable allocation\");\n        assertEq(OWNER.balance - ownerBefore, 2 ether, \"the refunded 2 ETH went to the owner\");\n        // The record says 3 ETH were spent on the opening buy; only 1 ETH was (the curve refunded 2 ETH).\n        assertEq(l.launchEth(), 1 ether, \"launchEth should equal what the curve actually took\");\n    }\n}","reproduction":"State: contract deployed with the four addresses, setMeta with a pinned expectedEconomics, 3.0005 ETH held, a curve whose sellable allocation is exhausted by 1 ETH (Pons behaviour; mocked by ClampingCurve in the proof). Call from owner: launch(salt, predictedToken, 0, 0). Expected: launchEth == 1 ether (what the curve took), owner refunded 2 ETH. Actual: owner is refunded 2 ETH and bought == allocation, but launchEth == 3 ether and Launched.ethSpent == 3 ether. The proof test fails on the current code at the assertion `launchEth should equal what the curve actually took: 3000000000000000000 != 1000000000000000000`.","severity":"low","snippet":"        (launchEth, launchBought) = (spend, bought);","title":"launch records the requested spend, not what the curve took: launchEth and Launched.ethSpent overstate a partially filled opening buy"},{"citation":"resolved","description":"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.","line":293,"path":"src/IMD6900PonsLaunch.sol","reproduction":"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.","severity":"info","snippet":"    function recoverEth(address to, uint256 amount) external nonReentrant {\n        if (msg.sender != timelock) revert NotTimelock();\n        if (amount == 0) amount = address(this).balance;\n        if (amount == 0) revert NoEth();","title":"recoverEth has no launch gate: the Robinhood timelock can move the team's opening ETH before the launch"}],"hash":"2b450e64687c0fe1bfecc821f7766c10c94afacfee69047a4b273af561520dd4","nodeId":"f4e0e865-5482-43e8-b95c-901a4cbf5b03","outcome":"completed","summary":"The review is complete. No tracked file changed, and `.imd-findings.json` holds two findings plus a coverage row for all 26 entry points and two invariants.\n\n**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.sol` interface matches the factory's `TokenParams` field for field.\n\n**Findings written**\n\n- **Low, `launch` at line 259.** Pons' curve fills an oversized opening buy only up to its sellable allocation and refunds the rest to the caller. `launch` forwards that refund to the owner correctly, but records `launchEth` and emits `Launched.ethSpent` as 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 a `buyEth = 0` launch. A self-contained proof under `test/scratch/PartialFill.t.sol` fails on the current code and is embedded in the findings file.\n- **Info, `recoverEth` at 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.\n\n**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.\n\n**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.","treeHash":null,"usage":{"cachedInputTokens":2282053,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":41883,"runtime":"claude","turns":47,"wallClockMs":584734}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9e51ef2afd7c2af8","findings":[{"citation":"resolved","description":"Trust-gap (access x asymmetry) note on an external privileged actor, not a defect in this contract's own guards. The contract forces creatorFeeRecipient = address(this) at launch (meta(), line 171) and only handOff() (onlyOwner) can move it from this side, so the documented guarantee reads as absolute. In the verified PonsV2LaunchFactory on Robinhood (Sourcify exact match, 0x7eD5...EC7e) the protocol owner (currently 0x263ed295dAFaE1d9AAdD6E56c4B6F9f38eE019Dd) may call setCreatorFeeRecipient(token, newRecipient) for any launch; after CREATOR_FEE_RECIPIENT_TIMELOCK (3 days) anyone may call executeCreatorFeeRecipientChange(token), which updates the curve's `deployer` (pre-graduation) or the hook's `info.creator` (post-graduation). From then on every sweep credits the new recipient's escrow balance, and this contract's collect() only sees SweepFailed (NotFeeSweepOperator) plus whatever was already credited to it. transferCreatorFeeRecipient (this contract's handOff) deliberately does not cancel a pending override. The contract cannot prevent this and does not need to; the defect is that the stated guarantee (\"to this contract and nothing else\", \"the creator fees always go to this distributor\" at line 46, README section 2 \"creator fees to: this contract, always\") omits the Pons-side override, so the team and the NFT-pot beneficiaries are told the fee stream is unconditional when it is conditional on Pons governance. Suggested fix: document the override as an external trust assumption (and that handOff cannot veto it); optionally have harvest() surface the live recipient (factory.getLaunchedToken(pons).creatorFeeRecipient) so a redirect is observable on-chain from this side.","line":31,"path":"src/IMD6900PonsLaunch.sol","reproduction":"State: IMD6900PonsLaunch launched (pons != 0). Sequence on the live factory: (1) Pons owner calls PonsV2LaunchFactory.setCreatorFeeRecipient(pons, X) with X != address(launch); (2) wait 3 days (< 6 days); (3) anyone calls executeCreatorFeeRecipientChange(pons): factory._launchedTokens[pons].creatorFeeRecipient = X, curve.setCreatorFeeRecipient(X) sets curve.deployer = X; (4) a trade accrues creator tax on the curve; (5) anyone calls launch.harvest(bytes32(0)): collect() -> curve.sweepFees(0) reverts NotFeeSweepOperator (msg.sender is no longer `deployer`) and is caught as SweepFailed; escrow.balanceOf(launch) is 0 so nothing is claimed; split() allocates 0. Expected per the documentation: the creator fees are always this contract's. Actual: all creator fees after step 3 are credited to X; the 70% pot share from then on is lost to the pot. No loss of ETH already held by this contract.","severity":"info","snippet":"///         It is also the coin's fee distributor. Pons pays the coin's creator fees (its 70% of the 1% base fee and the\n///         whole 5.9% creator tax) to this contract and nothing else ({launch} forces it), and the perps pool and its","title":"Creator-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 happen"}],"hash":"802675e3064fdca3e852c8cfbc001b75e2921b6babde84ff449dd587b36779e1","nodeId":"b6c6236a-7b35-447b-a5f2-2f7612aab9c9","outcome":"completed","summary":"The review is complete and written to `.imd-findings.json`. No tracked file was changed.\n\n**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.\n\n**What I checked, in the assigned area**\n\n- **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.\n- **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.\n- **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.\n\n**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.\n\n**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 `handOff` cannot veto it, and afterwards `harvest` only 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.\n\n**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.","treeHash":null,"usage":{"cachedInputTokens":2484185,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":42849,"runtime":"claude","turns":46,"wallClockMs":616242}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1507f63d3f1b973a","findings":[{"citation":"resolved","description":"Merged from audit_flow (low), audit_economics (info) and audit_math (low): one root cause. The verified PonsV2BondingCurve.buy (lines 483-515 of the Sourcify source for factory 0x7eD5...EC7e) fills a buy that would cross the reserved allocation only up to `sellable`, re-prices it from the token side, and refunds `received - spent` to msg.sender with CurveBuyRefunded, then auto-graduates. launch() sends `spend` on line 258, stores `launchEth = spend` on line 259 and emits Launched(token, curve_, spend, bought) on line 261 without measuring what the curve kept. The refund lands in receive() and is forwarded to the owner on line 260, so no ETH is lost, but the one public record of the opening buy (NatSpec line 77: \"ETH the opening buy spent\") and the event overstate it by the refund. With Pons config 0 (1e27 supply, phantom 1.68 ETH, graduation 4.2 ETH, 1% fee + 5.9% tax) the clamp triggers for any opening buy above ~4.51 ETH gross, inside the range the team can fund, and a pre-launch donation to receive() (anyone may send) also pushes a buyEth=0 launch over it. Fix: `uint256 before = address(this).balance; bought = IPonsCurve(curve_).buy{value: spend}(...); launchEth = before - address(this).balance;` and emit that value, before the owner refund on line 260. This preserves the constructor, brand, fee forcing and payee defaults.","line":259,"path":"src/IMD6900PonsLaunch.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMD6900PonsLaunch} from \"src/IMD6900PonsLaunch.sol\";\nimport {PonsTokenParams} from \"src/IPons.sol\";\n\n/// @dev Minimal coin: whole supply minted to the curve, plain transfers.\ncontract Coin {\n    mapping(address => uint256) public balanceOf;\n    uint256 public totalSupply;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n        totalSupply += a;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract Escrow {\n    mapping(address => uint256) public balanceOf;\n\n    function claim() external returns (uint256) {\n        return 0;\n    }\n}\n\n/// @dev A Pons-like curve: a buy larger than the sellable allocation is filled up to it, charged only for what it\n///      took, and the remainder is refunded to msg.sender (PonsV2BondingCurve.buy, \"CurveBuyRefunded\").\ncontract ClampingCurve {\n    Coin public immutable token;\n    uint256 public constant SELLABLE = 100e18; // 1 ETH buys it all at 100 coin / ETH\n    bool public graduated;\n\n    constructor(Coin t) {\n        token = t;\n    }\n\n    function buy(uint256 quoteIn, uint256, address to) external payable returns (uint256 out) {\n        require(msg.value == quoteIn, \"value\");\n        uint256 spent = quoteIn;\n        out = spent * 100;\n        if (out > SELLABLE) {\n            out = SELLABLE;\n            spent = 1 ether;\n        }\n        token.transfer(to, out);\n        uint256 refund = quoteIn - spent;\n        if (refund != 0) {\n            (bool ok,) = msg.sender.call{value: refund}(\"\");\n            require(ok, \"refund\");\n        }\n        graduated = true;\n    }\n}\n\ncontract Factory {\n    uint256 public constant launchFee = 0.0005 ether;\n    address public immutable feeEscrow = address(new Escrow());\n    address public constant memeHook = address(0xbeef);\n    bytes32 public constant economics = keccak256(\"economics\");\n\n    function previewLaunchEconomics(uint256, address) external pure returns (bytes32) {\n        return economics;\n    }\n\n    function launchToken(PonsTokenParams calldata p, uint256, address)\n        external\n        payable\n        returns (address token, address curve)\n    {\n        require(msg.value == launchFee, \"fee\");\n        Coin t = new Coin{salt: p.salt}();\n        ClampingCurve c = new ClampingCurve(t);\n        t.mint(address(c), 100e18);\n        return (address(t), address(c));\n    }\n\n    function predict(bytes32 salt) external view returns (address) {\n        return address(\n            uint160(\n                uint256(\n                    keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(type(Coin).creationCode)))\n                )\n            )\n        );\n    }\n}\n\ncontract PartialFillTest is Test {\n    address constant OWNER = 0x35dA9C0303507ddf708E87F2568EdDf12c47a059;\n\n    function test_launchEthOverstatesTheOpeningBuyOnAPartialFill() public {\n        Factory f = new Factory();\n        IMD6900PonsLaunch l = new IMD6900PonsLaunch(OWNER, address(f), address(0xdead), address(0xbeef));\n        PonsTokenParams memory m = l.meta();\n        m.expectedEconomics = f.economics();\n        vm.prank(OWNER);\n        l.setMeta(m);\n        vm.deal(address(l), 3 ether + 0.0005 ether); // the team's ETH, more than the curve can sell\n        bytes32 salt = keccak256(\"s\");\n        address expected = f.predict(salt);\n        uint256 ownerBefore = OWNER.balance;\n        vm.prank(OWNER);\n        (, uint256 bought) = l.launch(salt, expected, 0, 0);\n        assertEq(bought, 100e18, \"filled only up to the sellable allocation\");\n        assertEq(OWNER.balance - ownerBefore, 2 ether, \"the refunded 2 ETH went to the owner\");\n        // The record says 3 ETH were spent on the opening buy; only 1 ETH was (the curve refunded 2 ETH).\n        assertEq(l.launchEth(), 1 ether, \"launchEth should equal what the curve actually took\");\n    }\n}","reproduction":"State: contract deployed with the four addresses, setMeta with a nonzero expectedEconomics, 3.0005 ETH held, a curve whose sellable allocation is exhausted by 1 ETH (Pons clamp-and-refund behaviour, mocked by ClampingCurve in the proof). Call from owner: launch(salt, predictedToken, 0, 0). Expected: bought == allocation, owner refunded 2 ETH, launchEth == 1 ether. Actual: bought == allocation and the owner is refunded 2 ETH, but launchEth == 3 ether and Launched.ethSpent == 3 ether. Ran the supplied proof on this tree: FAIL \"launchEth should equal what the curve actually took: 3000000000000000000 != 1000000000000000000\". audit_math reports the same on the live curve: 5 ETH in, launchEth 4.9995e18 against 4.5113e18 actually taken.","severity":"low","snippet":"        (launchEth, launchBought) = (spend, bought);","title":"launchEth and Launched.ethSpent record the ETH offered, not what the curve took, when Pons clamps the opening buy at its sellable allocation"},{"citation":"resolved","description":"From audit_economics, reproduced. split() treats only ETH above totalPendingEth as new fees (line 332), and recoverEth() (lines 293-299) moves ETH out without touching the credits it backed; setPayees() (lines 266-278) changes the split but not pendingEth. Once the timelock has recovered the ETH held for a payee that refuses ETH (the documented reason for recoverEth) and the owner has removed that payee, the stale credit is still subtracted from `held` first, so the next fees up to its size are re-reserved for the removed payee instead of being split to the pot bridge and ops, and _pay() to the refuser fails again. There is no function that clears a credit: the only exits are the refuser starting to accept ETH (then the share the timelock already took out is paid to it a second time from later fees) or the timelock recovering again after every harvest, which recreates the same hole each time. With the default payees this is reachable through the POT_BRIDGE constant (70%): if the bridge ever reverts on receive, 70% of each harvest accrues as its credit and the cycle above applies. README section 5 and ADAPTATION.md state that credits survive recovery by design, so this is a design decision for the author; the economic effect is that the current payees forgo an amount equal to the recovered credit on every recovery cycle, and that ETH idles here until the timelock moves it. Minimal fix that keeps the timelock's role: let recovery (or the timelock, by a timelock-only cancelCredit(payee)) cancel the unfunded credit it leaves behind, reducing pendingEth[payee] and totalPendingEth. The attached proof (from the specialist) encodes the outcome that a removed payee's unfunded credit no longer absorbs new fees after the timelock has recovered it.","line":332,"path":"src/IMD6900PonsLaunch.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMD6900PonsLaunch} from \"src/IMD6900PonsLaunch.sol\";\nimport {PonsTokenParams} from \"src/IPons.sol\";\n\ncontract SCToken {\n    mapping(address => uint256) public balanceOf;\n    uint256 public totalSupply;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n        totalSupply += a;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract SCEscrow {\n    function balanceOf(address) external pure returns (uint256) {\n        return 0;\n    }\n\n    function claim() external pure returns (uint256) {\n        return 0;\n    }\n}\n\ncontract SCCurve {\n    SCToken immutable token;\n\n    constructor(SCToken t) {\n        token = t;\n    }\n\n    function graduated() external pure returns (bool) {\n        return false;\n    }\n\n    function sweepFees(uint256) external {}\n\n    function buy(uint256 quoteIn, uint256, address to) external payable returns (uint256 out) {\n        out = quoteIn * 250_000_000;\n        token.mint(to, out);\n    }\n}\n\ncontract SCFactory {\n    uint256 public constant launchFee = 0.0005 ether;\n    SCEscrow public immutable feeEscrow = new SCEscrow();\n    address public constant memeHook = address(0xbeef);\n\n    function launchToken(PonsTokenParams calldata p, uint256, address) external payable returns (address, address) {\n        require(msg.value == launchFee);\n        SCToken t = new SCToken{salt: p.salt}();\n        return (address(t), address(new SCCurve(t)));\n    }\n\n    function predict(bytes32 salt) external view returns (address) {\n        return address(\n            uint160(\n                uint256(\n                    keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(type(SCToken).creationCode)))\n                )\n            )\n        );\n    }\n}\n\ncontract Refuser {\n    receive() external payable {\n        revert(\"refused\");\n    }\n}\n\n/// @notice After the Robinhood timelock recovers the ETH behind a refused share and the owner removes that payee,\n///         the next fees are not split among the current payees: the removed payee's stale credit absorbs them first.\ncontract StaleCreditTest is Test {\n    address constant OWNER = 0x35dA9C0303507ddf708E87F2568EdDf12c47a059;\n    address constant TIMELOCK = 0x16D3f65B708883DF042d98E1C7a49B32A33E2A14;\n    IMD6900PonsLaunch l;\n\n    function setUp() public {\n        SCFactory factory = new SCFactory();\n        SCToken imdstr = new SCToken();\n        l = new IMD6900PonsLaunch(OWNER, address(factory), address(imdstr), TIMELOCK);\n        PonsTokenParams memory m = l.meta();\n        m.expectedEconomics = keccak256(\"pinned\");\n        vm.prank(OWNER);\n        l.setMeta(m);\n        vm.deal(address(l), 1 ether);\n        bytes32 salt = keccak256(\"s\");\n        address predicted = factory.predict(salt);\n        vm.prank(OWNER);\n        l.launch(salt, predicted, 0, 0);\n        vm.deal(address(this), 10 ether);\n    }\n\n    function _payees(address a, address b, uint16 share) internal {\n        address[] memory to = new address[](2);\n        uint16[] memory bps = new uint16[](2);\n        (to[0], to[1], bps[0], bps[1]) = (a, b, share, 10_000 - share);\n        vm.prank(OWNER);\n        l.setPayees(to, bps);\n    }\n\n    function test_RecoveredCreditOfARemovedPayeeStillTakesTheNextFeesFirst() public {\n        address refuser = address(new Refuser());\n        address ops = makeAddr(\"ops\");\n        _payees(refuser, ops, 5_000);\n        l.addFees{value: 1 ether}();\n        l.split(); // ops 0.5 ETH; the refuser's 0.5 ETH stays here as its credit\n        assertEq(address(l).balance, 0.5 ether);\n\n        vm.prank(TIMELOCK);\n        l.recoverEth(makeAddr(\"rescued\"), 0); // the timelock takes the refused 0.5 ETH out\n        assertEq(address(l).balance, 0);\n        _payees(l.POT_BRIDGE(), ops, 7_000); // the owner drops the refuser from the split\n\n        l.addFees{value: 1 ether}(); // the next fees\n        uint256 potBefore = l.POT_BRIDGE().balance;\n        uint256 opsBefore = ops.balance;\n        uint256 allocated = l.split();\n\n        // Expected: the 1 ETH of new fees is split 70/30 between the current payees.\n        // Actual: only 0.5 ETH is allocated (0.35 / 0.15); 0.5 ETH is held again for the removed refuser,\n        // whose backing the timelock already recovered. Every later recovery recreates the same hole.\n        assertEq(allocated, 1 ether, \"new fees allocated to the current payees\");\n        assertEq(l.POT_BRIDGE().balance - potBefore, 0.7 ether, \"pot share\");\n        assertEq(ops.balance - opsBefore, 0.3 ether, \"ops share\");\n        assertEq(address(l).balance, 0, \"nothing re-reserved for the removed payee\");\n    }\n}","reproduction":"Mock Pons (see proof). After launch: setPayees([refuser, ops], [5000, 5000]); addFees(1 ETH); split() -> ops 0.5 ETH, pendingEth[refuser] = 0.5 ETH, balance 0.5 ETH. TIMELOCK recoverEth(rescued, 0) -> balance 0, pendingEth[refuser] still 0.5 ETH. Owner setPayees([POT_BRIDGE, ops], [7000, 3000]) (refuser removed). addFees(1 ETH); split(). Expected: allocated 1 ETH, pot +0.7 ETH, ops +0.3 ETH, balance 0. Actual: allocated 0.5 ETH, pot +0.35 ETH, ops +0.15 ETH, 0.5 ETH held again for the removed refuser. Ran the supplied proof on this tree: FAIL \"new fees allocated to the current payees: 500000000000000000 != 1000000000000000000\".","severity":"low","snippet":"        amount = held > totalPendingEth ? held - totalPendingEth : 0;","title":"A refused payee's credit survives the timelock's recovery and its removal, and absorbs the next fees before the current payees are paid"},{"citation":"resolved","description":"From audit_math, reproduced. launch() reads factory.launchFee() on line 250 and forwards it in full on line 252; the opening buy silently gets what is left (line 256). In the verified PonsV2LaunchFactory, setLaunchFee(uint256) is onlyOwner with no cap (line 427), _launchToken requires msg.value == launchFee exactly (line 767), and _economicsDigest (lines 666-686) hashes phantomQuote, graduationThreshold, supply, curveFeeBps, poolFee, tickSpacing, protocolFeeShareBps, buybackBurnBps, hookFeeBps and maxInternalPriceImpactBps only: the launch fee is not pinned by expectedEconomics. Everything else that moves value in launch() is pinned (expectedToken, expectedEconomics); the fee is the one term left to whatever Pons quotes when the transaction lands, up to the entire balance held. Precondition: Pons' protocol owner raises the fee between the owner's review and the launch (a Pons-admin action, not an unprivileged attacker), so this is a bounded external trust gap rather than a loss path an outsider can trigger. Minimal fix preserving the design: a maxLaunchFee argument or constant, `if (fee > maxLaunchFee) revert LaunchFeeTooHigh();`.","line":250,"path":"src/IMD6900PonsLaunch.sol","reproduction":"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.","severity":"low","snippet":"        uint256 fee = factory.launchFee();","title":"launch() pays whatever launchFee Pons quotes at call time, unbounded and outside the pinned economics digest"},{"citation":"resolved","description":"Merged from audit_economics and audit_permissions: one root cause, an external trust assumption the documentation omits. The contract forces creatorFeeRecipient = address(this) (line 171) and only handOff() (line 282, onlyOwner) moves it from this side, so lines 31-32, line 46 (\"the creator fees always go to this distributor\"), IPons.sol line 45 and README section 2 (\"this contract, always\") read as absolute. In the verified PonsV2LaunchFactory, setCreatorFeeRecipient(token, newRecipient) is onlyOwner for any launch (line 946), executeCreatorFeeRecipientChange(token) is permissionless after CREATOR_FEE_RECIPIENT_TIMELOCK = 3 days (lines 99, 964-974), and transferCreatorFeeRecipient (this contract's handOff) deliberately does not cancel a pending override (lines 884-886, 933-939: \"a standing protocol power over creator fee routing\"). After execution the curve's deployer (pre-graduation) or the hook's info.creator (post-graduation) is the new address, so this contract's sweep reverts NotFeeSweepOperator (caught as SweepFailed) and new creator fees credit the override address. Not a defect in this contract's guards and nothing it can prevent; documentation and monitoring only (watch CreatorFeeRecipientChangeProposed for the coin; state the dependency in README sections 2 and 4 and the IPons NatSpec).","line":32,"path":"src/IMD6900PonsLaunch.sol","reproduction":"State: coin launched, this contract is launch.creatorFeeRecipient. (1) Pons owner calls factory.setCreatorFeeRecipient(pons, X), X != address(this); (2) 3 days pass; (3) anyone calls factory.executeCreatorFeeRecipientChange(pons): curve.deployer = X; (4) a trade accrues creator tax; (5) anyone calls harvest(bytes32(0)): collect() -> curve.sweepFees(0) reverts NotFeeSweepOperator (curve line 582-583, msg.sender != deployer), caught, SweepFailed emitted; escrow.balanceOf(this) == 0 so nothing is claimed; split() allocates 0. Expected per the documentation: creator fees always reach this distributor. Actual: every creator fee after step 3 is X's. Requires the Pons protocol owner; not reproducible by an unprivileged actor, and not runnable offline (no Pons mock of the override in the tree).","severity":"info","snippet":"///         whole 5.9% creator tax) to this contract and nothing else ({launch} forces it), and the perps pool and its","title":"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 veto"},{"citation":"resolved","description":"From audit_economics, reproduced against the verified PonsV2MemeHook source. sweepPoolFees (hook line 500-531) reverts InternalSwapRequiresOperator for a non-operator caller whenever _requiresTrustedOperator is true (line 508), which is whenever pendingFees or pendingCreatorTax in the memecoin is nonzero (lines 611-617), and it converts before it distributes (line 510-511 then 522-530), so the ETH-denominated legs cannot be swept by this contract either while any memecoin fee is pending; the hook's own rescuePoolFees NatSpec says so (\"would also strand its ETH fees behind them, since sweepPoolFees converts before it distributes and reverts as a unit\"). Every v4 swap whose fee currency is the memecoin adds such 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 on lines 304-305 and README section 4 describe the dependency as limited to \"token-denominated pool fees\", which understates it. Documentation only, plus an operational plan to request operator sweeps; the contract's handling (catch, then claim the escrow) is correct.","line":305,"path":"src/IMD6900PonsLaunch.sol","reproduction":"State: coin graduated, pool registered with this contract as creator, one swap whose fee was taken in the memecoin and one whose fee was taken in ETH. Call harvest(poolId) from any address. Expected from the documentation: the ETH-denominated fee is booked and split while only the memecoin part waits for the operator. Actual: sweepPoolFees reverts InternalSwapRequiresOperator at hook line 508 before any distribution, SweepFailed is emitted, the escrow balance is unchanged and split() has nothing new. Verified by reading the hook source; not runnable offline (no graduated-pool mock in the tree).","severity":"info","snippet":"    ///         pool fees require Pons' sweep operator to convert them with a nonzero minimum first. A failed sweep","title":"After graduation, any pending memecoin-denominated pool fee blocks the whole creator sweep, ETH legs included, until Pons' operator converts it"},{"citation":"resolved","description":"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.","line":294,"path":"src/IMD6900PonsLaunch.sol","reproduction":"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).","severity":"info","snippet":"        if (msg.sender != timelock) revert NotTimelock();","title":"recoverEth has no launch gate: the Robinhood timelock can move the team's opening ETH before the launch"},{"citation":"resolved","description":"From audit_math, reproduced. The toll rounds down, so `out` rounds 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.","line":463,"path":"src/IMD6900PonsLaunch.sol","reproduction":"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).","severity":"info","snippet":"        uint256 tax = (amount * redeemTaxBps) / 10_000;","title":"redeem() truncates the toll to zero on dust amounts"}],"hash":"a11a12d288c5d32301d0d476eba251409ef99b47d7dce8433dda456a61586e05","nodeId":"466fbdf3-8bd7-4f8d-9a56-1ac93c1ca3c7","outcome":"completed","summary":"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.\n\n## Review outcome\n\n**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.json` with a 30-row coverage record covering all 26 entry points plus four invariants.\n\n**Merges and drops.** Ten specialist findings collapsed to seven:\n\n- 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.\n- The Pons creator-fee-recipient override was reported by two areas and is now one info finding.\n- Nothing was dropped. Every specialist claim reproduced against the code or the verified Pons source.\n\n**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:\n\n- The vanity address cannot be front-run, since Pons salts CREATE2 with the original deployer's address.\n- The tool-flagged PUSH0 lead is dropped. Live Pons is compiled for Cancun and uses transient storage, so the chain supports this bytecode.\n- A pre-launch redeem cannot move IMDSTR, because the vendored solady library reverts on a token with no code.\n\n**Findings kept, by severity.**\n\n| # | Severity | Issue |\n|---|---|---|\n| 1 | low | launchEth and the Launched event record the offered spend, not what the curve took, when the opening buy is clamped. Proof attached. |\n| 2 | low | A refused payee's credit survives timelock recovery and removal, so later fees are re-reserved for it. Proof attached. |\n| 3 | low | launch forwards whatever launch fee Pons quotes, with no local cap and outside the economics pin. |\n| 4 | info | Pons governance holds a three-day timelocked override of the creator fee recipient that the documentation omits. |\n| 5 | info | After graduation, any pending memecoin fee blocks the entire sweep, not just the token-denominated part. |\n| 6 | info | recoverEth is not launch-gated, so the timelock can move pre-launch ETH. A documented trust assumption. |\n| 7 | info | The redeem toll truncates to zero on single-base-unit amounts. |\n\nOne 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.\n\nScratch tests were removed after running. The repository tree is unchanged apart from the findings file, which the project's gitignore excludes from status.","treeHash":null,"usage":{"cachedInputTokens":1844654,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":36072,"runtime":"claude","turns":33,"wallClockMs":527990}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dd2ee4882a1be950","findings":[{"citation":"resolved","description":"split() treats only ETH above totalPendingEth as new fees, and recoverEth() (timelock) removes ETH without touching the credits it backed. Once the timelock recovers the ETH held for a payee that refuses ETH (the documented reason for recoverEth), that payee's pendingEth stays owed forever: the owner can remove it with setPayees, but the stale credit is still subtracted from `held` first, so the next fees up to its size are re-reserved for the removed payee instead of being split 70/30 to the pot bridge and ops, and _pay() to the refuser fails again. The only ways out are (a) the refuser starting to accept ETH, i.e. the share the timelock took away is paid to it anyway from later fees, or (b) the timelock recovering again after every harvest, which recreates the same hole each time. With the default payees this is reachable through the 70% POT_BRIDGE constant: if the bridge's receive ever reverts, 70% of each harvest accrues as its credit and the cycle above applies. ADAPTATION.md states that credits survive recovery by design, so this is a design trade-off to decide on; the economic effect is that the current payees permanently forgo an amount equal to the recovered credit each recovery cycle. Minimal fix preserving the timelock's role: give the timelock a way to cancel a named payee's credit when it recovers (e.g. recoverEth(to, amount, payeeToCancel) or a timelock-only cancelCredit(payee) that reduces pendingEth and totalPendingEth), so a recovered refused share stops being counted as owed.","line":332,"path":"src/IMD6900PonsLaunch.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMD6900PonsLaunch} from \"src/IMD6900PonsLaunch.sol\";\nimport {PonsTokenParams} from \"src/IPons.sol\";\n\ncontract SCToken {\n    mapping(address => uint256) public balanceOf;\n    uint256 public totalSupply;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n        totalSupply += a;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract SCEscrow {\n    function balanceOf(address) external pure returns (uint256) {\n        return 0;\n    }\n\n    function claim() external pure returns (uint256) {\n        return 0;\n    }\n}\n\ncontract SCCurve {\n    SCToken immutable token;\n\n    constructor(SCToken t) {\n        token = t;\n    }\n\n    function graduated() external pure returns (bool) {\n        return false;\n    }\n\n    function sweepFees(uint256) external {}\n\n    function buy(uint256 quoteIn, uint256, address to) external payable returns (uint256 out) {\n        out = quoteIn * 250_000_000;\n        token.mint(to, out);\n    }\n}\n\ncontract SCFactory {\n    uint256 public constant launchFee = 0.0005 ether;\n    SCEscrow public immutable feeEscrow = new SCEscrow();\n    address public constant memeHook = address(0xbeef);\n\n    function launchToken(PonsTokenParams calldata p, uint256, address) external payable returns (address, address) {\n        require(msg.value == launchFee);\n        SCToken t = new SCToken{salt: p.salt}();\n        return (address(t), address(new SCCurve(t)));\n    }\n\n    function predict(bytes32 salt) external view returns (address) {\n        return address(\n            uint160(\n                uint256(\n                    keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(type(SCToken).creationCode)))\n                )\n            )\n        );\n    }\n}\n\ncontract Refuser {\n    receive() external payable {\n        revert(\"refused\");\n    }\n}\n\n/// @notice After the Robinhood timelock recovers the ETH behind a refused share and the owner removes that payee,\n///         the next fees are not split among the current payees: the removed payee's stale credit absorbs them first.\ncontract StaleCreditTest is Test {\n    address constant OWNER = 0x35dA9C0303507ddf708E87F2568EdDf12c47a059;\n    address constant TIMELOCK = 0x16D3f65B708883DF042d98E1C7a49B32A33E2A14;\n    IMD6900PonsLaunch l;\n\n    function setUp() public {\n        SCFactory factory = new SCFactory();\n        SCToken imdstr = new SCToken();\n        l = new IMD6900PonsLaunch(OWNER, address(factory), address(imdstr), TIMELOCK);\n        PonsTokenParams memory m = l.meta();\n        m.expectedEconomics = keccak256(\"pinned\");\n        vm.prank(OWNER);\n        l.setMeta(m);\n        vm.deal(address(l), 1 ether);\n        bytes32 salt = keccak256(\"s\");\n        address predicted = factory.predict(salt);\n        vm.prank(OWNER);\n        l.launch(salt, predicted, 0, 0);\n        vm.deal(address(this), 10 ether);\n    }\n\n    function _payees(address a, address b, uint16 share) internal {\n        address[] memory to = new address[](2);\n        uint16[] memory bps = new uint16[](2);\n        (to[0], to[1], bps[0], bps[1]) = (a, b, share, 10_000 - share);\n        vm.prank(OWNER);\n        l.setPayees(to, bps);\n    }\n\n    function test_RecoveredCreditOfARemovedPayeeStillTakesTheNextFeesFirst() public {\n        address refuser = address(new Refuser());\n        address ops = makeAddr(\"ops\");\n        _payees(refuser, ops, 5_000);\n        l.addFees{value: 1 ether}();\n        l.split(); // ops 0.5 ETH; the refuser's 0.5 ETH stays here as its credit\n        assertEq(address(l).balance, 0.5 ether);\n\n        vm.prank(TIMELOCK);\n        l.recoverEth(makeAddr(\"rescued\"), 0); // the timelock takes the refused 0.5 ETH out\n        assertEq(address(l).balance, 0);\n        _payees(l.POT_BRIDGE(), ops, 7_000); // the owner drops the refuser from the split\n\n        l.addFees{value: 1 ether}(); // the next fees\n        uint256 potBefore = l.POT_BRIDGE().balance;\n        uint256 opsBefore = ops.balance;\n        uint256 allocated = l.split();\n\n        // Expected: the 1 ETH of new fees is split 70/30 between the current payees.\n        // Actual: only 0.5 ETH is allocated (0.35 / 0.15); 0.5 ETH is held again for the removed refuser,\n        // whose backing the timelock already recovered. Every later recovery recreates the same hole.\n        assertEq(allocated, 1 ether, \"new fees allocated to the current payees\");\n        assertEq(l.POT_BRIDGE().balance - potBefore, 0.7 ether, \"pot share\");\n        assertEq(ops.balance - opsBefore, 0.3 ether, \"ops share\");\n        assertEq(address(l).balance, 0, \"nothing re-reserved for the removed payee\");\n    }\n}","reproduction":"Mock Pons (see proof). After launch: setPayees([refuser, ops], [5000, 5000]); addFees(1 ETH); split() -> ops 0.5 ETH, pendingEth[refuser] = 0.5 ETH, balance 0.5 ETH. TIMELOCK recoverEth(rescued, 0) -> balance 0, pendingEth[refuser] still 0.5 ETH. Owner setPayees([POT_BRIDGE, ops], [7000, 3000]) (refuser removed). addFees(1 ETH); split(). Expected: allocated 1 ETH, pot +0.7 ETH, ops +0.3 ETH, balance 0. Actual: allocated 0.5 ETH, pot +0.35 ETH, ops +0.15 ETH, 0.5 ETH held again for the removed refuser (pendingEth[refuser] = 0.5 ETH). A second recoverEth(…,0) + addFees(1 ETH) + split() again allocates only 0.5 ETH and holds 0.5 ETH. Verified with forge on this tree (test/scratch/StaleCredit.t.sol fails: 500000000000000000 != 1000000000000000000).","severity":"low","snippet":"        amount = held > totalPendingEth ? held - totalPendingEth : 0;","title":"A refused payee's credit survives the timelock's recovery and takes the first slice of every later fee receipt, even after the payee is removed"},{"citation":"resolved","description":"PonsV2BondingCurve.buy() fills a buy that would cross the reserved pool allocation only up to that allocation and refunds `received - spent` to msg.sender (verified in the live curve source, lines 483-515). launch() sends `spend`, 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.","line":259,"path":"src/IMD6900PonsLaunch.sol","reproduction":"Live Robinhood fork at block ~0x4f54f79 (scratch test/scratch/ForkProbe.t.sol): fund the launch contract with 6 ETH, launch(salt, predicted, 0, 0). Actual: bought 714,285,714.29 coin (the whole sellable allocation), the curve graduates inside the launch, 1.488221804511278194 ETH is refunded to the owner, address(this).balance == 0, yet launchEth == 5.9995 ETH. Expected launchEth == 5.9995 - 1.4882 = 4.511278195488721806 ETH. Same with the offline mock in test/scratch/Probe.t.sol test_LaunchEthOverstatedOnClampedBuy (launchEth 1.9995 ETH, refund 1.5995 ETH, 100M bought).","severity":"info","snippet":"        (launchEth, launchBought) = (spend, bought);","title":"launchEth and the Launched event record the ETH offered, not the ETH the curve actually took, when Pons clamps the opening buy at its sellable allocation"},{"citation":"resolved","description":"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.","line":45,"path":"src/IPons.sol","reproduction":"State: coin launched, this contract is launch.creatorFeeRecipient. Pons owner calls factory.setCreatorFeeRecipient(pons, X); after the timelock anyone calls executeCreatorFeeRecipientChange(pons). Afterwards the curve's `deployer` (and post-graduation the hook's info.creator) is X: sweepFees from this contract reverts NotFeeSweepOperator (caught, SweepFailed), new creator fees credit X in the escrow, and harvest() distributes nothing new. Expected per the brief: creator fees always reach this distributor. (Pons-admin precondition; not reproducible by an unprivileged actor.)","severity":"info","snippet":"    /// @notice Only the current creator fee recipient may hand its fees to a new address","title":"Pons' protocol owner can redirect this contract's creator fees through a timelocked override; the interface NatSpec says only the recipient can"},{"citation":"resolved","description":"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.","line":305,"path":"src/IMD6900PonsLaunch.sol","reproduction":"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.","severity":"info","snippet":"    ///         pool fees require Pons' sweep operator to convert them with a nonzero minimum first. A failed sweep","title":"After graduation, any pending memecoin-denominated fee blocks the whole creator sweep, ETH legs included, until Pons' operator converts it"}],"hash":"ad23f2cb0e10c5627f4733045d8a5a85d72f507d1ee1c982f29e91add018b598","nodeId":"01025835-0cf4-4eec-9b19-a2eb9e1f3869","outcome":"completed","summary":"The review is complete and the findings file is in place. Nothing else is pending.\n\n**Outcome.** The economics of `IMD6900PonsLaunch` hold 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.json` holds one low finding, three informational ones, and a coverage row for all 26 entry points plus three invariants.\n\n**What I verified**\n- Offline suite (42 tests) and the live Robinhood fork suite (8 tests, through the local RPC route) both pass.\n- 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.\n- IMDSTR's verified source confirms the distributor rule that `claim`, `redeem` and `releaseImdstr` rely on. Live local supply is about 320M, so full 1:1 backing depends on the owner's ceiling and inventory, as the README states.\n- Scratch probes confirmed the reserve conservation under claim, redeem and release, the credit conservation in `split`, and reentrancy protection.\n\n**Findings**\n- **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.\n- **Info.** `launchEth` records 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.\n- **Info.** Pons' protocol owner can redirect the creator fee recipient through a timelocked override. The interface comment says only the recipient can.\n- **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.\n\n**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.json` and `test/scratch/` was changed.","treeHash":null,"usage":{"cachedInputTokens":3254577,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":65107,"runtime":"claude","turns":65,"wallClockMs":1019165}},{"artifacts":[],"attempt":1,"bundleHash":"2a30b63e7c60dc7b6a827386b23fef9faea423709a6508d42342f0dcf762640b","device":"b3450b02f80aa5c2","findings":[],"hash":"bead1ea9c1b76e1f408d942806ae4da067a499ebb0455591dd9b40dfadcb409e","nodeId":"7f59cfdf-44b3-43c0-bc89-9d3041f4cb21","outcome":"completed","summary":"Created `launch.json` with one `IMD6900PonsLaunch`, `$owner`, and the three supplied addresses in constructor order. Only the manifest changed outside permitted scratch artifacts.\n\nValidation passed against the supplied schema and compiled constructor ABI. `forge build --offline` succeeded; `forge test --offline`: 42 passed, 0 failed, 2 skipped because fork/mining inputs were unavailable.","treeHash":"02ad07c2b7dce626ccd110a2e386eed4c1d7ca22","usage":{"cachedInputTokens":191744,"inputTokens":44498,"model":"gpt-6-astra","outputTokens":2788,"runtime":"codex","turns":3,"wallClockMs":123758}},{"artifacts":[],"attempt":1,"bundleHash":"bc6562aefda4d7595eadf40dd05c60aa5970d1a4ea96325a0c68b6e129d199fa","device":"9b84a1e2e2a9ac5e","findings":[],"hash":"c6982e1f56e833bcd255b148933ab84de74515fc3ae7f32fd886ff16ef2e2ef4","nodeId":"d14ca226-76d2-400a-be59-ec02a3652c27","outcome":"completed","summary":"Implemented the audit fixes and strict launch-address check, preserving the constructor, brand, defaults, and timelock role.\n\nValidation: `forge build` passed; 42 offline tests and 8 live fork tests passed.\n\nChanges and launch prerequisites are documented in [ADAPTATION.md](/home/imd/.identitymd/work/f0191de7-47dd-4658-8578-1f6f236538e4/d14ca226-76d2-400a-be59-ec02a3652c27/ADAPTATION.md). The owner must pin economics before launch; releases require a verified global supply ceiling. Full backing still requires sufficient coin inventory.\n\nNo transactions were broadcast.","treeHash":"78024181aa229db9c283760806e147495284a10d","usage":{"cachedInputTokens":2047616,"inputTokens":145262,"model":"gpt-6-astra","outputTokens":22956,"runtime":"codex","turns":7,"wallClockMs":900617}},{"artifacts":[],"attempt":1,"bundleHash":"44879364fd5ca8117a5312dbf5b79631ea4bfb2d1ca13b68722f6b8473a4758a","device":"dd0f9bb0bf93f9dd","findings":[],"hash":"f046de44c3e2409e614eedfc0af1a0b62d1fd10fa608fc41ac3f779e95408141","nodeId":"778dff99-3cd7-4685-bea2-3ee2f4bf26fa","outcome":"completed","summary":"Added failure-path tests, 1,000 round-trip fuzz cases, and two invariant campaigns covering 32,768 random calls.\n\n`forge build --offline` and `forge test --offline` pass: **69 passed, 0 failed, 2 skipped**.\n\nOnly `test/` changed. No new defects reproduced. Live fork validation remains pending an RPC; fork and vanity tests skipped cleanly.","treeHash":"ba06f9130be2c3d7a4ff8546b4f710fdf56939c5","usage":{"cachedInputTokens":1286528,"inputTokens":122018,"model":"gpt-6-astra","outputTokens":17724,"runtime":"codex","turns":6,"wallClockMs":989683}}],"verification":[{"checks":[{"durationMs":10746,"exitCode":0,"name":"build","output":"Compiling 33 files with Solc 0.8.30\nSolc 0.8.30 finished in 10.64s\nCompiler run successful!\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PonsLaunch.sol:142:68\n    │\n142 │     constructor(address owner_, address factory_, address imdstr_, address timelock_) {\n    │                                                                    ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PonsLaunch.sol:142:51\n    │\n142 │     constructor(address owner_, address factory_, address imdstr_, address timelock_) {\n    │                                                   ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:202:9\n    │\n202 │         emit ClaimSupplyCeilingSet(supply);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:212:9\n    │\n212 │         SafeTransferLib.safeTransferETH(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:208:73\n    │\n208 │     function withdrawEth(address to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:213:9\n    │\n213 │         emit EthWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:224:9\n    │\n224 │         emit MetaSet(m.name, m.symbol, m.logo, m.creatorTaxBps, address(this));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:230:9\n    │\n230 │         emit LaunchConfigSet(id);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:258:18\n    │\n258 │         bought = IPonsCurve(curve_).buy{value: spend}(spend, minTokensOut, address(this));\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:260:41\n    │\n260 │         if (address(this).balance != 0) SafeTransferLib.safeTransferETH(owner(), address(this).balance);\n    │                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:242:9\n    │\n242 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:261:9\n    │\n261 │         emit Launched(token, curve_, spend, bought);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/IMD6900PonsLaunch.sol:260:13\n    │\n260 │         if (address(this).balance != 0) SafeTransferLib.safeTransferETH(owner(), address(this).balance);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMD6900PonsLaunch.sol:273:13\n    │\n273 │             sum += bps[i];\n    │             ━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:277:9\n    │\n277 │         emit PayeesSet(to, bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMD6900PonsLaunch.sol:271:38\n    │\n271 │             if (to[i] == address(0)) revert BadPayees();\n    │                                      ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PonsLaunch.sol:276:23\n    │\n276 │         _payeeCount = uint8(n);\n    │                       ━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:285:9\n    │\n285 │         emit HandedOff(newRecipient);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:298:9\n    │\n298 │         emit EthRecovered(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:313:17\n    │\n313 │                 emit SweepFailed(reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:318:17\n    │\n318 │                 emit SweepFailed(reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:323:9\n    │\n323 │         emit Collected(got);\n    │         ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/IMD6900PonsLaunch.sol:322:13\n    │\n322 │         if (escrow.balanceOf(address(this)) != 0) got = escrow.claim();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:364:13\n    │\n364 │             emit PayFailed(payee, paid);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:367:9\n    │\n367 │         emit Paid(payee, paid);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:343:9\n    │\n343 │         emit Split(amount);\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:397:69\n    │\n397 │     function release(uint256 amount, address to) external onlyOwner nonReentrant {\n    │                                                                     ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:402:9\n    │\n402 │         emit Released(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:409:9\n    │\n409 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:418:9\n    │\n418 │         emit Released(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:423:75\n    │\n423 │     function releaseImdstr(uint256 amount, address to) external onlyOwner nonReentrant {\n    │                                                                           ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:431:9\n    │\n431 │         emit ImdstrReleased(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:436:9\n    │\n436 │         emit RedeemOpenSet(open);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:444:9\n    │\n444 │         emit RedeemTaxSet(bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:456:9\n    │\n456 │         emit Claimed(msg.sender, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:469:9\n    │\n469 │         emit Redeemed(msg.sender, amount, out, tax);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":117,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/PonsLaunch.fork.t.sol:PonsLaunchForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 315.54µs (0.00ns CPU time)\n\nRan 1 test for test/MineVanity.t.sol:MineVanityTest\n[SKIP] test_mine() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 274.54µs (98.11µs CPU time)\n\nRan 21 tests for test/IMD6900PonsLaunch.t.sol:IMD6900PonsLaunchTest\n[PASS] test_ARefusingPayeeKeepsItsShareHere() (gas: 1655106)\n[PASS] test_AddFeesFromThePerpsPool_splitWithEverythingElse() (gas: 1718395)\n[PASS] test_ClaimNotBeforeTheLaunch() (gas: 57419)\n[PASS] test_ClaimOneToOne() (gas: 1375929)\n[PASS] test_DeploysOnAFreshChain() (gas: 89720)\n[PASS] test_EthComesInAndGoesBackToTheOwner() (gas: 228862)\n[PASS] test_FitsOneTransaction() (gas: 30123)\nLogs:\n  deploy gas, calldata included: 745702\n\n[PASS] test_HandOffTheFees() (gas: 1258090)\n[PASS] test_HarvestSplitsPonsFeesToThePotAndOps() (gas: 1463426)\n[PASS] test_LaunchBuysFirstWithAllTheEth() (gas: 1359125)\n[PASS] test_LaunchOnlyTheOwner_andWithEth() (gas: 1039405)\n[PASS] test_LaunchRefusesAnAddressItWasNotMinedFor() (gas: 1041859)\n[PASS] test_LaunchWithTheEthSentAlong_partOfIt_theRestBackToTheOwner() (gas: 1212277)\n[PASS] test_MetaOnlyTheOwner_bounded_untilTheLaunch() (gas: 1483198)\n[PASS] test_PassesTheAdmissionScan() (gas: 10125840)\n[PASS] test_RedeemTaxed_onlyWhenOpen_andIMDSTROnlyWhenClosed() (gas: 1737567)\n[PASS] test_ReleaseAsEth() (gas: 1330083)\n[PASS] test_ReleaseOnlyWhatNoHolderCanClaim() (gas: 1558997)\n[PASS] test_SetPayeesBounded_onlyTheOwner() (gas: 208828)\n[PASS] test_SplitNotBeforeTheLaunch_theEthIsTheOpeningBuys() (gas: 85122)\n[PASS] test_TimelockRecoversEth_theOwnerCant() (gas: 1341370)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 29.00ms (32.19ms CPU time)\n\nRan 21 tests for test/LaunchAudit.t.sol:LaunchAuditTest\n[PASS] testFuzz_RefusedCreditAndRoundingAreConserved(uint96,uint16,uint8) (runs: 256, μ: 2014576, ~: 1952285)\nLogs:\n  Bound result 629\n  Bound result 2000\n  Bound result 1\n\n[PASS] test_CeilingOnlyOwnerBoundedAndFrozenAtLaunch() (gas: 1296389)\n[PASS] test_ChangedEconomicsRevertsWithoutLosingOpeningFunds() (gas: 1464855)\n[PASS] test_DefaultOpsFollowsOwnershipButOldCreditDoesNot() (gas: 1699697)\n[PASS] test_FullTimelockRecoveryDoesNotUnderflowSplit() (gas: 1851851)\n[PASS] test_LaunchRequiresAnExplicitExpectedAddress() (gas: 70707)\n[PASS] test_LaunchRequiresEconomicsPinnedBeforehand() (gas: 263411)\n[PASS] test_LocalSupplyAbovePinnedCeilingIncreasesReserve() (gas: 1236804)\n[PASS] test_OwnerCanRouteAllFeesToItself_TrustAssumption() (gas: 1423694)\n[PASS] test_PoolConversionFailureStillClaimsBookedEscrow() (gas: 1329066)\n[PASS] test_RefusedFeesSurviveRepeatedSplitsAndNewFees() (gas: 2479924)\n[PASS] test_ReleaseAsEthAlsoReservesForBridgeIns() (gas: 1332484)\n[PASS] test_ReleaseImdstrCannotRecirculateUnbackedClaimRights() (gas: 1865636)\n[PASS] test_ReleaseReservesForFutureBridgeIns() (gas: 1776049)\n[PASS] test_RemovedPayeeKeepsItsOwnCredit() (gas: 1823835)\n[PASS] test_RenunciationCannotBurnDefaultOpsFees() (gas: 1383068)\n[PASS] test_SetMetaCannotRedirectCreatorFeesOrEnableBuyback() (gas: 1390329)\n[PASS] test_SplitReentrancyCannotSpendACreditTwice() (gas: 1617472)\n[PASS] test_TimelockMayRecoverRetainedFeesAndFutureFeesRestoreCredits() (gas: 1924877)\n[PASS] test_UnderfundedCeilingAllowsClaimsButNoReleases() (gas: 1559683)\n[PASS] test_UnsetCeilingDisablesBothTokenReleasePaths() (gas: 1913114)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 39.07ms (46.92ms CPU time)\n\nRan 4 test suites in 39.68ms (68.67ms CPU time): 42 tests passed, 0 failed, 2 skipped (44 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMD6900PonsLaunch.addFees()\",\"IMD6900PonsLaunch.cancelOwnershipHandover()\",\"IMD6900PonsLaunch.claim(uint256)\",\"IMD6900PonsLaunch.collect(bytes32)\",\"IMD6900PonsLaunch.completeOwnershipHandover(address)\",\"IMD6900PonsLaunch.handOff(address)\",\"IMD6900PonsLaunch.harvest(bytes32)\",\"IMD6900PonsLaunch.launch(bytes32,address,uint256,uint256)\",\"IMD6900PonsLaunch.receive()\",\"IMD6900PonsLaunch.recoverEth(address,uint256)\",\"IMD6900PonsLaunch.redeem(uint256,uint256)\",\"IMD6900PonsLaunch.release(uint256,address)\",\"IMD6900PonsLaunch.releaseAsEth(uint256,uint256,address)\",\"IMD6900PonsLaunch.releaseImdstr(uint256,address)\",\"IMD6900PonsLaunch.renounceOwnership()\",\"IMD6900PonsLaunch.requestOwnershipHandover()\",\"IMD6900PonsLaunch.setClaimSupplyCeiling(uint256)\",\"IMD6900PonsLaunch.setLaunchConfig(uint256)\",\"IMD6900PonsLaunch.setMeta((string,string,string,string,(string,string,string,string,string),address,uint16,bool,bytes32,bytes32))\",\"IMD6900PonsLaunch.setPayees(address[],uint16[])\",\"IMD6900PonsLaunch.setRedeemOpen(bool)\",\"IMD6900PonsLaunch.setRedeemTaxBps(uint16)\",\"IMD6900PonsLaunch.split()\",\"IMD6900PonsLaunch.split(address)\",\"IMD6900PonsLaunch.transferOwnership(address)\",\"IMD6900PonsLaunch.withdrawEth(address,uint256)\"],\"files\":{\".gitignore\":4,\"ADAPTATION.md\":99,\"README.md\":141,\"foundry.toml\":25,\"launch.json\":15,\"src/IMD6900PonsLaunch.sol\":471,\"src/IPons.sol\":66,\"test/IMD6900PonsLaunch.t.sol\":554,\"test/LaunchAudit.t.sol\":376,\"test/MineVanity.t.sol\":33,\"test/PonsLaunch.fork.t.sol\":199,\"test/PonsPredict.sol\":133},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"bead1ea9c1b76e1f408d942806ae4da067a499ebb0455591dd9b40dfadcb409e","verifiedTreeHash":"02ad07c2b7dce626ccd110a2e386eed4c1d7ca22","verifierVersion":"0.1.0+ad90ce4c"},{"checks":[{"durationMs":12812,"exitCode":0,"name":"build","output":"Compiling 33 files with Solc 0.8.30\nSolc 0.8.30 finished in 12.69s\nCompiler run successful!\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PonsLaunch.sol:142:51\n    │\n142 │     constructor(address owner_, address factory_, address imdstr_, address timelock_) {\n    │                                                   ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PonsLaunch.sol:142:68\n    │\n142 │     constructor(address owner_, address factory_, address imdstr_, address timelock_) {\n    │                                                                    ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:202:9\n    │\n202 │         emit ClaimSupplyCeilingSet(supply);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:212:9\n    │\n212 │         SafeTransferLib.safeTransferETH(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:208:73\n    │\n208 │     function withdrawEth(address to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:213:9\n    │\n213 │         emit EthWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:224:9\n    │\n224 │         emit MetaSet(m.name, m.symbol, m.logo, m.creatorTaxBps, address(this));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:230:9\n    │\n230 │         emit LaunchConfigSet(id);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:258:18\n    │\n258 │         bought = IPonsCurve(curve_).buy{value: spend}(spend, minTokensOut, address(this));\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:260:41\n    │\n260 │         if (address(this).balance != 0) SafeTransferLib.safeTransferETH(owner(), address(this).balance);\n    │                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:242:9\n    │\n242 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:261:9\n    │\n261 │         emit Launched(token, curve_, spend, bought);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/IMD6900PonsLaunch.sol:260:13\n    │\n260 │         if (address(this).balance != 0) SafeTransferLib.safeTransferETH(owner(), address(this).balance);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMD6900PonsLaunch.sol:273:13\n    │\n273 │             sum += bps[i];\n    │             ━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:277:9\n    │\n277 │         emit PayeesSet(to, bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMD6900PonsLaunch.sol:271:38\n    │\n271 │             if (to[i] == address(0)) revert BadPayees();\n    │                                      ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PonsLaunch.sol:276:23\n    │\n276 │         _payeeCount = uint8(n);\n    │                       ━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:285:9\n    │\n285 │         emit HandedOff(newRecipient);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:298:9\n    │\n298 │         emit EthRecovered(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:313:17\n    │\n313 │                 emit SweepFailed(reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:318:17\n    │\n318 │                 emit SweepFailed(reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:323:9\n    │\n323 │         emit Collected(got);\n    │         ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/IMD6900PonsLaunch.sol:322:13\n    │\n322 │         if (escrow.balanceOf(address(this)) != 0) got = escrow.claim();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:364:13\n    │\n364 │             emit PayFailed(payee, paid);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:367:9\n    │\n367 │         emit Paid(payee, paid);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:343:9\n    │\n343 │         emit Split(amount);\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:397:69\n    │\n397 │     function release(uint256 amount, address to) external onlyOwner nonReentrant {\n    │                                                                     ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:402:9\n    │\n402 │         emit Released(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:409:9\n    │\n409 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:418:9\n    │\n418 │         emit Released(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:423:75\n    │\n423 │     function releaseImdstr(uint256 amount, address to) external onlyOwner nonReentrant {\n    │                                                                           ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:431:9\n    │\n431 │         emit ImdstrReleased(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:436:9\n    │\n436 │         emit RedeemOpenSet(open);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:444:9\n    │\n444 │         emit RedeemTaxSet(bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:456:9\n    │\n456 │         emit Claimed(msg.sender, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:469:9\n    │\n469 │         emit Redeemed(msg.sender, amount, out, tax);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":122,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MineVanity.t.sol:MineVanityTest\n[SKIP] test_mine() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 933.80µs (723.57µs CPU time)\n\nRan 1 test for test/PonsLaunch.fork.t.sol:PonsLaunchForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 953.38µs (0.00ns CPU time)\n\nRan 21 tests for test/IMD6900PonsLaunch.t.sol:IMD6900PonsLaunchTest\n[PASS] test_ARefusingPayeeKeepsItsShareHere() (gas: 1655106)\n[PASS] test_AddFeesFromThePerpsPool_splitWithEverythingElse() (gas: 1718395)\n[PASS] test_ClaimNotBeforeTheLaunch() (gas: 57419)\n[PASS] test_ClaimOneToOne() (gas: 1375929)\n[PASS] test_DeploysOnAFreshChain() (gas: 89720)\n[PASS] test_EthComesInAndGoesBackToTheOwner() (gas: 228862)\n[PASS] test_FitsOneTransaction() (gas: 30123)\nLogs:\n  deploy gas, calldata included: 745702\n\n[PASS] test_HandOffTheFees() (gas: 1258090)\n[PASS] test_HarvestSplitsPonsFeesToThePotAndOps() (gas: 1463426)\n[PASS] test_LaunchBuysFirstWithAllTheEth() (gas: 1359125)\n[PASS] test_LaunchOnlyTheOwner_andWithEth() (gas: 1039405)\n[PASS] test_LaunchRefusesAnAddressItWasNotMinedFor() (gas: 1041859)\n[PASS] test_LaunchWithTheEthSentAlong_partOfIt_theRestBackToTheOwner() (gas: 1212277)\n[PASS] test_MetaOnlyTheOwner_bounded_untilTheLaunch() (gas: 1483198)\n[PASS] test_PassesTheAdmissionScan() (gas: 10125840)\n[PASS] test_RedeemTaxed_onlyWhenOpen_andIMDSTROnlyWhenClosed() (gas: 1737567)\n[PASS] test_ReleaseAsEth() (gas: 1330083)\n[PASS] test_ReleaseOnlyWhatNoHolderCanClaim() (gas: 1558997)\n[PASS] test_SetPayeesBounded_onlyTheOwner() (gas: 208828)\n[PASS] test_SplitNotBeforeTheLaunch_theEthIsTheOpeningBuys() (gas: 85122)\n[PASS] test_TimelockRecoversEth_theOwnerCant() (gas: 1341370)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 31.26ms (26.11ms CPU time)\n\nRan 21 tests for test/LaunchAudit.t.sol:LaunchAuditTest\n[PASS] testFuzz_RefusedCreditAndRoundingAreConserved(uint96,uint16,uint8) (runs: 256, μ: 2019899, ~: 1998314)\nLogs:\n  Bound result 10854\n  Bound result 4018\n  Bound result 7\n\n[PASS] test_CeilingOnlyOwnerBoundedAndFrozenAtLaunch() (gas: 1296389)\n[PASS] test_ChangedEconomicsRevertsWithoutLosingOpeningFunds() (gas: 1464855)\n[PASS] test_DefaultOpsFollowsOwnershipButOldCreditDoesNot() (gas: 1699697)\n[PASS] test_FullTimelockRecoveryDoesNotUnderflowSplit() (gas: 1851851)\n[PASS] test_LaunchRequiresAnExplicitExpectedAddress() (gas: 70707)\n[PASS] test_LaunchRequiresEconomicsPinnedBeforehand() (gas: 263411)\n[PASS] test_LocalSupplyAbovePinnedCeilingIncreasesReserve() (gas: 1236804)\n[PASS] test_OwnerCanRouteAllFeesToItself_TrustAssumption() (gas: 1423694)\n[PASS] test_PoolConversionFailureStillClaimsBookedEscrow() (gas: 1329066)\n[PASS] test_RefusedFeesSurviveRepeatedSplitsAndNewFees() (gas: 2479924)\n[PASS] test_ReleaseAsEthAlsoReservesForBridgeIns() (gas: 1332484)\n[PASS] test_ReleaseImdstrCannotRecirculateUnbackedClaimRights() (gas: 1865636)\n[PASS] test_ReleaseReservesForFutureBridgeIns() (gas: 1776049)\n[PASS] test_RemovedPayeeKeepsItsOwnCredit() (gas: 1823835)\n[PASS] test_RenunciationCannotBurnDefaultOpsFees() (gas: 1383068)\n[PASS] test_SetMetaCannotRedirectCreatorFeesOrEnableBuyback() (gas: 1390329)\n[PASS] test_SplitReentrancyCannotSpendACreditTwice() (gas: 1617472)\n[PASS] test_TimelockMayRecoverRetainedFeesAndFutureFeesRestoreCredits() (gas: 1924877)\n[PASS] test_UnderfundedCeilingAllowsClaimsButNoReleases() (gas: 1559683)\n[PASS] test_UnsetCeilingDisablesBothTokenReleasePaths() (gas: 1913114)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 31.55ms (43.15ms CPU time)\n\nRan 4 test suites in 33.02ms (64.70ms CPU time): 42 tests passed, 0 failed, 2 skipped (44 total tests)\n","passed":true},{"durationMs":52,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMD6900PonsLaunch.addFees()\",\"IMD6900PonsLaunch.cancelOwnershipHandover()\",\"IMD6900PonsLaunch.claim(uint256)\",\"IMD6900PonsLaunch.collect(bytes32)\",\"IMD6900PonsLaunch.completeOwnershipHandover(address)\",\"IMD6900PonsLaunch.handOff(address)\",\"IMD6900PonsLaunch.harvest(bytes32)\",\"IMD6900PonsLaunch.launch(bytes32,address,uint256,uint256)\",\"IMD6900PonsLaunch.receive()\",\"IMD6900PonsLaunch.recoverEth(address,uint256)\",\"IMD6900PonsLaunch.redeem(uint256,uint256)\",\"IMD6900PonsLaunch.release(uint256,address)\",\"IMD6900PonsLaunch.releaseAsEth(uint256,uint256,address)\",\"IMD6900PonsLaunch.releaseImdstr(uint256,address)\",\"IMD6900PonsLaunch.renounceOwnership()\",\"IMD6900PonsLaunch.requestOwnershipHandover()\",\"IMD6900PonsLaunch.setClaimSupplyCeiling(uint256)\",\"IMD6900PonsLaunch.setLaunchConfig(uint256)\",\"IMD6900PonsLaunch.setMeta((string,string,string,string,(string,string,string,string,string),address,uint16,bool,bytes32,bytes32))\",\"IMD6900PonsLaunch.setPayees(address[],uint16[])\",\"IMD6900PonsLaunch.setRedeemOpen(bool)\",\"IMD6900PonsLaunch.setRedeemTaxBps(uint16)\",\"IMD6900PonsLaunch.split()\",\"IMD6900PonsLaunch.split(address)\",\"IMD6900PonsLaunch.transferOwnership(address)\",\"IMD6900PonsLaunch.withdrawEth(address,uint256)\"],\"files\":{\".gitignore\":4,\"ADAPTATION.md\":99,\"README.md\":141,\"foundry.toml\":25,\"src/IMD6900PonsLaunch.sol\":471,\"src/IPons.sol\":66,\"test/IMD6900PonsLaunch.t.sol\":554,\"test/LaunchAudit.t.sol\":376,\"test/MineVanity.t.sol\":33,\"test/PonsLaunch.fork.t.sol\":199,\"test/PonsPredict.sol\":133},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2088,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/IMD6900PonsLaunch.sol:353: IMD6900PonsLaunch._pay(address) (src/IMD6900PonsLaunch.sol#353-368) sends eth to arbitrary user\n[high/medium] reentrancy-eth at src/IMD6900PonsLaunch.sol:353: Reentrancy in IMD6900PonsLaunch._pay(address) (src/IMD6900PonsLaunch.sol#353-368):\n[high/medium] reentrancy-eth at src/IMD6900PonsLaunch.sol:238: Reentrancy in IMD6900PonsLaunch.launch(bytes32,address,uint256,uint256) (src/IMD6900PonsLaunch.sol#238-262):\n[medium/high] incorrect-equality at src/IMD6900PonsLaunch.sol:293: IMD6900PonsLaunch.recoverEth(address,uint256) (src/IMD6900PonsLaunch.sol#293-299) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/IMD6900PonsLaunch.sol:208: IMD6900PonsLaunch.withdrawEth(address,uint256) (src/IMD6900PonsLaunch.sol#208-214) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/IMD6900PonsLaunch.sol:238: IMD6900PonsLaunch.launch(bytes32,address,uint256,uint256) (src/IMD6900PonsLaunch.sol#238-262) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/IMD6900PonsLaunch.sol:353: IMD6900PonsLaunch._pay(address) (src/IMD6900PonsLaunch.sol#353-368) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/IMD6900PonsLaunch.sol:269: IMD6900PonsLaunch.setPayees(address[],uint16[]).sum (src/IMD6900PonsLaunch.sol#269) is a local variable never initialized\n[low/medium] missing-zero-check at src/IMD6900PonsLaunch.sol:142: IMD6900PonsLaunch.constructor(address,address,address,address).timelock_ (src/IMD6900PonsLaunch.sol#142) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PonsLaunch.sol:142: IMD6900PonsLaunch.constructor(address,address,address,address).imdstr_ (src/IMD6900PonsLaunch.sol#142) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PonsLaunch.sol:251: IMD6900PonsLaunch.launch(bytes32,address,uint256,uint256).curve_ (src/IMD6900PonsLaunch.sol#251) lacks a zero-check on :\n[low/medium] calls-loop at src/IMD6900PonsLaunch.sol:353: IMD6900PonsLaunch._pay(address) (src/IMD6900PonsLaunch.sol#353-368) has external calls inside a loop: (ok,None) = payee.call{value: paid}() (src/IMD6900PonsLaunch.sol#360)\n[low/medium] calls-loop at src/IMD6900PonsLaunch.sol:353: IMD6900PonsLaunch._pay(address) (src/IMD6900PonsLaunch.sol#353-368) has external calls inside a loop: (ok,None) = payee.call{value: paid}() (src/IMD6900PonsLaunch.sol#360)\n[low/medium] reentrancy-benign at src/IMD6900PonsLaunch.sol:238: Reentrancy in IMD6900PonsLaunch.launch(bytes32,address,uint256,uint256) (src/IMD6900PonsLaunch.sol#238-262):\n[low/medium] reentrancy-benign at src/IMD6900PonsLaunch.sol:238: Reentrancy in IMD6900PonsLaunch.launch(bytes32,address,uint256,uint256) (src/IMD6900PonsLaunch.sol#238-262):\n[low/medium] reentrancy-events at src/IMD6900PonsLaunch.sol:371: Reentrancy in IMD6900PonsLaunch.harvest(bytes32) (src/IMD6900PonsLaunch.sol#371-374):\n[low/medium] reentrancy-events at src/IMD6900PonsLaunch.sol:353: Reentrancy in IMD6900PonsLaunch._pay(address) (src/IMD6900PonsLaunch.sol#353-368):\n[low/medium] reentrancy-events at src/IMD6900PonsLaunch.sol:282: Reentrancy in IMD6900PonsLaunch.handOff(address) (src/IMD6900PonsLaunch.sol#282-286):","passed":true},{"durationMs":296,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/IMD6900PonsLaunch.sol:200: Reentrancy: State change after external call (4 places)\n[high] strict-equality-contract-balance at src/IMD6900PonsLaunch.sol:260: Dangerous strict equality checks on contract balances\n[low] centralization-risk at src/IMD6900PonsLaunch.sol:37: Centralization Risk (14 places)\n[low] costly-loop at src/IMD6900PonsLaunch.sol:270: Costly operations inside loop (3 places)\n[low] large-numeric-literal at src/IMD6900PonsLaunch.sol:180: Large Numeric Literal (4 places)\n[low] literal-instead-of-constant at src/IMD6900PonsLaunch.sol:180: Literal Instead of Constant (4 places)\n[low] non-reentrant-not-first at src/IMD6900PonsLaunch.sol:208: `nonReentrant` is Not the First Modifier (5 places)\n[low] push-zero-opcode at src/IMD6900PonsLaunch.sol:2: PUSH0 Opcode (2 places)\n[low] require-revert-in-loop at src/IMD6900PonsLaunch.sol:270: Loop Contains `require`/`revert`\n[low] unchecked-return at src/IMD6900PonsLaunch.sol:341: Unchecked Return (2 places)\n[low] uninitialized-local-variable at src/IMD6900PonsLaunch.sol:340: Uninitialized Local Variable\n[low] unspecific-solidity-pragma at src/IMD6900PonsLaunch.sol:2: Unspecific Solidity Pragma (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"c6982e1f56e833bcd255b148933ab84de74515fc3ae7f32fd886ff16ef2e2ef4","verifiedTreeHash":"78024181aa229db9c283760806e147495284a10d","verifierVersion":"0.1.0+ad90ce4c"},{"checks":[{"durationMs":19612,"exitCode":0,"name":"build","output":"Compiling 35 files with Solc 0.8.30\nSolc 0.8.30 finished in 19.50s\nCompiler run successful!\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PonsLaunch.sol:142:51\n    │\n142 │     constructor(address owner_, address factory_, address imdstr_, address timelock_) {\n    │                                                   ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PonsLaunch.sol:142:68\n    │\n142 │     constructor(address owner_, address factory_, address imdstr_, address timelock_) {\n    │                                                                    ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:202:9\n    │\n202 │         emit ClaimSupplyCeilingSet(supply);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:212:9\n    │\n212 │         SafeTransferLib.safeTransferETH(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:208:73\n    │\n208 │     function withdrawEth(address to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:213:9\n    │\n213 │         emit EthWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:224:9\n    │\n224 │         emit MetaSet(m.name, m.symbol, m.logo, m.creatorTaxBps, address(this));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:230:9\n    │\n230 │         emit LaunchConfigSet(id);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:258:18\n    │\n258 │         bought = IPonsCurve(curve_).buy{value: spend}(spend, minTokensOut, address(this));\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:260:41\n    │\n260 │         if (address(this).balance != 0) SafeTransferLib.safeTransferETH(owner(), address(this).balance);\n    │                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:242:9\n    │\n242 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:261:9\n    │\n261 │         emit Launched(token, curve_, spend, bought);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/IMD6900PonsLaunch.sol:260:13\n    │\n260 │         if (address(this).balance != 0) SafeTransferLib.safeTransferETH(owner(), address(this).balance);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/IMD6900PonsLaunch.sol:273:13\n    │\n273 │             sum += bps[i];\n    │             ━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:277:9\n    │\n277 │         emit PayeesSet(to, bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMD6900PonsLaunch.sol:271:38\n    │\n271 │             if (to[i] == address(0)) revert BadPayees();\n    │                                      ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PonsLaunch.sol:276:23\n    │\n276 │         _payeeCount = uint8(n);\n    │                       ━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:285:9\n    │\n285 │         emit HandedOff(newRecipient);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:298:9\n    │\n298 │         emit EthRecovered(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:313:17\n    │\n313 │                 emit SweepFailed(reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:318:17\n    │\n318 │                 emit SweepFailed(reason);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:323:9\n    │\n323 │         emit Collected(got);\n    │         ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/IMD6900PonsLaunch.sol:322:13\n    │\n322 │         if (escrow.balanceOf(address(this)) != 0) got = escrow.claim();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:364:13\n    │\n364 │             emit PayFailed(payee, paid);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:367:9\n    │\n367 │         emit Paid(payee, paid);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:343:9\n    │\n343 │         emit Split(amount);\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PonsLaunch.sol:360:22\n    │\n360 │         (bool ok,) = payee.call{value: paid}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:397:69\n    │\n397 │     function release(uint256 amount, address to) external onlyOwner nonReentrant {\n    │                                                                     ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:402:9\n    │\n402 │         emit Released(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:409:9\n    │\n409 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:418:9\n    │\n418 │         emit Released(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PonsLaunch.sol:423:75\n    │\n423 │     function releaseImdstr(uint256 amount, address to) external onlyOwner nonReentrant {\n    │                                                                           ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:431:9\n    │\n431 │         emit ImdstrReleased(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:436:9\n    │\n436 │         emit RedeemOpenSet(open);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:444:9\n    │\n444 │         emit RedeemTaxSet(bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:456:9\n    │\n456 │         emit Claimed(msg.sender, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PonsLaunch.sol:469:9\n    │\n469 │         emit Redeemed(msg.sender, amount, out, tax);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":2442,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/PonsLaunch.fork.t.sol:PonsLaunchForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 331.20µs (0.00ns CPU time)\n\nRan 1 test for test/MineVanity.t.sol:MineVanityTest\n[SKIP] test_mine() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 310.34µs (55.44µs CPU time)\n\nRan 21 tests for test/IMD6900PonsLaunch.t.sol:IMD6900PonsLaunchTest\n[PASS] test_ARefusingPayeeKeepsItsShareHere() (gas: 1655106)\n[PASS] test_AddFeesFromThePerpsPool_splitWithEverythingElse() (gas: 1718395)\n[PASS] test_ClaimNotBeforeTheLaunch() (gas: 57419)\n[PASS] test_ClaimOneToOne() (gas: 1375929)\n[PASS] test_DeploysOnAFreshChain() (gas: 89720)\n[PASS] test_EthComesInAndGoesBackToTheOwner() (gas: 228862)\n[PASS] test_FitsOneTransaction() (gas: 30123)\nLogs:\n  deploy gas, calldata included: 745702\n\n[PASS] test_HandOffTheFees() (gas: 1258090)\n[PASS] test_HarvestSplitsPonsFeesToThePotAndOps() (gas: 1463426)\n[PASS] test_LaunchBuysFirstWithAllTheEth() (gas: 1359125)\n[PASS] test_LaunchOnlyTheOwner_andWithEth() (gas: 1039405)\n[PASS] test_LaunchRefusesAnAddressItWasNotMinedFor() (gas: 1041859)\n[PASS] test_LaunchWithTheEthSentAlong_partOfIt_theRestBackToTheOwner() (gas: 1212277)\n[PASS] test_MetaOnlyTheOwner_bounded_untilTheLaunch() (gas: 1483198)\n[PASS] test_PassesTheAdmissionScan() (gas: 10125840)\n[PASS] test_RedeemTaxed_onlyWhenOpen_andIMDSTROnlyWhenClosed() (gas: 1737567)\n[PASS] test_ReleaseAsEth() (gas: 1330083)\n[PASS] test_ReleaseOnlyWhatNoHolderCanClaim() (gas: 1558997)\n[PASS] test_SetPayeesBounded_onlyTheOwner() (gas: 208828)\n[PASS] test_SplitNotBeforeTheLaunch_theEthIsTheOpeningBuys() (gas: 85122)\n[PASS] test_TimelockRecoversEth_theOwnerCant() (gas: 1341370)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 40.01ms (27.04ms CPU time)\n\nRan 23 tests for test/LaunchFailurePaths.t.sol:LaunchFailurePathsTest\n[PASS] testFuzz_ClaimRedeemRoundTripConservesTokens(uint256,uint16) (runs: 1000, μ: 1629263, ~: 1629586)\nLogs:\n  Bound result 3477051341855414109348706\n  Bound result 2\n\n[PASS] test_AllOwnerEntrypointsRejectAnUnprivilegedCaller() (gas: 455042)\n[PASS] test_ClaimInventoryShortfallDoesNotTakeLegacyTokens() (gas: 1346823)\n[PASS] test_ClaimZeroMissingAllowanceAndInsufficientBalanceAreAtomic() (gas: 1393653)\n[PASS] test_CurveSweepFailureStillCollectsAlreadyBookedFeesOnlyOnce() (gas: 1366594)\n[PASS] test_EightPayeesKeepRoundingDustAndShrinkingRemovesStaleEntries() (gas: 2110993)\n[PASS] test_EmptySymbolAndMaximumCreatorTax() (gas: 433549)\n[PASS] test_EscrowClaimFailureRollsBackTheSweep() (gas: 1378958)\n[PASS] test_FailedReleaseSaleRollsBackInventoryAndAllowance() (gas: 1485731)\n[PASS] test_FullHolderBalanceRoundTripAtZeroTax() (gas: 1630119)\n[PASS] test_GraduatedCollectSkipsCurveAndZeroPoolThenSweepsSpecifiedPool() (gas: 1299306)\n[PASS] test_InsufficientFactoryFeeDoesNotCommitLaunch() (gas: 168143)\n[PASS] test_InvalidPayeeArraysRollBackExistingPayees() (gas: 259227)\n[PASS] test_LaunchSlippageRevertsCreationAndCanRetrySameSalt() (gas: 2241612)\n[PASS] test_OneBaseUnitRedeemStillReturnsSomethingAtMaximumTax() (gas: 1608275)\n[PASS] test_OneWeiOpeningBuyAndFrozenConfiguration() (gas: 1224469)\n[PASS] test_PrelaunchDistributionAndReleaseAreClosed() (gas: 332103)\n[PASS] test_RedeemInsufficientLegacyBackingRollsBackCoinTransfer() (gas: 1610849)\n[PASS] test_RedeemZeroSlippageAndMissingApprovalAreAtomic() (gas: 1535687)\n[PASS] test_RefusedAndExcessiveEthWithdrawalsPreserveFunds() (gas: 295983)\n[PASS] test_ReleasesRejectMaximumAmountWithoutMovingTokens() (gas: 1518435)\n[PASS] test_RestrictedLegacyTransferDoesNotCreditAClaim() (gas: 1415847)\n[PASS] test_TooLargeOpeningBuyRollsBackFactoryAndCallValue() (gas: 1064390)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 41.58ms (49.21ms CPU time)\n\nRan 21 tests for test/LaunchAudit.t.sol:LaunchAuditTest\n[PASS] testFuzz_RefusedCreditAndRoundingAreConserved(uint96,uint16,uint8) (runs: 256, μ: 2009611, ~: 1952285)\nLogs:\n  Bound result 16887\n  Bound result 152\n  Bound result 5\n\n[PASS] test_CeilingOnlyOwnerBoundedAndFrozenAtLaunch() (gas: 1296389)\n[PASS] test_ChangedEconomicsRevertsWithoutLosingOpeningFunds() (gas: 1464855)\n[PASS] test_DefaultOpsFollowsOwnershipButOldCreditDoesNot() (gas: 1699697)\n[PASS] test_FullTimelockRecoveryDoesNotUnderflowSplit() (gas: 1851851)\n[PASS] test_LaunchRequiresAnExplicitExpectedAddress() (gas: 70707)\n[PASS] test_LaunchRequiresEconomicsPinnedBeforehand() (gas: 263411)\n[PASS] test_LocalSupplyAbovePinnedCeilingIncreasesReserve() (gas: 1236804)\n[PASS] test_OwnerCanRouteAllFeesToItself_TrustAssumption() (gas: 1423694)\n[PASS] test_PoolConversionFailureStillClaimsBookedEscrow() (gas: 1329066)\n[PASS] test_RefusedFeesSurviveRepeatedSplitsAndNewFees() (gas: 2479924)\n[PASS] test_ReleaseAsEthAlsoReservesForBridgeIns() (gas: 1332484)\n[PASS] test_ReleaseImdstrCannotRecirculateUnbackedClaimRights() (gas: 1865636)\n[PASS] test_ReleaseReservesForFutureBridgeIns() (gas: 1776049)\n[PASS] test_RemovedPayeeKeepsItsOwnCredit() (gas: 1823835)\n[PASS] test_RenunciationCannotBurnDefaultOpsFees() (gas: 1383068)\n[PASS] test_SetMetaCannotRedirectCreatorFeesOrEnableBuyback() (gas: 1390329)\n[PASS] test_SplitReentrancyCannotSpendACreditTwice() (gas: 1617472)\n[PASS] test_TimelockMayRecoverRetainedFeesAndFutureFeesRestoreCredits() (gas: 1924877)\n[PASS] test_UnderfundedCeilingAllowsClaimsButNoReleases() (gas: 1559683)\n[PASS] test_UnsetCeilingDisablesBothTokenReleasePaths() (gas: 1913114)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 44.81ms (53.52ms CPU time)\n\nRan 2 tests for test/LaunchInvariant.t.sol:LaunchFeeInvariantTest\n[PASS] invariant_EthAndEveryPayeesEntitlementAreConserved() (runs: 256, calls: 16384, reverts: 0)\n\n╭------------------+-------------------+-------+---------+----------╮\n| Contract         | Selector          | Calls | Reverts | Discards |\n+===================================================================+\n| LaunchFeeHandler | changePayees      | 2075  | 0       | 0        |\n|------------------+-------------------+-------+---------+----------|\n| LaunchFeeHandler | collect           | 2033  | 0       | 0        |\n|------------------+-------------------+-------+---------+----------|\n| LaunchFeeHandler | deposit           | 2063  | 0       | 0        |\n|------------------+-------------------+-------+---------+----------|\n| LaunchFeeHandler | recover           | 2106  | 0       | 0        |\n|------------------+-------------------+-------+---------+----------|\n| LaunchFeeHandler | refusal           | 2007  | 0       | 0        |\n|------------------+-------------------+-------+---------+----------|\n| LaunchFeeHandler | retry             | 2003  | 0       | 0        |\n|------------------+-------------------+-------+---------+----------|\n| LaunchFeeHandler | split             | 2095  | 0       | 0        |\n|------------------+-------------------+-------+---------+----------|\n| LaunchFeeHandler | transferOwnership | 2002  | 0       | 0        |\n╰------------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 5000\n  Bound result 1001\n  Bound result 2\n  Bound result 248\n  Bound result 435\n  Bound result 9691121921352689349\n  Bound result 9224\n  Bound result 48\n  Bound result 2\n  Bound result 8\n  Bound result 332\n  Bound result 17\n  Bound result 10293\n  Bound result 6128\n  Bound result 10321\n  Bound result 17\n  Bound result 8636\n  Bound result 7\n  Bound result 1037\n  Bound result 16070\n  Bound result 3693\n  Bound result 0\n\n[PASS] test_HandlerExercisesRefusalsRecoveryAndRemovedPayeeRetries() (gas: 1027677)\nLogs:\n  Bound result 5000\n  Bound result 1000000000000000000\n  Bound result 250000000000000000\n  Bound result 7000\n  Bound result 1000000000000000000\n  Bound result 1\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.24s (1.24s CPU time)\n\nRan 2 tests for test/LaunchInvariant.t.sol:LaunchTokenInvariantTest\n[PASS] invariant_InventoryAndCountersMatchAllTokenFlows() (runs: 256, calls: 16384, reverts: 0)\n\n╭--------------------+------------------------+-------+---------+----------╮\n| Contract           | Selector               | Calls | Reverts | Discards |\n+==========================================================================+\n| LaunchTokenHandler | attemptReconfiguration | 2087  | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| LaunchTokenHandler | bridge                 | 1993  | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| LaunchTokenHandler | claim                  | 2027  | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| LaunchTokenHandler | donate                 | 1989  | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| LaunchTokenHandler | redeem                 | 2048  | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| LaunchTokenHandler | redeemPolicy           | 2077  | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| LaunchTokenHandler | release                | 2065  | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| LaunchTokenHandler | releaseLegacy          | 2098  | 0       | 0        |\n╰--------------------+------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 5774\n  Bound result 57\n  Bound result 4873\n  Bound result 29999999999999999999994225\n  Bound result 590\n  Bound result 50000000000000000000\n  Bound result 10\n  Bound result 3714\n  Bound result 2000\n  Bound result 8298\n  Bound result 1\n  Bound result 590\n  Bound result 12616\n  Bound result 700000000000000000\n  Bound result 242\n  Bound result 4\n  Bound result 15042\n  Bound result 1682\n  Bound result 96\n  Bound result 1701473557\n  Bound result 9621\n  Bound result 86343467462439038971831710\n  Bound result 60000000699999999999996499\n  Bound result 2\n  Bound result 352\n  Bound result 65535\n  Bound result 16006\n  Bound result 16\n  Bound result 57\n  Bound result 1000000000000000000000000\n  Bound result 1000000000000000001\n  Bound result 14876\n  Bound result 1000\n  Bound result 4\n  Bound result 3865\n  Bound result 2624\n  Bound result 6876\n  Bound result 7943\n  Bound result 40\n  Bound result 13656531837560961028219278\n  Bound result 109999998299999998298516710\n  Bound result 2535\n\n[PASS] test_HandlerExercisesClaimsRedeemsReleasesAndBridgeArrivals() (gas: 1385585)\nLogs:\n  Bound result 100000000000000000000\n  Bound result 6900\n  Bound result 50000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 0\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 100000000000000000000000000\n  Bound result 109999915500000000000000000\n  Bound result 1000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.36s (2.36s CPU time)\n\nRan 7 test suites in 2.36s (3.73s CPU time): 69 tests passed, 0 failed, 2 skipped (71 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMD6900PonsLaunch.addFees()\",\"IMD6900PonsLaunch.cancelOwnershipHandover()\",\"IMD6900PonsLaunch.claim(uint256)\",\"IMD6900PonsLaunch.collect(bytes32)\",\"IMD6900PonsLaunch.completeOwnershipHandover(address)\",\"IMD6900PonsLaunch.handOff(address)\",\"IMD6900PonsLaunch.harvest(bytes32)\",\"IMD6900PonsLaunch.launch(bytes32,address,uint256,uint256)\",\"IMD6900PonsLaunch.receive()\",\"IMD6900PonsLaunch.recoverEth(address,uint256)\",\"IMD6900PonsLaunch.redeem(uint256,uint256)\",\"IMD6900PonsLaunch.release(uint256,address)\",\"IMD6900PonsLaunch.releaseAsEth(uint256,uint256,address)\",\"IMD6900PonsLaunch.releaseImdstr(uint256,address)\",\"IMD6900PonsLaunch.renounceOwnership()\",\"IMD6900PonsLaunch.requestOwnershipHandover()\",\"IMD6900PonsLaunch.setClaimSupplyCeiling(uint256)\",\"IMD6900PonsLaunch.setLaunchConfig(uint256)\",\"IMD6900PonsLaunch.setMeta((string,string,string,string,(string,string,string,string,string),address,uint16,bool,bytes32,bytes32))\",\"IMD6900PonsLaunch.setPayees(address[],uint16[])\",\"IMD6900PonsLaunch.setRedeemOpen(bool)\",\"IMD6900PonsLaunch.setRedeemTaxBps(uint16)\",\"IMD6900PonsLaunch.split()\",\"IMD6900PonsLaunch.split(address)\",\"IMD6900PonsLaunch.transferOwnership(address)\",\"IMD6900PonsLaunch.withdrawEth(address,uint256)\"],\"files\":{\".gitignore\":4,\"ADAPTATION.md\":99,\"README.md\":141,\"foundry.toml\":25,\"src/IMD6900PonsLaunch.sol\":471,\"src/IPons.sol\":66,\"test/IMD6900PonsLaunch.t.sol\":554,\"test/LaunchAudit.t.sol\":376,\"test/LaunchFailurePaths.t.sol\":420,\"test/LaunchInvariant.t.sol\":452,\"test/MineVanity.t.sol\":33,\"test/PonsLaunch.fork.t.sol\":199,\"test/PonsPredict.sol\":133,\"test/TESTING.md\":33},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f046de44c3e2409e614eedfc0af1a0b62d1fd10fa608fc41ac3f779e95408141","verifiedTreeHash":"ba06f9130be2c3d7a4ff8546b4f710fdf56939c5","verifierVersion":"0.1.0+ad90ce4c"}]}