{"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":"35d16e4a-e1e6-43b7-9efe-3b7b1fe99404","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"b057d3d9b185f02a1c148b2d48b11adf6964fca092891777b55f30bc3b781dac","dependsOn":["build_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":"8b409f0989c66f856e38b4a72fa2063f24cdbd3e4cea051dea779b624572dc24","dependsOn":["build_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":"c6cc8892dec1333746e2a80460f8f6d4a8ef5e21b72473c9616efe8e5802d2d0","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"1cbc7f45652f8ea2c7922dfcca927720692d236438ad2b543f4b65df49eb4a27","dependsOn":["build_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":"6e5d6558265f6175684f7f868596d9584329be6c33888c28e832ecfc3624250d","dependsOn":["build_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":"6c565e8becc6abd4a1fdc822db77f09db632a5f3e753ff86eacdf36b46aaa37c","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"00880417221d37dc709ecd7616980560492c632dd1876ebaa9684668e118562a","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"cd1b901a36ffef3dfb8ebd2c8c6b3a1d1bce67bb8fc6a53771938cc9c5abad0a","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build this:\n\nA protocol that enables a user to one click sell all swarm launched tokens in his wallet onchain on Ethereum\n1. build a website where user connects his wallet. It reads all the tokens in his wallet, identifies the ones launched by the swarm, lists them on the website, shows number of tokens and their USD value\n2. Shows total USD values of all those tokens.\n3. Option to one click sell all of them, receive $IMD (0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7) for all the sale.\n4. deduct 2% of all IMD to be received as sell tax and send to the treasury wallet (0xFC3ad9C531e1736690b2d5f24FebD35ebf051cFB)\n5. Make sure to sell tokens only which are sellable, have enough liquidity to be sold and receive IMD/ETH, if token is not sellable, do not initiate txn and highlight on the list of tokens on the frontend.\n6. in case of a failed txn, refund the failed txn token back to the user.\n7. conduct full testing end to end","parentJobId":null,"planHash":"4f84e7f7472359af75de84e6632c64c3e7e59577d53a5f9ebd20efeb48ac4b96","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"35d16e4a-e1e6-43b7-9efe-3b7b1fe99404","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1682-build-protocol-enables-user"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52614","feedbackHash":"c3f3f06855dd24112fa944cf3f2944dacfa0829ebe67cbb15b88574d848adee6","nodeKey":"audit_economics","submissionHash":"b057d3d9b185f02a1c148b2d48b11adf6964fca092891777b55f30bc3b781dac","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52613","feedbackHash":"2436892739afa59bf4d9d65b128f858e0a357df549ad32e4333a8470b6bc1fa9","nodeKey":"audit_flow","submissionHash":"8b409f0989c66f856e38b4a72fa2063f24cdbd3e4cea051dea779b624572dc24","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51507","feedbackHash":"f479017bf00fde535bba70baa655f0d79f7c2032155cf26dd2f56208617ebef8","nodeKey":"audit_judge","submissionHash":"d25fc421df0c226d9546a9d7bfaba11f18ace13af019554488b074076d1e5b82","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51423","feedbackHash":"6dcd2b8e3fe541ee7b75d5ac6f01ae61d35340753c2da8e23dffd934beafeda3","nodeKey":"audit_judge","submissionHash":"17bdfa5b4b1d294403cead77ca99d3d633a507d3f0b7004a1bba5bf757791d4a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51285","feedbackHash":"e2664baf1c7f7b1f7270918a4c2b83fc1d4d75a95f2fb5fac53971c3f59c7de8","nodeKey":"audit_judge","submissionHash":"c6cc8892dec1333746e2a80460f8f6d4a8ef5e21b72473c9616efe8e5802d2d0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51305","feedbackHash":"7de177c4f6c8fdd89ccfd9c8467b97ef1973305d2ed4542dbf37740af50e4a01","nodeKey":"audit_math","submissionHash":"1cbc7f45652f8ea2c7922dfcca927720692d236438ad2b543f4b65df49eb4a27","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52626","feedbackHash":"e6584e20fc75e540d7e2fe49cb3eb5ccdee6628389781f091a922f3d40c4b02d","nodeKey":"audit_permissions","submissionHash":"6e5d6558265f6175684f7f868596d9584329be6c33888c28e832ecfc3624250d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52254","feedbackHash":"944f37dc9f0b0673428a4781bdd4444c6d1c1f4edb8f4a87d8b4f8743264d882","nodeKey":"build_contract_project","submissionHash":"6c565e8becc6abd4a1fdc822db77f09db632a5f3e753ff86eacdf36b46aaa37c","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52586","feedbackHash":"e1c071ead9473bcffd0b3eb05d1f58c7ea1c20fa6623754b2891ebb200d09e42","nodeKey":"build_contract_project","submissionHash":"634bf29025e47f67fb71a3e6abb92aceb74da5af7d59b931d3c6d3f92e8de466","tag1":"verification:checks","tag2":"acceptance-v2","value":0},{"agentId":"53932","feedbackHash":"a36cfeffa7e4dd0ced0b197a351768ad9b95a00ad3fa03fc5b5328d6f9c234a4","nodeKey":"build_contract_project","submissionHash":"d13f19cb1f97cc68bfcdfdd97f13601b6053ba7e98536614ac4ea2f28385502e","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50986","feedbackHash":"ca29c84f4b47d844421d7d04736ca051d08b25ee1212faa46af39bc00845f3a6","nodeKey":"build_contract_project","submissionHash":"9da411f133da0de82c8947b4db2d7be5d6e8c52a8ebf65f7dbc1a73674b295d7","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50985","feedbackHash":"d01f3aba7d4816bea566a180ed7ba6d21884086dd45e990bae68b812d55ae96e","nodeKey":"manifest","submissionHash":"00880417221d37dc709ecd7616980560492c632dd1876ebaa9684668e118562a","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51297","feedbackHash":"137b5c1f53aa754cf690fe0aeab7fa894b94b191c8a56f75cc17791793bb5680","nodeKey":"manifest","submissionHash":"e4ba6e120a71929eb229d03e375d15acc68b7b7579dbc1ae7c40a0b220de84a8","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52624","feedbackHash":"373bc7e240c64621089e40241bfac4a8524d8191f384446b85245125ad9d5b9e","nodeKey":"manifest","submissionHash":"029b7a17d5261472784819c0adb6bca856ce2a783ade201b60c6e29d53266004","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52193","feedbackHash":"1aeeb17c24dd8461cbcb23e88ec155afb83089c4402f91b6436f5aa07b871784","nodeKey":"write_foundry_tests","submissionHash":"cd1b901a36ffef3dfb8ebd2c8c6b3a1d1bce67bb8fc6a53771938cc9c5abad0a","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52686","feedbackHash":"8219ee1736838a5a5a832ad97ede63f9f08062a0a1207c9c82ba8adc6900727a","nodeKey":"write_foundry_tests","submissionHash":"a251d6872dca81e4d205ba0771cb73943361cdb843243178340f536b323a9961","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52292","feedbackHash":"e16180c1df9367be99f55a39e7e7bc3f171a30bdba881071ccfef835aaccfac0","nodeKey":"write_foundry_tests","submissionHash":"9c0bdd2e9a491964c4c26d6744ed6b6657cc30b8876b1fd704af200b08096140","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"6c332db6e5c06d97a096ede57eb6a760a25a3f2759eb15cbcbccfa4462f3c7d4","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"51398957321aae4be6701d19780d3224dbaa149bcc79531fa50cfa5c057f132a","device":"410247426ec5a6df","findings":[],"hash":"00880417221d37dc709ecd7616980560492c632dd1876ebaa9684668e118562a","nodeId":"bcb1d52e-ab77-4c09-98ce-eb5a124eaaad","outcome":"completed","summary":"The manifest revision is complete and only launch.json changed.\n\n**What I checked.** The accepted source still has one application contract, SwarmExit, whose constructor takes a single owner address and makes no external calls. The manifest names it with `$owner` as its only argument, which matches the constructor, the Ownable2Step base and the owner-check protected test. The protected deployment probe needs only that the runtime is present, under the EIP-170 limit and free of DELEGATECALL, CALLCODE and SELFDESTRUCT, which the existing runtime test covers.\n\n**What I changed.** The accepted source gained one behaviour since the last round: the Uniswap v4 callback now pays the input into the PoolManager first and swaps only the amount it credited, so fee-on-transfer tokens sell their measured amount. I added one clause describing this to the notes so they still describe the accepted implementation. Contract name, constructor arguments and every other sentence of the notes are unchanged.\n\n**Verification.**\n\n| Check | Result |\n|---|---|\n| Manifest keys | exactly kind, contracts, notes |\n| Notes length | 1605 characters |\n| forge build | clean |\n| forge test with verifier seed | 63 passed, 0 failed |\n| Files changed | launch.json only |\n\nNo findings were listed in the brief, so no other revision was required. The website continues to take the confirmed SwarmExit address from the launch handoff; token registry, routes and the Chainlink feed are owner configuration after deployment.","treeHash":"ea53fa66d92277850af51fb5b89c6ef61ced8e4f","usage":{"cachedInputTokens":275961,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":3817,"runtime":"claude","turns":9,"wallClockMs":80010}},{"artifacts":[],"attempt":1,"bundleHash":"199fe9e8e7bdb5c607b016f393d7bb36d618dbc28cb37a311d7035d28f555ab9","device":"af4be7a5238dbcbf","findings":[],"hash":"029b7a17d5261472784819c0adb6bca856ce2a783ade201b60c6e29d53266004","nodeId":"bcb1d52e-ab77-4c09-98ce-eb5a124eaaad","outcome":"completed","summary":"Done. Only launch.json changed.\n\n**What I found.** The accepted contract is a single `SwarmExit` whose constructor takes one owner address and makes no external calls. The existing manifest already named it correctly with `$owner`. The notes, however, described an older implementation: they mentioned only Uniswap V2 and V3 routes and left out the Uniswap v4 venue, the held-refund claim path, the price-impact bound and the rescue restrictions that the current source enforces.\n\n**What I did.**\n- Kept the contract list and constructor argument unchanged, since they match `constructor(address owner_)` in `src/SwarmExit.sol:198`.\n- Rewrote the notes so they describe the accepted source: fixed IMD settlement and treasury addresses, the 2% tax, V2/V3/v4 routes via WETH or native ETH, pre-pull skips, same-transaction refunds, held refunds claimable later, preview functions, and the owner-only configuration powers.\n- Checked the manifest has exactly the keys kind, contracts and notes, string arguments, a name under 32 characters, and notes under 4000 characters.\n\n**Verification.** `forge build` succeeds and the full suite passes with the verifier's fuzz seed.\n\n| Check | Result |\n|---|---|\n| forge build | ok |\n| forge test with FOUNDRY_FUZZ_SEED | 61 passed, 0 failed |\n\nNo findings from a prior review were included in the brief, so this revision was limited to bringing the notes in line with the source.","treeHash":"9f8edfea648305375e3385e82ecdc631340e8640","usage":{"cachedInputTokens":255318,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":3223,"runtime":"claude","turns":8,"wallClockMs":53904}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"56e50117311155be","findings":[{"citation":"resolved","description":"unlockCallback runs the hops first (swap with amountSpecified = -amountIn, which debits the contract exactly amountIn of currencyIn) and only then pays: sync, transfer amountIn, settle. Uniswap v4's settle credits the balance delta the manager actually received, so for a token that charges a fee on transfer the credit is amountIn minus the fee, the currency delta stays negative and unlock reverts CurrencyNotSettled (the repository's MockV4PoolManager enforces the same rule). The quote path never settles (it reverts with V4Quote before paying), so _quoteToImd succeeds, _previewOne reports sellable == true / Status.Sold, and sellAll's pre-pull check at line 476-478 passes: the token is pulled (one fee), executeSale reverts inside _swapV4, and the refund transfer pays a second fee. The README (Trust assumptions, 'Fee-on-transfer tokens sell through V2 or v4 routes (measured amounts)') and the NatSpec promise ('Predictable failures ... are skipped before the token is pulled, so a fee-on-transfer token pays no fee for a sale that cannot work') are both violated for this class: the failure is deterministic, not an execution-time price move. The README directs the owner to v4 routes because the IMD launch factory opens pools on v4, so swarm tokens with a transfer fee are exactly the tokens that will be routed this way. From the website the dry run excludes the token (it can never be sold from the page, contrary to the README); a direct caller of sellAll loses two fees per attempt and receives no IMD. Design-preserving fix: in unlockCallback for an ERC20 input, sync, transfer and settle first, read the credited amount returned by settle(), and swap that measured amount (then take); this is how measured-amount selling works on v4 and keeps the InsufficientLiquidity check. Alternatively document that v4 routes do not support fee-on-transfer tokens and have preview flag them.","line":451,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {MockERC20, FeeOnTransferToken} from \"test/mocks/MockTokens.sol\";\nimport {MockV4PoolManager} from \"test/mocks/MockVenues.sol\";\n\n/// A fee-on-transfer token registered with a Uniswap v4 route: preview says sellable, the sale\n/// always fails (the manager receives less than the swap consumed), and the user pays two fees.\ncontract FotV4Test is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    MockERC20 imd;\n    MockV4PoolManager pm;\n    FeeOnTransferToken feeTok;\n    MockERC20 tokenV;\n    address user = makeAddr(\"user\");\n    address lp = makeAddr(\"lp\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        imd = MockERC20(IMD_ADDR);\n        vm.etch(IMD_ADDR, address(new MockERC20(\"IMD\", \"IMD\")).code);\n        pm = new MockV4PoolManager();\n        exit = new SwarmExit(OWNER);\n        feeTok = new FeeOnTransferToken(500);\n        tokenV = new MockERC20(\"V\", \"V\");\n\n        _addV4(address(feeTok), IMD_ADDR, 50_000e18, 100_000e18);\n        _addV4(address(tokenV), IMD_ADDR, 50_000e18, 100_000e18);\n\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(feeTok), true);\n        exit.setTokenRoute(address(feeTok), SwarmExit.Venue.UniswapV4, address(pm), address(0), _v4(address(feeTok), IMD_ADDR));\n        exit.setSwarmToken(address(tokenV), true);\n        exit.setTokenRoute(address(tokenV), SwarmExit.Venue.UniswapV4, address(pm), address(0), _v4(address(tokenV), IMD_ADDR));\n        vm.stopPrank();\n\n        feeTok.mint(user, 10_000e18);\n        tokenV.mint(user, 1_000e18);\n        vm.startPrank(user);\n        feeTok.approve(address(exit), type(uint256).max);\n        tokenV.approve(address(exit), type(uint256).max);\n        vm.stopPrank();\n    }\n\n    function test_feeOnTransferTokenOnV4RouteIsSellableInPreviewButAlwaysFailsAndBurnsTwoFees() public {\n        address[] memory one = new address[](1);\n        one[0] = address(feeTok);\n        SwarmExit.Preview[] memory p = exit.preview(user, one);\n        assertTrue(p[0].sellable, \"preview says sellable\");\n        assertEq(uint256(p[0].status), uint256(SwarmExit.Status.Sold));\n\n        address[] memory two = new address[](2);\n        two[0] = address(feeTok);\n        two[1] = address(tokenV);\n        uint256[] memory mins = new uint256[](2);\n        vm.prank(user);\n        (SwarmExit.Outcome[] memory o,,,) = exit.sellAll(two, mins, block.timestamp);\n        emit log_named_uint(\"feeTok status\", uint256(o[0].status));\n        emit log_named_uint(\"feeTok balance after\", feeTok.balanceOf(user));\n        emit log_named_uint(\"tokenV status\", uint256(o[1].status));\n        // README: \"Fee-on-transfer tokens sell through V2 or v4 routes (measured amounts)\".\n        assertEq(uint256(o[0].status), uint256(SwarmExit.Status.Sold), \"fee-on-transfer token sold through v4\");\n    }\n\n    function _addV4(address a, address b, uint256 amtA, uint256 amtB) internal {\n        (address c0, address c1, uint256 a0, uint256 a1) = a < b ? (a, b, amtA, amtB) : (b, a, amtB, amtA);\n        MockERC20(c0).mint(lp, a0);\n        MockERC20(c1).mint(lp, a1);\n        vm.startPrank(lp);\n        IERC20(c0).approve(address(pm), a0);\n        IERC20(c1).approve(address(pm), a1);\n        pm.addLiquidity(MockV4PoolManager.PoolKey(c0, c1, 3000, 60, address(0)), a0, a1);\n        vm.stopPrank();\n    }\n\n    function _v4(address cin, address cout) internal pure returns (bytes memory) {\n        SwarmExit.V4Hop[] memory hops = new SwarmExit.V4Hop[](1);\n        hops[0] = SwarmExit.V4Hop(cout, 3000, 60, address(0));\n        return abi.encode(cin, hops);\n    }\n}","reproduction":"State (test/scratch/FotV4.t.sol, included as proof): MockV4PoolManager; FeeOnTransferToken(500) (5% burned on every transfer) registered with a v4 route feeTok -> IMD (fee 3000, tickSpacing 60, no hooks) into a pool of 50_000 FEE / 100_000 IMD; tokenV (plain) with the same kind of route so the batch settles; user holds 10_000e18 FEE, both tokens approved for max. preview(user, [feeTok])[0] -> sellable == true, status == Sold. User calls sellAll([feeTok, tokenV], [0, 0], block.timestamp). Expected (README, NatSpec lines 32-33): feeTok sold with the measured amount, or skipped before the pull with no fee paid. Actual: outcomes[0].status == SaleFailed (7), outcomes[0].imdOut == 0, feeTok.balanceOf(user) == 9_025e18 (500e18 burned on the pull, 475e18 on the refund), TokenRefunded emitted; tokenV sold. Run: forge test --match-path test/scratch/FotV4.t.sol -> '[FAIL: fee-on-transfer token sold through v4: 7 != 0]'. The same holds on the real PoolManager: settle() returns balanceOfSelf() - reservesBefore and unlock reverts CurrencyNotSettled when NonzeroDeltaCount != 0.","severity":"medium","snippet":"            pm.sync(currencyIn);\n            IERC20(currencyIn).safeTransfer(msg.sender, amountIn);\n            pm.settle();","title":"Fee-on-transfer tokens cannot settle through a Uniswap v4 route: the callback settles amountIn after the swap, the PoolManager receives less and reverts CurrencyNotSettled, yet preview marks the token"}],"hash":"17bdfa5b4b1d294403cead77ca99d3d633a507d3f0b7004a1bba5bf757791d4a","nodeId":"649d41fa-5838-4f17-80ba-30d3ebe41463","outcome":"completed","summary":"Review complete. The findings file at `.imd-findings.json` holds one finding and a 28-row coverage record.\n\n**Settlement of the earlier round.** Every finding I raised is fixed, and all eight attached proofs now pass on this tree alongside the project's 115 tests and invariants.\n\n- **High, no venue reaches IMD liquidity:** fixed. The contract gained a Uniswap v4 venue. I rehearsed it on a mainnet fork at block 26188753 against the real PoolManager and the real IMD token. On the hookless 1% ETH/IMD pool, which holds live liquidity, the contract's quote and its real unlock/settle/take swap return the same IMD amount, the deployed contract's bridge quote works, and the real IMD token delivers the tax and the net amount exactly. The empty 0.3% pool is refused by both the quote and the swap, so a misconfigured route refunds rather than settles.\n- **Medium, 1 wei donation reverts the sale:** fixed, pull is capped by allowance. Proof passes.\n- **Medium, forceApprove in the catch block:** fixed, removed. Proof passes.\n- **Medium, pull and refund outside the try:** fixed with non-reverting calls and held refunds claimable by the user. Proof passes.\n- **Medium, zero-proceeds sale settles as Sold:** fixed, the contract floor is enforced on chain and a bridged leg yielding nothing reverts. Proof passes.\n- **Medium, fee-on-transfer double fee:** fixed for V2 and V3 routes by the pre-pull quote. The v4 case is the new finding below.\n- **All advisory items** are confirmed fixed in code or README. The two disputed items stand as the author argued.\n\n**New finding, medium.** A fee-on-transfer token on a Uniswap v4 route can never settle. The callback swaps first and then transfers the full input, so the PoolManager receives less than the swap consumed and reverts with CurrencyNotSettled. The quote path never settles, so preview reports the token sellable and sellAll pulls it. Each attempt burns two transfer fees and yields no IMD. This contradicts the README's claim that fee-on-transfer tokens sell through v4 routes, and the README directs swarm tokens to v4. The attached proof fails on this code with the user left holding 9,025 of 10,000 tokens after a sale that sold nothing. The design-preserving fix is to sync, transfer and settle first, then swap the credited amount.\n\n**Not verifiable here.** The live swarm tokens' own pool keys and the full end-to-end sellAll with a real swarm token remain an open launch item. The IMD leg itself is now rehearsed.","treeHash":null,"usage":{"cachedInputTokens":2097855,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":37124,"runtime":"claude","turns":29,"wallClockMs":742958}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4576f670afa0131d","findings":[{"citation":"resolved","description":"Boundary (external dependency). The contract can only route through Uniswap V2 style routers and Uniswap V3 SwapRouter02/QuoterV2 (enum Venue; `_swap` and `_quote`). The README instructs the owner to set the IMD route to the Uniswap V3 1% WETH/IMD pool through SwapRouter02 and QuoterV2 and states that 'IMD trades today in the Uniswap V3 1% WETH/IMD pool'. On Ethereum mainnet at block 26188160 that pool (0xd6a822d028bbf7b6edfa1533e110ee40c08551d9, token0 WETH) has `liquidity() == 0`, holds 6612 wei of WETH and 0.436 IMD, and QuoterV2.quoteExactInput reverts with 'Unexpected error' for both directions (IMD->WETH 1e18 and WETH->IMD 1e16). There is no WETH/IMD V3 pool at the 0.01%, 0.05% or 0.3% tiers, no Uniswap V2 pair and no SushiSwap pair. IMD's liquidity sits in the Uniswap v4 PoolManager (0x000000000004444c5dc75cB358380D2e3dE08A90 holds 222,235 IMD and 1,924 WETH), which the README itself says this version cannot route ('Uniswap V4 pools are not supported'). The IMD launch factory also opens its launch pools on Uniswap v4, so the swarm tokens' own venues are most likely v4 as well (lead, not verified per token). Consequence on chain: with any configurable IMD route, `_quote(second, out)` fails so `previewAll` reports QuoteFailed for every bridged token (status `BridgeRouteMissing` while unset), and `executeSale` reverts inside the second `_swap`, so `sellAll` refunds every bridged token and reverts with `NothingSold` when nothing routes directly into IMD. `quoteImdInBridge` reverts with NotConfigured('imdRoute quote'), so USD values are 'unavailable' on the site. The protocol as shipped cannot perform requirement 3 (receive IMD for the sale) for any token that must bridge through WETH. Fix (scope decision for the author): add a Uniswap v4 venue (swap through the PoolManager unlock/settle flow or the v4 Universal Router, quote with the v4 Quoter/StateView) for both the IMD leg and the swarm-token legs, or document a WETH settlement fallback the brief would accept. A no-fork Foundry proof cannot express an on-chain liquidity fact; the reproduction below is the on-chain state.","line":49,"path":"src/SwarmExit.sol","reproduction":"Against any Ethereum mainnet JSON-RPC endpoint (block 26188160, 2026-10-14):\n1. `cast call 0x1F98431c8aD98523631AE4a59f267346ea31F984 'getPool(address,address,uint24)(address)' 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 10000` -> 0xd6a822d028bbf7b6edfa1533e110ee40c08551d9; the same call with 100, 500 and 3000 -> 0x0.\n2. `cast call 0xd6a822d028bbf7b6edfa1533e110ee40c08551d9 'liquidity()(uint128)'` -> 0; `balanceOf(pool)` on WETH -> 6612 wei, on IMD -> 436143244290069553 (0.436 IMD).\n3. `cast call 0x61fFE014bA17989E743c5F6cB21bF9697530B21e 'quoteExactInput(bytes,uint256)' $(cast concat-hex 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 0x002710 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) 1000000000000000000` -> execution reverted: Unexpected error (same for WETH->IMD with 1e16).\n4. `cast call 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f 'getPair(address,address)(address)' WETH IMD` -> 0x0; SushiSwap factory 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac -> 0x0.\n5. `cast call IMD 'balanceOf(address)(uint256)' 0x000000000004444c5dc75cB358380D2e3dE08A90` -> 222235223733826335980069 (v4 PoolManager).\nExpected: owner can configure `setImdRoute` so that `previewAll` marks held tokens sellable and `sellAll` settles in IMD. Actual: with the README's route (venue 2, SwapRouter02, QuoterV2, WETH|10000|IMD) `_quote` returns (false,0) and `_swap` reverts; every WETH-bridged token is QuoteFailed in preview and SaleFailed/refunded in `sellAll`; `sellAll` on only such tokens reverts NothingSold.","severity":"high","snippet":"    enum Venue {\n        None,\n        UniswapV2,\n        UniswapV3\n    }","title":"No supported venue reaches IMD liquidity on mainnet: the only V2/V3 WETH/IMD pool is empty, so every bridged sale is refunded and the product cannot sell anything"},{"citation":"resolved","description":"Boundary: external call `erc.safeTransfer(user, leftover)` in the catch block of `_sellOne`. Assumption: once the self-call `executeSale` has reverted, the refund back to the user always succeeds, so a failed sale is isolated to that token (README step 6, brief requirement 6). Actual: the refund runs outside the try, on an owner-registered token the contract does not control. Any token whose transfer from this contract to the user reverts after its sale reverted (anti-bot 'transfer delay' code keyed on tx.origin that exempts only the V2 router/pair, so the pull user->SwarmExit sets the origin's block, the transfer into a non-exempt pool reverts, and the refund to the user in the same block reverts; or a token that blocks transfers from a non-whitelisted sender) makes `safeTransfer` revert, which reverts `sellAll` entirely: the other tokens in the batch are not sold, and nothing is refunded by the mechanism the brief asked for. Because web/app.js relies on `sellAll.staticCall` returning per-token outcomes (app.js:178-191) to exclude failing tokens, a whole-call revert leaves it with no outcomes: it logs an error and the user cannot sell any of their tokens, with no control to deselect the offending one. No funds are lost (the transaction reverts), but requirement 5/6 ('refund the failed txn token', sell the rest) is broken for every wallet that holds one such token. Fix that keeps the design: make the refund best-effort (low-level call / `_callOptionalReturnBool`-style) and, when it fails, hold the pulled amount in a `refunds[user][token]` balance with a `claimRefund(token)` pull function (the owner's `rescue` already refuses swarm tokens, so held refunds cannot be swept), and still set `Status.SaleFailed`. The redundant `forceApprove` calls in the same catch block (finding 3) should go for the same reason.","line":387,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {IUniswapV2Router} from \"src/interfaces/IDexVenues.sol\";\n\n/// @dev Minimal constant-product pool shared by the mock router.\ncontract Pool {\n    using SafeERC20 for IERC20;\n\n    mapping(bytes32 => uint256) public rA;\n    mapping(bytes32 => uint256) public rB;\n\n    function key(address a, address b) public pure returns (bytes32, bool) {\n        return a < b ? (keccak256(abi.encode(a, b)), false) : (keccak256(abi.encode(b, a)), true);\n    }\n\n    function add(address a, address b, uint256 x, uint256 y) external {\n        IERC20(a).safeTransferFrom(msg.sender, address(this), x);\n        IERC20(b).safeTransferFrom(msg.sender, address(this), y);\n        (bytes32 k, bool f) = key(a, b);\n        if (f) (x, y) = (y, x);\n        rA[k] += x;\n        rB[k] += y;\n    }\n\n    function out(address i, address o, uint256 amt) public view returns (uint256) {\n        (bytes32 k, bool f) = key(i, o);\n        uint256 ri = f ? rB[k] : rA[k];\n        uint256 ro = f ? rA[k] : rB[k];\n        require(ri > 0 && ro > 0, \"no liquidity\");\n        uint256 w = amt * 997;\n        return (w * ro) / (ri * 1000 + w);\n    }\n\n    function swap(address i, address o, uint256 amt, address to) external returns (uint256 got) {\n        uint256 b = IERC20(i).balanceOf(address(this));\n        IERC20(i).safeTransferFrom(msg.sender, address(this), amt);\n        uint256 recv = IERC20(i).balanceOf(address(this)) - b;\n        got = out(i, o, recv);\n        (bytes32 k, bool f) = key(i, o);\n        if (f) {\n            rB[k] += recv;\n            rA[k] -= got;\n        } else {\n            rA[k] += recv;\n            rB[k] -= got;\n        }\n        IERC20(o).safeTransfer(to, got);\n    }\n}\n\ncontract Router is IUniswapV2Router {\n    using SafeERC20 for IERC20;\n\n    Pool public immutable pool;\n\n    constructor(Pool p) {\n        pool = p;\n    }\n\n    function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory a) {\n        a = new uint256[](path.length);\n        a[0] = amountIn;\n        for (uint256 i; i + 1 < path.length; ++i) {\n            a[i + 1] = pool.out(path[i], path[i + 1], a[i]);\n        }\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256 amountOutMin,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);\n        uint256 amt = IERC20(path[0]).balanceOf(address(this));\n        for (uint256 i; i + 1 < path.length; ++i) {\n            IERC20(path[i]).forceApprove(address(pool), amt);\n            address rcpt = i + 2 == path.length ? to : address(this);\n            amt = pool.swap(path[i], path[i + 1], amt, rcpt);\n        }\n        require(amt >= amountOutMin, \"INSUFFICIENT_OUTPUT_AMOUNT\");\n    }\n}\n\ncontract Plain is ERC20 {\n    constructor() ERC20(\"P\", \"P\") {}\n\n    function mint(address to, uint256 a) external {\n        _mint(to, a);\n    }\n}\n\n/// @dev \"Transfer delay\" anti-bot token (one transfer per block per tx.origin, exempting the router\n/// and the pair). The user's pull sets the origin's block; the sale's transfer into the pool then\n/// reverts, and so does the refund back to the user.\ncontract TransferDelayToken is ERC20 {\n    address public router;\n    mapping(address => uint256) public lastBlock;\n\n    constructor() ERC20(\"D\", \"D\") {}\n\n    function setRouter(address r) external {\n        router = r;\n    }\n\n    function mint(address to, uint256 a) external {\n        _mint(to, a);\n    }\n\n    function _update(address from, address to, uint256 value) internal override {\n        if (from != address(0) && to != router) {\n            require(lastBlock[tx.origin] < block.number, \"Only one transfer per block allowed.\");\n            lastBlock[tx.origin] = block.number;\n        }\n        super._update(from, to, value);\n    }\n}\n\ncontract RefundRevertTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    Plain imd;\n    Plain good;\n    TransferDelayToken bad;\n    Pool pool;\n    Router router;\n    address user = makeAddr(\"user\");\n\n    function setUp() public {\n        vm.etch(IMD_ADDR, address(new Plain()).code);\n        imd = Plain(IMD_ADDR);\n        pool = new Pool();\n        router = new Router(pool);\n        exit = new SwarmExit(OWNER);\n        good = new Plain();\n        bad = new TransferDelayToken();\n        bad.setRouter(address(router));\n\n        _liq(address(good), 1_000_000e18, 100_000e18);\n        _liq(address(bad), 1_000_000e18, 100_000e18);\n\n        address[] memory pGood = new address[](2);\n        pGood[0] = address(good);\n        pGood[1] = IMD_ADDR;\n        address[] memory pBad = new address[](2);\n        pBad[0] = address(bad);\n        pBad[1] = IMD_ADDR;\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(good), true);\n        exit.setSwarmToken(address(bad), true);\n        exit.setTokenRoute(address(good), SwarmExit.Venue.UniswapV2, address(router), address(0), abi.encode(pGood));\n        exit.setTokenRoute(address(bad), SwarmExit.Venue.UniswapV2, address(router), address(0), abi.encode(pBad));\n        vm.stopPrank();\n\n        good.mint(user, 1_000e18);\n        bad.mint(user, 1_000e18);\n        vm.startPrank(user);\n        good.approve(address(exit), type(uint256).max);\n        bad.approve(address(exit), type(uint256).max);\n        vm.stopPrank();\n    }\n\n    function _liq(address t, uint256 x, uint256 y) internal {\n        address lp = makeAddr(\"lp\");\n        Plain(t).mint(lp, x);\n        imd.mint(lp, y);\n        vm.startPrank(lp);\n        IERC20(t).approve(address(pool), x);\n        imd.approve(address(pool), y);\n        pool.add(t, IMD_ADDR, x, y);\n        vm.stopPrank();\n    }\n\n    /// Expected: `bad` fails to sell and is refunded (or held for the user), `good` is sold.\n    /// Actual on the current code: the refund transfer in the catch block reverts and the whole\n    /// `sellAll` reverts, so `good` cannot be sold either.\n    function test_refundThatRevertsMustNotBlockOtherSales() public {\n        address[] memory tokens = new address[](2);\n        tokens[0] = address(bad);\n        tokens[1] = address(good);\n        uint256[] memory mins = new uint256[](2);\n        uint256 badBefore = bad.balanceOf(user);\n\n        vm.prank(user, user);\n        (SwarmExit.Outcome[] memory out,,,) = exit.sellAll(tokens, mins, block.timestamp);\n\n        assertTrue(out[1].status == SwarmExit.Status.Sold, \"good token sold\");\n        assertGt(imd.balanceOf(user), 0, \"user received IMD for the good token\");\n        assertTrue(out[0].status == SwarmExit.Status.SaleFailed, \"bad token not sold\");\n        assertEq(bad.balanceOf(user) + bad.balanceOf(address(exit)), badBefore, \"bad token not lost\");\n    }\n}","reproduction":"Proof file test/scratch/RefundRevert.t.sol (fails on this tree with 'Only one transfer per block allowed.'). State: SwarmExit with two registered V2 routes direct to IMD: `good` (plain ERC20) and `bad` (ERC20 whose _update requires lastBlock[tx.origin] < block.number for every transfer not addressed to the router, then records block.number). User holds 1,000e18 of each, both approved. Call: `vm.prank(user, user); exit.sellAll([bad, good], [0, 0], block.timestamp)`. Trace: `_sellOne(bad)` pulls 1,000e18 (to = SwarmExit, records block), `executeSale` -> router -> pool transfer reverts (delay), caught; catch block `erc.safeTransfer(user, 1000e18)` reverts (delay) -> whole `sellAll` reverts. Expected: outcomes[0].status == SaleFailed with the bad tokens refunded or held for the user, outcomes[1].status == Sold, user receives IMD for `good`. Actual: revert 'Only one transfer per block allowed.', nothing sold.","severity":"medium","snippet":"            if (leftover > 0) erc.safeTransfer(user, leftover);","title":"A refund transfer that reverts escalates one failed sale into reverting the whole batch, and the frontend simulation then cannot exclude the token"},{"citation":"resolved","description":"Boundary: external calls `erc.forceApprove(route.router, 0)` and `IERC20(route.tokenOut).forceApprove(_imdRoute.router, 0)` in the catch path of `_sellOne`. Assumption: allowances left by the failed `executeSale` must be reset. Actual: `executeSale` is an external self-call; when it reverts the EVM discards every allowance it set, so both resets are no-ops in every reachable state (the allowance was 0 before the call and the `before`/`leftover` accounting does not depend on them). They only add a failure surface: a token whose `approve` reverts when the spender is the router (anti-bot spender blocklists) fails inside `_swap`'s forceApprove (caught) and then again in the catch block (not caught), so the entire `sellAll` reverts instead of refunding that token and selling the others; the frontend simulation reverts as a whole and cannot exclude the token (same consequence as finding 2). Fix: delete the two `forceApprove` calls from the catch block.","line":382,"path":"src/SwarmExit.sol","reproduction":"test/scratch/Misc.t.sol::test_approveBlockTokenRevertsWholeCall. State: token `ab` whose `_approve` reverts with 'AB: spender blocked' when spender == the V2 router; registered with a V2 route ab->WETH, plus `tokenC` with a route direct to IMD; user holds 10,000e18 ab and 1,000e18 C, both approved to SwarmExit (approving SwarmExit is allowed). Call: `sellAll([ab, tokenC], [0,0], block.timestamp)`. Expected: outcomes[0] SaleFailed with 10,000e18 ab refunded, outcomes[1] Sold. Actual: revert 'AB: spender blocked' from the catch block's `erc.forceApprove(route.router, 0)`; nothing sold.","severity":"low","snippet":"            erc.forceApprove(route.router, 0);\n            if (route.tokenOut != address(IMD) && _imdRoute.venue != Venue.None) {\n                IERC20(route.tokenOut).forceApprove(_imdRoute.router, 0);\n            }","title":"Catch block calls forceApprove on the failing token outside the try; a token whose approve reverts for the router turns a refundable failure into a whole-call revert"},{"citation":"resolved","description":"Boundary x invariant seam between contract and frontend. `_sellOne` reads `balance = erc.balanceOf(user)` at execution time and pulls all of it; web/app.js `ensureApprovals` (app.js:155-158) approves exactly `r.balance` as read by `previewAll` at refresh time. If the balance grows by even 1 wei between the approval and the block that includes `sellAll` (an airdrop, a pending transfer, a positive rebase, a reflection-style token crediting holders on every trade, which is common for launched meme tokens), `safeTransferFrom` reverts with ERC20InsufficientAllowance and the whole batch reverts, including every other token (no funds lost, but requirement 3 fails and the user must re-approve and retry). Reflection tokens make this systematic: every other holder's trade in the mempool changes the user's balance. Fix in the contract (keeps the exact-approval UX): pull `min(balanceOf(user), allowance(user, this))` and treat the shortfall as dust, or in the site approve `type(uint256).max` / the balance plus a margin.","line":368,"path":"src/SwarmExit.sol","reproduction":"test/scratch/Misc.t.sol::test_exactApprovalThenBalanceGrowsRevertsWholeCall. State: user holds 1,000e18 tokenC (V2 route direct to IMD), approves exactly 1,000e18 to SwarmExit, then receives 1 wei more. Call: `sellAll([tokenC], [0], block.timestamp)`. Expected: 1,000e18 (or 1,000e18+1) sold. Actual: revert ERC20InsufficientAllowance(SwarmExit, 1000e18, 1000e18+1).","severity":"low","snippet":"        erc.safeTransferFrom(user, address(this), balance);","title":"sellAll pulls the full live balance while the site approves the exact balance read earlier, so any incoming transfer between approval and inclusion reverts the whole sale"},{"citation":"resolved","description":"Precision x invariant (query/do drift): `_previewOne` quotes `p.balance`, the user's wallet balance, while `_sellOne` sells `received`, the balance delta after the pull. For a fee-on-transfer token (README says these are supported through V2 routes) the sale amount is smaller than the quoted amount by the transfer fee, so `expectedImd` overstates the real proceeds; the site shows that number as the token's IMD and USD value (app.js:129-137) and in the totals, and the README's own step 5 and the NatSpec of `minImdOuts` describe minimums derived from the expected IMD. With the README's 3% slippage applied to the preview of a 5% fee token, the on-chain `minOut` exceeds what the sale yields, `executeSale` reverts SlippageExceeded and the token is refunded every time (the site avoids this only because it re-quotes from a zero-minimum `sellAll.staticCall`; a direct caller or any other integrator does not). Fix: quote `balance - expectedFee` is not knowable generically, so quote in `preview` with the same measurement the sale uses, or state in the Preview struct that the figure is pre-fee and let the site derive minimums only from the simulation (it already does); at minimum do not display the overstated USD value for tokens whose first leg is a fee-on-transfer route.","line":431,"path":"src/SwarmExit.sol","reproduction":"test/scratch/Misc.t.sol::test_previewOverstatesFeeOnTransfer. State: `feeTok` burns 5% on every transfer, V2 route feeTok->WETH then WETH->IMD, user holds 10,000e18. `preview(user,[feeTok])[0].expectedImd` = 8,960,114,241,323,618,637,858 (8,960 IMD). `sellAll([feeTok],[0],now)` -> outcomes[0].imdOut = 7,791,738,712,179,540,407,553 (7,792 IMD), 13% less in this mock (one fee per hop). Expected: preview within slippage of execution. Actual: 8,960 * 0.97 = 8,691 > 7,792, so a minimum of 3% under the preview is refunded with SlippageExceeded(7791..., 8691...).","severity":"low","snippet":"        (bool ok, uint256 out) = _quote(first, p.balance);","title":"preview quotes the pre-fee balance for fee-on-transfer tokens, so expectedImd and the USD value are overstated and a minimum derived from it is always refunded"},{"citation":"resolved","description":"Boundary x invariant. Requirement 5 asks the protocol to sell only tokens that 'have enough liquidity to be sold'. The implementation's definition is: the quote succeeds and the quoted IMD is at least `minImdOut` (default `DEFAULT_MIN_IMD_OUT = 1e15`, 0.001 IMD, an absolute amount independent of the holding's size or of IMD's price). A quote succeeds for any pool with non-zero reserves, however thin, so a holding whose fair (reserve-ratio) value is 1 IMD but which the pool can only pay 0.4977 IMD for (50% impact) is `sellable == true`; at 100,000 IMD fair value and a pool paying 1 IMD it is still sellable. The site then takes the simulated output as the quote and derives the minimum from it (app.js:200), so the 3% slippage guard protects only against movement after the simulation, never against the pool being too thin: the user's one click sells the whole holding into the thin pool at whatever it pays. No funds are stolen, but the 'enough liquidity' guarantee the brief asks for is not implemented, and `minImdOut` cannot express it. Fix: in `_previewOne` compare the quote with a reference (quote of a small amount scaled up, or the pool's spot) and mark the token `InsufficientLiquidity` above an owner-set `maxImpactBps`; or state in the README that only the absolute floor is enforced and show the impact on the site.","line":449,"path":"src/SwarmExit.sol","reproduction":"test/scratch/Misc.t.sol::test_thinPoolSellable. State: token `thin` with a V2 pool of 1e24 thin against 1e15 wei WETH (0.001 WETH), WETH/IMD at 1,000 IMD per WETH, user holds 1e24 thin (value at the reserve ratio = 0.001 WETH = 1 IMD). `preview(user,[thin])[0]` -> expectedImd = 497,748,649,140,524,926 (0.4977 IMD), sellable == true, status Sold. Expected per requirement 5: not sellable / flagged as insufficient liquidity (50% price impact). Actual: listed as sellable and included in the one-click sale.","severity":"low","snippet":"        if (out < minImdOut) {","title":"Sellability has no price-impact bound: the only liquidity gate is an absolute 0.001 IMD floor, so a holding that would move its pool 50% is marked sellable"},{"citation":"resolved","description":"Boundary in the site's minimum computation. The slippage field is `<input type=\"number\" min=\"0\" max=\"5000\">` (index.html:55) with no `step`, and the browser still returns a decimal string from `.value`. `Number('2.5')` is 2.5, the clamp keeps it, and `BigInt(2.5)` throws RangeError ('The number 2.5 cannot be converted to a BigInt because it is not an integer'). `sellAll()` catches it after `ensureApprovals` has already sent the approval transactions, logs 'Error: ...' and no sale is sent. Fix: `BigInt(Math.round(...))` or `Math.trunc`, and set `step=\"1\"` on the input.","line":148,"path":"web/app.js","reproduction":"Open the page, connect, type 2.5 (or 0.5) in the Slippage (bps) field, click Sell. Expected: minimums 2.5 bps (or rounded) under the simulated outputs and the transaction sent. Actual: after the approvals, log shows 'Error: The number 2.5 cannot be converted to a BigInt because it is not an integer' and no sellAll transaction is sent.","severity":"low","snippet":"    const v = BigInt(Math.max(0, Math.min(5000, Number($(\"slippage\").value) || 0)));","title":"A non-integer slippage value throws in BigInt() and disables the sell flow"},{"citation":"resolved","description":"Math precision: fees should round up in the protocol's favour. `tax = grossImd * 200 / 10000` truncates, so each `sellAll` pays the treasury at most 1 wei of IMD less than 2% and a call with `grossImd < 50` pays zero tax. Economically negligible (1 wei per call, not farmable for value) and `grossImd * 200` cannot overflow for any real IMD supply (total supply is about 4.2e24 wei). Noted for completeness; if the 2% is meant to be a floor, use `Math.ceilDiv` or `(grossImd * SELL_TAX_BPS + BPS - 1) / BPS`.","line":318,"path":"src/SwarmExit.sol","reproduction":"test/scratch/Misc.t.sol::test_taxRoundsDown: grossImd = 49 -> tax 0 (expected 1 if rounding up); grossImd = 149 -> tax 2 (2.98 truncated). Expected 2% of 49 wei = 0.98 -> 1 wei with ceiling; actual 0.","severity":"info","snippet":"        tax = (grossImd * SELL_TAX_BPS) / BPS;","title":"Sell tax rounds down, so the treasury loses up to 1 wei per call and gross proceeds below 50 wei pay no tax"}],"hash":"1cbc7f45652f8ea2c7922dfcca927720692d236438ad2b543f4b65df49eb4a27","nodeId":"8c925ecd-7f57-4ea3-a429-73f47c18ddd4","outcome":"completed","summary":"The review is complete. The findings file `.imd-findings.json` holds 8 findings and 18 coverage rows, with one Foundry proof. No source, test or config file was changed; my scratch tests live only under `test/scratch/`.\n\n**Findings, most severe first**\n\n- **High. No supported venue reaches IMD liquidity on mainnet.** The contract routes only through Uniswap V2 and V3. The README's intended IMD route is the V3 1% WETH/IMD pool, but on mainnet at block 26188160 that pool has zero liquidity, holds dust, and QuoterV2 reverts for it. No other V2, V3 or SushiSwap WETH/IMD pool exists. IMD liquidity is in the Uniswap v4 PoolManager, which this version cannot route. Every WETH-bridged token would be reported QuoteFailed and refunded, so the product cannot settle in IMD. No no-fork proof can express an on-chain fact, so the reproduction is a list of `cast call` reads.\n- **Medium. A refund that reverts escalates one failed sale into reverting the whole batch** (`src/SwarmExit.sol:387`). The refund transfer runs outside the try. A token whose transfer back to the user reverts after its sale reverted, such as transfer-delay anti-bot code keyed on tx.origin, makes the whole `sellAll` revert, and the site's simulation then returns no outcomes to exclude it, so the user can sell nothing. Proof test attached; it fails on this tree.\n- **Low.** The catch block's two `forceApprove` resets are no-ops after a reverted self-call and only add a revert surface (a token whose approve rejects the router reverts the whole batch).\n- **Low.** Full live-balance pull versus the site's exact-balance approval: one incoming wei between approval and inclusion reverts the entire sale.\n- **Low.** `preview` quotes the pre-fee balance for fee-on-transfer tokens, overstating IMD and USD values. A 3% minimum derived from it is always refunded.\n- **Low.** Sellability has no price-impact bound. The only gate is an absolute 0.001 IMD floor, so a holding with 50% impact is marked sellable.\n- **Low.** A non-integer slippage value throws in `BigInt()` in the site after approvals were already sent.\n- **Info.** Sell tax rounds down, so gross proceeds under 50 wei pay no tax.\n\n**Coverage.** All 16 entry points have a row. The route math held up: I hand-traced the V3 path reversal for one and two hops, the V2 reversal, the path-length boundaries, the IMD balance-delta accounting, and tax conservation. Nothing in my area was left unreached.","treeHash":null,"usage":{"cachedInputTokens":1901633,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":52873,"runtime":"claude","turns":29,"wallClockMs":750601}},{"artifacts":[],"attempt":1,"bundleHash":"68bd1921e8ec7bdd13004ed3da73a262c925c9a031fa98e90d4eabe2311846c1","device":"66ceb1fcb4b0b455","findings":[],"hash":"634bf29025e47f67fb71a3e6abb92aceb74da5af7d59b931d3c6d3f92e8de466","nodeId":"32c833d4-7acc-4db8-9e43-826b544036d4","outcome":"completed","summary":"I fixed and tested every finding from the review. The four proof tests now pass, and `forge build`, `forge test` (63 tests, run with the verifier's fuzz seed) and `forge fmt --check` all pass. Nothing is committed; the answers to all 18 findings are in `.imd-responses.json`.\n\n**High finding: no reachable IMD liquidity.** The reviewer was right. I checked mainnet: IMD's only funded pool is a Uniswap v4 pool against native ETH (1% fee, tick spacing 200, no hooks, about 222k IMD). The old V3 pool is empty.\n- **v4 support:** I added a v4 route type to `SwarmExit` that swaps directly through Uniswap's v4 pool contract (the PoolManager). Native ETH is converted to and from WETH, so the contract never holds raw ETH.\n- **What it connects:** a token's existing V2/V3 route to WETH now chains into the v4 ETH/IMD pool, and a token launched on v4 can sell to ETH and then IMD in one step.\n- **Quotes:** the contract quotes v4 swaps by simulating them, so it needs no extra quoter address. A pool that can only fill part of a sale counts as a failed sale.\n- **Tests:** 15 new tests in `test/SwarmExitV4.t.sol`, run against a mock PoolManager.\n- **Mainnet check:** I also ran a one-off test against a mainnet fork. 0.01 WETH sold through the real pool for exactly the previewed 2.879 IMD, and the real IMD token paid the 2% tax and the user's share exactly. This test is not committed because the verifier has no network.\n- **Owner setting:** the README's \"After launch\" section now gives the exact `setImdRoute` value for this pool.\n\n**Medium findings: one redesign covers four of them.** Each token's pull from the wallet now happens inside the same all-or-nothing step as its swaps. If anything fails, that token never leaves the user's wallet and the other tokens still sell. As a result:\n- There is no refund transfer any more, so a token that blocks transfers back to the user can't stop the whole sale.\n- The failure path no longer calls into the failing token, which fixes the approve-blocking honeypot.\n- A failed sale of a fee-on-transfer token now costs the user nothing; before, it lost about twice the fee.\n- The contract pulls the smaller of the user's balance and their approval, so a 1 wei donation can't block the sale.\n- A sale that returns zero IMD, or less than the contract's dust minimum (`minImdOut`), is now treated as failed, so the on-chain sale and the preview agree.\n\n**Low and info findings:**\n- **Contract:** unregistering a token now deletes its route, and token or router addresses without contract code are refused. One broken token can no longer stop the website from listing the others.\n- **Website:** token symbols are escaped (closing the script-injection hole), decimal slippage values no longer crash the sale, and wallets with more than 50 tokens sell in batches.\n- **Documentation only:** the README now states that routes are fully trusted owner settings, that the rescue guard can be bypassed by the owner, that preview values are before any transfer fee, and that there is no price-impact limit.\n\n**Two findings disputed:**\n- The 2% tax rounds down, by at most 1 wei of IMD per sale. That's negligible and in the user's favour, so I left it and documented it.\n- Preview alone can't spot a honeypot. The website's test run of the sale catches it, and on chain such a token's sale simply fails without affecting the others.\n\n**Before launch:**\n- Re-check the IMD pool before setting the route, in case the liquidity moves to another pool.\n- Token sales can only take more than the user's slippage limit if the owner sets a bad route or passes zero minimums.\n- An independent security review is still needed before this holds users' funds.","treeHash":"8a2e35d4a808bf78dcaaf0fa72eae158aa7cc0e3","usage":{"cachedInputTokens":3889360,"inputTokens":66,"model":"claude-opus-5-5","outputTokens":68007,"runtime":"claude","turns":35,"wallClockMs":721833}},{"artifacts":[],"attempt":1,"bundleHash":"9da51cd726fcac51be63dbbc7804ff19728cd2ee51288ec4edafa75b42dc6fa2","device":"584bd638e7e022f0","findings":[],"hash":"6c565e8becc6abd4a1fdc822db77f09db632a5f3e753ff86eacdf36b46aaa37c","nodeId":"32c833d4-7acc-4db8-9e43-826b544036d4","outcome":"completed","summary":"The revision is complete and verified.\n\n**Finding 59ff49b7 (fee-on-transfer tokens cannot settle through a v4 route): fixed.** It reproduced exactly as reported. The proof copied under test/scratch failed on the starting tree with status SaleFailed and the user left holding 9,025 tokens after two 5% fees and no IMD.\n\n**Cause.** The v4 callback in src/SwarmExit.sol ran the swap hops on the full input first and only then transferred and settled. The PoolManager credited the post-fee amount, the currency delta stayed negative, and unlock reverted with CurrencyNotSettled.\n\n**Change made.** In sale mode the callback now pays first and swaps what was credited:\n\n- sync, transfer and settle for an ERC20 input, or settle with value for native ETH\n- the amount settle returns is the measured input, and a zero credit reverts NoProceeds\n- the hops swap that measured amount, keeping the InsufficientLiquidity check on partial fills, then the output is taken\n- quote mode is unchanged: it swaps the requested amount and reverts with V4Quote\n\n**Tests and docs.** Two tests were added to test/SwarmExitRevision.t.sol. One sells a 5% fee token on a v4 route in a batch with a plain token and asserts both are sold, the fee token's proceeds equal the pool output for the twice-feed amount, nothing rests in the contract, and the manager's delta is zero. The other asserts the native ETH leg still settles first and sells everything. The README trust assumptions now describe the settle-first callback and that preview overstates a v4 fee-on-transfer sale by two fees. The answer to the reviewer is in .imd-responses.json at the repository root.\n\n**Verification.**\n\n| Check | Result |\n|---|---|\n| forge build | success |\n| forge test with the verifier's fuzz seed | 64 passed, 0 failed (includes the proof copy) |\n| forge fmt --check | clean |\n\nThe diff touches only the callback, the two new tests and two README lines. The static-analysis findings quoted in the brief referred to an earlier rejected attempt, not to the accepted tree this revision started from, so nothing else was changed.","treeHash":"036ae8c63f27f4fa6d27c6d8431657214d241d13","usage":{"cachedInputTokens":1293381,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":10230,"runtime":"claude","turns":21,"wallClockMs":167587}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"790f4e545968ee92","findings":[{"citation":"resolved","description":"Asymmetry between the happy path and the refund path. `_sellOne` pulls the user's whole balance (line 368) before it knows whether `executeSale` will succeed; for a fee-on-transfer token the pull burns one fee and the refund transfer (line 387) burns a second one. The README and the brief (item 6) promise that a failed sale refunds the token to the user; for these tokens the refund is partial and the shortfall is burned, not paid to anyone. The failure can be triggered by anyone, not just the user: any sandwich or ordinary price move that pushes the output below the user's `minImdOuts[i]`, a missing bridge route, or a honeypot pool makes the sale fail and the user pays the double fee for nothing. The README explicitly routes fee-on-transfer tokens here (`Fee-on-transfer tokens sell through V2 routes`), so this class is in scope by design. Minimal fix preserving the design: before pulling, run `_quote(route, balance)` (and the bridge quote) and skip the token with a new status (e.g. `QuoteBelowMinimum`) when the quote is already below `minOut` or fails; this avoids the pull for the predictable failures (missing bridge route, dead pool, stale min) and leaves only genuine same-block price moves exposed. Also document the residual cost for fee-on-transfer tokens.","line":386,"path":"src/SwarmExit.sol","reproduction":"State: FeeOnTransferToken with 5% fee (test/mocks/MockTokens.sol) registered with a V2 route; user holds 10_000e18 and approved SwarmExit; tokenA registered too so the batch settles. Call: user.sellAll([fee, tokenA], [quote+1, 0], now). Expected (README/brief): fee token sale fails and the user gets the tokens back. Actual: outcome[0].status == SaleFailed, TokenRefunded emitted, but user's fee-token balance is 9_025e18: 500e18 burned on the pull, 475e18 burned on the refund, 0 IMD received. Confirmed with test/scratch/Leads.t.sol::test_failedFeeTokenSaleRefundsPartially (passes, printing 9025e18).","severity":"medium","snippet":"            uint256 leftover = erc.balanceOf(address(this)) - before;\n            if (leftover > 0) erc.safeTransfer(user, leftover);","title":"Failed sale of a fee-on-transfer token refunds net of two transfer fees: the user loses ~2x the token fee and receives no IMD"},{"citation":"resolved","description":"The amount pulled is read on chain from `balanceOf(user)` (line 362) and transferred with `safeTransferFrom(user, this, balance)` (line 368), while web/app.js:158 approves exactly `r.balance`. The user does not control their own balance: any third party can transfer 1 wei of a registered swarm token to the user, in the same block before the user's `sellAll`, and the pull then exceeds the allowance. `safeTransferFrom` reverts and the entire batch reverts (`test_missingApprovalRevertsWholeCall` shows the whole-revert behaviour). The griefer can repeat this each time the user re-approves, at the cost of 1 wei of token and gas, so the one-click exit is deniable per target. This is an asymmetry between the amount approved (chosen by the user off chain) and the amount pulled (chosen by whoever last touched the user's balance). Minimal fix: pull `min(balanceOf(user), allowance(user, address(this)))`, or let the caller pass per-token amounts, or have the website approve `type(uint256).max`/a margin. Also unrelated front-running by the user's own incoming transfers (airdrops, vesting) trips the same revert.","line":362,"path":"src/SwarmExit.sol","reproduction":"State: tokenA registered with a working route; user holds 10_000e18 tokenA and calls tokenA.approve(exit, 10_000e18) as the website does; griefer holds 1 wei tokenA. Sequence: griefer.transfer(user, 1); user.sellAll([tokenA], [0], now). Expected: user's 10_000e18 (or 10_000e18 + 1) sold. Actual: revert ERC20InsufficientAllowance(exit, 10_000e18, 10_000e18 + 1); nothing sold. Confirmed with test/scratch/Leads.t.sol::test_dustTransferBlocksSellAll.","severity":"low","snippet":"        uint256 balance = erc.balanceOf(user);","title":"Anyone can block a user's sellAll with a 1 wei transfer because the contract pulls balanceOf(user) while the website approves the exact balance"},{"citation":"resolved","description":"Trust gap (access x economics x asymmetry). `setTokenRoute`/`setImdRoute` accept any non-zero `router` without checking it is a Uniswap router, and `_swap` approves the full `amountIn` to it and swaps with `amountOutMinimum: 0` (src/SwarmExit.sol:402 and :407). The only bound on what the user receives is the per-token `minImdOuts[i]` the user passes; the sale is reported `Sold` for any output >= that minimum, including 0 when the minimum is 0. The website computes the minimum from a simulation against the routes as they are at simulation time, then sends a transaction that executes against the routes as they are at inclusion time. Because the owner can change a route in between (there is no timelock and `sellAll` does not bind the caller to the routes it simulated), the owner, or anyone holding the owner key, can front-run every `sellAll` with a route to a router that returns exactly the caller's minimum and keeps the remainder: with the website default of 300 bps that is 3% of every sale, on top of the 2% treasury tax, and for any caller who passes a 0 minimum it is 100% of that token while the outcome still says Sold. This is an owner power and the design asks for an owner, so it is reported as a misdocumented trust assumption rather than a bypass. The README line cited is wrong as written and should state the actual bound (owner can redirect up to the caller's slippage tolerance, and all of a zero-minimum sale). Design-preserving hardening: let `sellAll` take a `bytes32 routesHash` (hash of the router/data of each token route plus the IMD route) and refund instead of selling when the stored routes no longer match, so a route change between simulation and execution can never execute; or add a delay to route changes.","line":60,"path":"README.md","reproduction":"State: tokenA registered with an honest V2 route quoting 1000e18 IMD for the user's 10_000e18. User approves and (as the website does) prepares sellAll([tokenA], [970e18], now). Owner front-runs with setTokenRoute(tokenA, UniswapV2, greedyRouter, 0, abi.encode([tokenA, IMD])) where greedyRouter.getAmountsOut still quotes 1000e18 but its swap pulls amountIn and pays exactly 970e18. Expected per README: a wrong route cannot steal funds. Actual: outcome.status == Sold, grossImd == 970e18, greedyRouter holds all 10_000e18 tokenA; the 30e18 IMD difference is kept by the router. With minImdOuts = [0,0] and a second token in the batch, outcome[0].status == Sold with imdOut == 0 and the user's tokenA balance is 0. Confirmed with test/scratch/Leads.t.sol::test_ownerRouteSwapTakesSlippage.","severity":"low","snippet":"- The owner decides which tokens count as swarm-launched and which pools sell them. A wrong route cannot steal funds (a failed sale refunds) but can make a token unsellable. The owner can pause selling.","title":"Owner-set router is arbitrary code that receives the user's full pulled balance with amountOutMin 0; README's claim that a wrong route cannot steal funds is false, and a route change between simulatio"},{"citation":"resolved","description":"Register/unregister asymmetry. `setTokenRoute` requires `isSwarmToken[token]` (line 172), so a route is only meant to exist for a registered token, but the unregister branch of `setSwarmToken` removes the token from `isSwarmToken` and `_swarmTokens` without touching `_tokenRoutes[token]`. Consequences: `tokenRoute(token)` reports a live route for a token the contract says is not a swarm token; and if the owner later re-registers the token (for example after unregistering it because its pool was drained or its route was wrong) the old route is immediately active again without a `setTokenRoute` call, so users can be routed through a venue the owner had retired. Minimal fix: `delete _tokenRoutes[token]` in the unregister branch (and emit `TokenRouteSet(token, None, ...)`).","line":154,"path":"src/SwarmExit.sol","reproduction":"State: tokenA registered with a V2 route (setUp of the existing suite). Sequence: owner.setSwarmToken(tokenA, false); tokenRoute(tokenA).venue -> expected None, actual UniswapV2. owner.setSwarmToken(tokenA, true); preview(user, [tokenA])[0].sellable -> expected false (NoRoute until the owner sets a route again), actual true through the stale route. Confirmed with test/scratch/Leads.t.sol::test_unregisterKeepsRoute.","severity":"low","snippet":"        } else {\n            uint256 n = _swarmTokens.length;\n            for (uint256 i; i < n; ++i) {\n                if (_swarmTokens[i] == token) {\n                    _swarmTokens[i] = _swarmTokens[n - 1];\n                    _swarmTokens.pop();\n                    break;\n                }\n            }","title":"setSwarmToken(token, false) leaves the token's route in storage; re-registering silently resurrects it"},{"citation":"resolved","description":"Requirement 5 says a token that is not sellable must not start a transaction and must be highlighted. On chain, `_sellOne` refunds only failures inside `executeSale`; a failure in the pull itself (`safeTransferFrom` at src/SwarmExit.sol:368: token paused, user blacklisted, honeypot that blocks transfers out of wallets, insufficient allowance) reverts the entire `sellAll`, which the README acknowledges. The page relies on one `staticCall` of `sellAll` over every token that `previewAll` marked sellable, and `previewAll` only quotes pools, so such a token passes preview. When the single simulation reverts, the `catch` at the end of `sellAll()` logs a generic error, no row is highlighted, and `selected` is never narrowed, so the user can sell nothing from the page while holding one such token, even though every other token is fine. Minimal fix: on a simulation revert, bisect (simulate each selected token alone, or drop tokens one at a time), mark the culprit with status 7 and continue with the rest; or simulate per token first.","line":178,"path":"web/app.js","reproduction":"State: register a token whose `_update` reverts when `from == user` (or simply give the user a registered token with a working pool but revoke/never grant the allowance to SwarmExit after preview; `test_missingApprovalRevertsWholeCall` shows the revert). previewAll(user) marks it sellable (quotes succeed). Click Sell: `exit.sellAll.staticCall([...all sellable...], zeros, deadline)` rejects; expected: that row turns red with a reason and the remaining tokens are sold; actual: log shows `Error: ...`, `rows` unchanged, no transaction is possible until the user removes the token from their wallet by other means.","severity":"low","snippet":"      const sim = await exit.sellAll.staticCall(selected.map((r) => r.token), selected.map(() => 0n), deadline());","title":"Website cannot highlight or exclude a token whose transferFrom reverts: the whole simulation throws and no token can be sold from the page"},{"citation":"resolved","description":"The check reads a flag the same `onlyOwner` role writes, so the owner can always `setSwarmToken(token, false)` and then `rescue(token, to, amount)`. The README presents it as the reason the owner cannot claim a user's tokens in flight; the real reason is that `sellAll` is `nonReentrant` and settles or refunds in one transaction, so swarm tokens never rest in the contract. No funds are at risk today; this is a defensive check whose stated guarantee is weaker than documented. Either document it as a convenience guard or make it meaningful (for example refuse to rescue any token that currently has a route, or any token ever registered).","line":222,"path":"src/SwarmExit.sol","reproduction":"Sequence: tokenA.mint(exit, 1e18) (stray dust); owner.rescue(tokenA, owner, 1e18) -> reverts CannotRescueSwarmToken; owner.setSwarmToken(tokenA, false); owner.rescue(tokenA, owner, 1e18) -> succeeds; owner.setSwarmToken(tokenA, true) restores the registry. Expected per README: registered swarm tokens cannot be rescued; actual: the same key rescues them in two calls.","severity":"info","snippet":"        if (isSwarmToken[token]) revert CannotRescueSwarmToken();","title":"rescue's swarm-token guard is bypassable by the same role via setSwarmToken(token,false); it protects nothing beyond what nonReentrant already does"}],"hash":"6e5d6558265f6175684f7f868596d9584329be6c33888c28e832ecfc3624250d","nodeId":"3cb511e5-190b-49df-9387-8c4865dab0a9","outcome":"completed","summary":"Review complete. The findings are in `.imd-findings.json` with coverage rows for all 16 entry points plus three invariants. Only the findings file and my scratch test under `test/scratch/` were created; no project file was changed.\n\n**Outcome.** No critical or high defect in the access control, trust gap or asymmetry area. All owner setters are `onlyOwner`, `executeSale` is self-only, ownership is two-step, the treasury and tax are constants, and IMD never rests in the contract. I found one medium and four low defects, plus one informational note. Each was reproduced with a scratch Foundry test that passes against the current code.\n\n**Findings, most severe first:**\n\n1. **Medium. Partial refund of fee-on-transfer tokens.** A failed sale pulls the whole balance first, then refunds it, so a 5% fee token loses 9.75% of the holding and the user receives no IMD. The brief promises a full refund. Any price move or sandwich below the user's minimum triggers it.\n2. **Low. One-wei grief blocks sellAll.** The contract pulls `balanceOf(user)` while the website approves the exact balance. Anyone sending 1 wei of a swarm token to the user before their transaction makes the whole batch revert, repeatably.\n3. **Low. Router is arbitrary owner-set code; README claim is false.** A route change between simulation and execution lets the owner key take the caller's full slippage allowance, or everything for a zero-minimum sale, while the outcome still reads Sold. Reported as a misdocumented trust assumption, with a design-preserving hardening.\n4. **Low. Unregistering a token keeps its route.** Re-registering silently resurrects the old venue without a new `setTokenRoute`.\n5. **Low. Website cannot isolate a token whose pull reverts.** One such token makes the single simulation throw, nothing is highlighted, and no token can be sold from the page.\n6. **Info. The rescue guard is bypassable by the same owner role** in two calls. Nothing rests in the contract, so no funds are exposed.\n\n**Coverage.** Every listed entry point has a row. Static-analysis leads were checked: the reentrancy-balance and strict-equality lines are delta accounting guarded by `nonReentrant` and intentional status checks, and the aderyn storage-array lead on line 281 is a false positive, so none became a finding on its own.","treeHash":null,"usage":{"cachedInputTokens":950175,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":32272,"runtime":"claude","turns":25,"wallClockMs":487841}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7eaf1cd0fb3df712","findings":[{"citation":"resolved","description":"_sellOne reads the user's live balance and calls safeTransferFrom for that whole amount (lines 362-368). The amount is not a parameter and the pull is outside the try/catch, so it must succeed for every token in the list or the entire sellAll reverts. The website (web/app.js ensureApprovals, line 152-162) approves exactly the balance it displayed and skips re-approval when allowance >= that stale balance. Any increase of the user's balance between approval and execution therefore makes transferFrom fail with ERC20InsufficientAllowance and nothing is sold. Anyone can cause this on purpose: front-run or simply precede the user's sellAll with a transfer of 1 wei of any listed token to the user. Reflection tokens (balances grow on every transfer, common among launched memecoins) trigger it without an attacker. The user sees only a reverted transaction; the website cannot tell which token caused it, and re-approving the new exact balance can be griefed again at a cost of 1 wei per attempt. Fix (keeps the design): pull min(balanceOf(user), allowance(user, this)) or accept an explicit amounts[] array, and/or have the website approve type(uint256).max or re-read balances immediately before sending.","line":362,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {IUniswapV2Router} from \"src/interfaces/IDexVenues.sol\";\n\ncontract PlainToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Minimal V2-style router: a fixed 1:2 rate, pulls exactly amountIn, pays tokenOut it holds.\ncontract FixedRateRouter is IUniswapV2Router {\n    using SafeERC20 for IERC20;\n\n    function getAmountsOut(uint256 amountIn, address[] calldata path)\n        external\n        pure\n        returns (uint256[] memory amounts)\n    {\n        amounts = new uint256[](path.length);\n        amounts[0] = amountIn;\n        amounts[path.length - 1] = amountIn * 2;\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);\n        IERC20(path[path.length - 1]).safeTransfer(to, amountIn * 2);\n    }\n}\n\n/// @notice `sellAll` pulls `balanceOf(user)` at execution time, not the amount the user approved.\n/// The website approves the exact balance it displayed. Anyone who sends the user one wei of a\n/// listed token between approval and execution makes the whole sale revert.\ncontract ExactBalancePullGriefTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    PlainToken tokenA;\n    FixedRateRouter router;\n    address user = makeAddr(\"user\");\n    address griefer = makeAddr(\"griefer\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        vm.etch(IMD_ADDR, address(new PlainToken(\"IMD\")).code);\n        router = new FixedRateRouter();\n        PlainToken(IMD_ADDR).mint(address(router), 1_000_000e18);\n        tokenA = new PlainToken(\"A\");\n        exit = new SwarmExit(OWNER);\n\n        address[] memory path = new address[](2);\n        path[0] = address(tokenA);\n        path[1] = IMD_ADDR;\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(tokenA), true);\n        exit.setTokenRoute(address(tokenA), SwarmExit.Venue.UniswapV2, address(router), address(0), abi.encode(path));\n        vm.stopPrank();\n\n        tokenA.mint(user, 10_000e18);\n        tokenA.mint(griefer, 1);\n    }\n\n    function test_oneWeiDonationAfterExactApprovalDoesNotBlockTheSale() public {\n        // The website approves exactly the balance it showed (web/app.js ensureApprovals).\n        vm.prank(user);\n        tokenA.approve(address(exit), 10_000e18);\n\n        // Anyone front-runs the sale with a 1 wei transfer to the user.\n        vm.prank(griefer);\n        tokenA.transfer(user, 1);\n\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(tokenA);\n        uint256[] memory mins = new uint256[](1);\n\n        // Expected: the approved 10_000e18 is sold (or at least the sale does not revert).\n        // Actual: the contract pulls balanceOf(user) = 10_000e18 + 1 and reverts with\n        // ERC20InsufficientAllowance, so the user cannot sell anything.\n        vm.prank(user);\n        (SwarmExit.Outcome[] memory out,,, uint256 net) = exit.sellAll(tokens, mins, block.timestamp);\n        assertTrue(out[0].status == SwarmExit.Status.Sold);\n        assertGt(net, 0);\n        assertEq(IERC20(IMD_ADDR).balanceOf(user), net);\n    }\n}","reproduction":"State: tokenA registered with a working route; user holds 10_000e18 tokenA and approves SwarmExit for exactly 10_000e18 (what the website does). Griefer transfers 1 wei tokenA to user. User calls sellAll([tokenA],[0],block.timestamp). Expected: the sale proceeds (10_000e18 sold, or at least the approved amount). Actual: revert ERC20InsufficientAllowance(SwarmExit, 10000000000000000000000, 10000000000000000000001); nothing is sold. test/scratch/ExactBalancePullGrief.t.sol fails with exactly that error on the current code.","severity":"medium","snippet":"        uint256 balance = erc.balanceOf(user);","title":"sellAll pulls balanceOf(user) at execution time, so any 1-wei transfer to the user (or a reflection/rebase credit) after the website's exact-balance approval reverts the whole sale"},{"citation":"resolved","description":"The documented guarantee (NatSpec line 19-20, README) is that a token whose sale reverts is refunded inside the same transaction and the remaining tokens are still sold. The catch block that implements the refund makes three external calls to the failing token (forceApprove, balanceOf, safeTransfer). A revert in executeSale is already fully rolled back, so the forceApprove(router, 0) calls are redundant, but when the token's approve reverts (approvals closed / caller not whitelisted / paused, which is the very reason the swap failed) the catch block reverts and takes the whole sellAll with it. The same holds for a token whose transfer back to the user reverts (transfer cooldown, blacklist of the user after the pull) and for a token whose transferFrom reverts, because the pull at line 368 is outside the try. In all these cases previewAll still reports the token as sellable (quotes succeed), the website's dry run reverts as a whole with no per-token outcome, and the user cannot sell any of their other tokens through the one-click flow until the owner unregisters the offending token. Fix (keeps the design): drop the forceApprove calls from the catch block (the revert already cleared them), move the pull inside the self-call so a failing transferFrom becomes a SaleFailed outcome, and wrap the refund transfer so a failed refund is reported rather than fatal.","line":382,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {IUniswapV2Router} from \"src/interfaces/IDexVenues.sol\";\n\ncontract PlainToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Honeypot variant: holders can transfer freely, but `approve` reverts for every caller\n/// except the ones the token's owner whitelisted (a common \"trading not open\" pattern).\ncontract ApproveBlockedToken is ERC20 {\n    mapping(address => bool) public allowedApprover;\n\n    constructor() ERC20(\"Trap\", \"TRAP\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n\n    function setAllowedApprover(address who, bool ok) external {\n        allowedApprover[who] = ok;\n    }\n\n    function _approve(address owner_, address spender, uint256 value, bool emitEvent) internal override {\n        require(allowedApprover[owner_], \"TRAP: approvals closed\");\n        super._approve(owner_, spender, value, emitEvent);\n    }\n}\n\n/// @dev Minimal V2-style router: a fixed 1:2 rate, pulls exactly amountIn, pays tokenOut it holds.\ncontract FixedRateRouter is IUniswapV2Router {\n    using SafeERC20 for IERC20;\n\n    function getAmountsOut(uint256 amountIn, address[] calldata path)\n        external\n        pure\n        returns (uint256[] memory amounts)\n    {\n        amounts = new uint256[](path.length);\n        amounts[0] = amountIn;\n        amounts[path.length - 1] = amountIn * 2;\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);\n        IERC20(path[path.length - 1]).safeTransfer(to, amountIn * 2);\n    }\n}\n\n/// @notice The refund path in `_sellOne`'s catch block calls `forceApprove` on the token whose\n/// sale just failed. If that token's `approve` reverts (which is exactly why the sale failed),\n/// the catch block reverts too and the whole `sellAll` reverts instead of refunding that token\n/// and selling the others.\ncontract RefundPathRevertsTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    PlainToken good;\n    ApproveBlockedToken trap;\n    FixedRateRouter router;\n    address user = makeAddr(\"user\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        vm.etch(IMD_ADDR, address(new PlainToken(\"IMD\")).code);\n        router = new FixedRateRouter();\n        PlainToken(IMD_ADDR).mint(address(router), 1_000_000e18);\n        good = new PlainToken(\"GOOD\");\n        trap = new ApproveBlockedToken();\n        trap.setAllowedApprover(user, true); // the user can approve SwarmExit; SwarmExit cannot approve the router\n        exit = new SwarmExit(OWNER);\n\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(good), true);\n        exit.setSwarmToken(address(trap), true);\n        exit.setTokenRoute(\n            address(good), SwarmExit.Venue.UniswapV2, address(router), address(0), _path(address(good), IMD_ADDR)\n        );\n        exit.setTokenRoute(\n            address(trap), SwarmExit.Venue.UniswapV2, address(router), address(0), _path(address(trap), IMD_ADDR)\n        );\n        vm.stopPrank();\n\n        good.mint(user, 1_000e18);\n        trap.mint(user, 1_000e18);\n        vm.startPrank(user);\n        good.approve(address(exit), type(uint256).max);\n        trap.approve(address(exit), type(uint256).max);\n        vm.stopPrank();\n    }\n\n    function _path(address a, address b) internal pure returns (bytes memory) {\n        address[] memory p = new address[](2);\n        p[0] = a;\n        p[1] = b;\n        return abi.encode(p);\n    }\n\n    function test_tokenWhoseApproveRevertsIsRefundedAndOthersStillSell() public {\n        address[] memory tokens = new address[](2);\n        tokens[0] = address(trap);\n        tokens[1] = address(good);\n        uint256[] memory mins = new uint256[](2);\n\n        // Expected (README and NatSpec): trap is refunded with status SaleFailed, good is sold.\n        // Actual: `erc.forceApprove(route.router, 0)` inside the catch block reverts with\n        // \"TRAP: approvals closed\", so the entire sellAll reverts and nothing can be sold.\n        vm.prank(user);\n        (SwarmExit.Outcome[] memory out,,,) = exit.sellAll(tokens, mins, block.timestamp);\n        assertTrue(out[0].status == SwarmExit.Status.SaleFailed);\n        assertEq(trap.balanceOf(user), 1_000e18, \"trap refunded\");\n        assertTrue(out[1].status == SwarmExit.Status.Sold);\n        assertEq(good.balanceOf(user), 0);\n        assertGt(IERC20(IMD_ADDR).balanceOf(user), 0);\n    }\n}","reproduction":"State: two registered tokens with working routes: GOOD (plain ERC20) and TRAP, an ERC20 whose approve() reverts for every caller except the user (user can approve SwarmExit; SwarmExit cannot approve the router). User holds 1_000e18 of each, approves SwarmExit for both, calls sellAll([TRAP, GOOD],[0,0],block.timestamp). Expected: outcomes[0].status == SaleFailed with TRAP refunded in full, outcomes[1].status == Sold and the user receives IMD for GOOD. Actual: executeSale reverts at _swap's forceApprove, the catch block calls erc.forceApprove(route.router, 0) which reverts with 'TRAP: approvals closed', and the whole sellAll reverts. test/scratch/RefundPathReverts.t.sol fails with 'TRAP: approvals closed' on the current code.","severity":"medium","snippet":"            erc.forceApprove(route.router, 0);\n            if (route.tokenOut != address(IMD) && _imdRoute.venue != Venue.None) {\n                IERC20(route.tokenOut).forceApprove(_imdRoute.router, 0);\n            }\n            uint256 leftover = erc.balanceOf(address(this)) - before;\n            if (leftover > 0) erc.safeTransfer(user, leftover);","title":"Refund path is not revert-safe: the catch block calls forceApprove on the token whose sale just failed, and the pull itself is outside the try, so one misbehaving token reverts the whole sale instead "},{"citation":"resolved","description":"preview marks a token unsellable when the quote is below minImdOut, but sellAll/executeSale only compare against the caller-supplied minImdOuts[i]. The website simulates with all-zero minimums and then takes the simulated imdOut as the expected value; a simulated outcome of status Sold with imdOut == 0 (quote succeeded, swap paid nothing, e.g. a pool that rounds the output to zero or a token that burns its whole transfer into the pair) is accepted and the real transaction is sent with minimum 0 for that token, so the user's tokens are consumed for nothing while the brief asks that only tokens with enough liquidity are sold. Fix: in executeSale revert when imdOut < max(minOut, minImdOut) (or at least when imdOut == 0), and in web/app.js treat a simulated outcome with imdOut below minImdOut as unsellable.","line":344,"path":"src/SwarmExit.sol","reproduction":"State: token Z registered with a V2-style route whose getAmountsOut quotes 2e18 for 1e18 but whose swap pulls the input and pays 0; token GOOD with a working route. User holds 1e18 of each, approves both. preview(user,[Z,GOOD])[0].sellable == true. User calls sellAll([Z,GOOD],[0,0],block.timestamp) (what the website sends when the dry run returned imdOut 0 for Z). Expected: Z is refunded (status SaleFailed or BelowMinimum). Actual: outcomes[0].status == Sold, outcomes[0].imdOut == 0, Z.balanceOf(user) == 0. Demonstrated by test_zeroOutputSaleSettlesAsSold in test/scratch/LowLeads.t.sol (passes by asserting the defective behaviour).","severity":"low","snippet":"        if (imdOut < minOut) revert SlippageExceeded(imdOut, minOut);","title":"A sale that returns zero IMD settles as Sold: the on-chain check is only the caller's minOut, the contract's own dust floor minImdOut is never enforced in executeSale"},{"citation":"resolved","description":"_sellOne transfers the full balance in, then tries the sale, then transfers the delta back on failure. For a token that charges a fee on every transfer (the README explicitly supports fee-on-transfer tokens through V2 routes), a failed sale costs the user two fees although nothing was sold, which contradicts the 'refund the failed txn token back to the user' requirement. The website's dry run avoids this only when the failure is reproducible in simulation; a sale that fails on chain because liquidity moved after the simulation still burns the fee twice. Fix: quote before pulling and skip tokens whose quote fails, or perform the pull inside the self-call so a failed sale rolls the pull back instead of refunding it.","line":387,"path":"src/SwarmExit.sol","reproduction":"State: FEE token with a 5% burn on every transfer, registered with a route whose quote succeeds but whose swap reverts ('no liquidity'); GOOD token with a working route so NothingSold is not hit. User holds 10_000e18 FEE, approves, calls sellAll([FEE, GOOD],[0,0],block.timestamp). Expected: FEE refunded in full (10_000e18) or not pulled at all. Actual: outcomes[0].status == SaleFailed and FEE.balanceOf(user) == 9_025e18; 975e18 burned by a sale that sold nothing. Demonstrated by test_failedSaleRefundOfFeeTokenLosesTwoFees in test/scratch/LowLeads.t.sol.","severity":"low","snippet":"            if (leftover > 0) erc.safeTransfer(user, leftover);","title":"A failed sale of a fee-on-transfer token refunds less than was pulled: the token is pulled before the sale is known to be possible, so the refund pays the transfer fee twice"},{"citation":"resolved","description":"isSwarmToken and _tokenRoutes are coupled but only setSwarmToken writes the first and only setTokenRoute writes the second, and setTokenRoute refuses unregistered tokens (line 172). An owner who unregisters a token because its route is wrong or dangerous cannot delete the route; the next setSwarmToken(token,true) immediately makes the stale route live again (sellAll will use it with no further owner action). The same desynchronisation lets rescue() move an unregistered token's balance even though a route for it still exists. Fix: delete _tokenRoutes[token] when unregistering, or allow setTokenRoute(token, Venue.None, ...) for unregistered tokens.","line":151,"path":"src/SwarmExit.sol","reproduction":"Owner: setSwarmToken(A,true); setTokenRoute(A, UniswapV2, routerR, 0, abi.encode([A, IMD])); setSwarmToken(A,false); setTokenRoute(A, None, 0, 0, '') reverts NotSwarmToken(A); tokenRoute(A).venue is still UniswapV2 with router routerR; setSwarmToken(A,true) -> tokenRoute(A) is the old route, sellAll([A]) sells through routerR. Expected: an unregistered token has no live route and re-registration starts from NoRoute. Demonstrated by test_unregisterLeavesRouteAndReregisterResurrectsIt in test/scratch/LowLeads.t.sol.","severity":"low","snippet":"        isSwarmToken[token] = registered;","title":"setSwarmToken(token,false) leaves the token's route in storage, the route cannot be cleared while unregistered, and re-registering silently resurrects it"},{"citation":"resolved","description":"_previewOne calls balanceOf on every registered token before any route check and outside any try/catch; setSwarmToken accepts any non-zero address without checking that it has code. One wrong entry (typo, EOA, a token that self-destructed or that reverts balanceOf for some callers) makes previewAll revert for all users, and the website (refresh() in web/app.js) depends solely on previewAll, so no token is listed and nothing can be sold until the owner unregisters the entry. Fix: check token.code.length > 0 in setSwarmToken and/or wrap the balanceOf read in a try so the entry is reported as unsellable instead of aborting the whole preview.","line":421,"path":"src/SwarmExit.sol","reproduction":"Owner: setSwarmToken(0x0000000000000000000000000000000000001234, true) (an address without code) alongside properly configured tokens the user holds. Any caller: previewAll(user). Expected: an array with that entry marked unsellable and the other tokens quoted. Actual: the call reverts (empty return data decoded as uint256), the website logs an error and lists no tokens. Demonstrated by test_registeredAddressWithoutCodeBricksPreviewAll in test/scratch/LowLeads.t.sol.","severity":"low","snippet":"        p.balance = IERC20(token).balanceOf(user);","title":"previewAll reverts for every user if any registered address has no code or a reverting balanceOf, so one bad registry entry blanks the website's token list"},{"citation":"resolved","description":"executeSale forwards the first leg's measured output straight into the second leg without checking it is non-zero. A Uniswap V3 first leg with a tiny input can complete with amountOut == 0 (exactInput only requires amountOut >= amountOutMinimum, which is 0 here). SwapRouter02.exactInput treats params.amountIn == 0 as Constants.CONTRACT_BALANCE: it sets amountIn to the router's own balance of tokenIn and pays from the router instead of from msg.sender. If the router holds residual WETH (left there by other users' mistakes), that WETH is sold and the IMD is delivered to SwarmExit and credited to the caller as 'proceeds' of a token that fetched nothing (status Sold, imdOut > 0, tax taken on it); if the router holds none, the pool reverts and the token is refunded. Neither matches the intended accounting. Fix: in executeSale revert (so the token is refunded) when bridgeOut == 0, and in _swap require amountIn > 0.","line":341,"path":"src/SwarmExit.sol","reproduction":"State: imdRoute is the documented V3 route WETH|10000|IMD through SwapRouter02 (0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45); token T has a V3 route T|fee|WETH; the router holds some stray WETH. User holds 1 wei of T (or any amount whose V3 output rounds to 0 WETH) and calls sellAll([T],[0],deadline). Trace: _swap(first, 1) returns received = 0; _swap(second, 0) calls exactInput with amountIn 0; SwapRouter02 sets hasAlreadyPaid = true and amountIn = WETH.balanceOf(router) and swaps the router's WETH to IMD for SwarmExit. Expected: the sale of T is refunded as SaleFailed (nothing was obtained for it). Actual: outcomes[0].status == Sold with imdOut equal to the IMD bought with the router's WETH. Not reproducible against the repository mocks (the mock router has no CONTRACT_BALANCE sentinel); traced from the SwapRouter02 source (Constants.CONTRACT_BALANCE = 0).","severity":"low","snippet":"            _swap(second, bridgeOut, deadline);","title":"Second leg can be called with amountIn == 0, which Uniswap SwapRouter02 interprets as 'swap the router's own entire balance' (CONTRACT_BALANCE sentinel)"},{"citation":"resolved","description":"Checked on mainnet on 2026-10-14 (publicnode RPC): symbol IMD, decimals 18, runtime code about 23 KB, owner() = 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7, a zero-value transfer from an arbitrary address succeeds, and none of the usual tax/limit getters (buyTax, sellTax, maxWallet, isExcludedFromFees, limitsInEffect, ...) exist. The decimal assumptions in the contract (DEFAULT_MIN_IMD_OUT = 1e15) and website (quoteImdInBridge(1e18), fmt with 18 decimals) therefore hold. What could not be verified is whether IMD applies a fee, a max-wallet or a blacklist on transfers from a non-pair contract: if it does, IMD.safeTransfer(TREASURY, tax) / IMD.safeTransfer(user, netImd) at lines 320-321 either deliver less than the emitted SoldAll amounts or revert the whole sale. Not a code defect on its own; an open item for the launch: run the happy path on a mainnet fork against the real IMD before release.","line":32,"path":"src/SwarmExit.sol","reproduction":"On a mainnet fork: configure the V3 WETH|10000|IMD route, sell any swarm token, and assert IMD.balanceOf(user) == netImd and IMD.balanceOf(TREASURY) increased by tax. The repository suite only asserts this against an etched plain ERC20.","severity":"info","snippet":"    IERC20 public constant IMD = IERC20(0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7);","title":"The real IMD token is a 23 KB custom contract with an owner; the suite etches a plain ERC20 at its address, so the settlement transfers are untested against the token's real transfer rules"}],"hash":"8b409f0989c66f856e38b4a72fa2063f24cdbd3e4cea051dea779b624572dc24","nodeId":"57f16a89-552b-469a-becf-c8073f3cb203","outcome":"completed","summary":"All eight snippets match the tree, both proofs fail on the current code for the stated reasons, and the findings file is complete with 18 coverage rows covering all 16 entry points plus two invariants.\n\n**Outcome:** the review is written to `.imd-findings.json` with 8 findings (2 medium with Foundry proofs, 5 low, 1 info) and a full coverage record. No repository files were changed outside `test/scratch/`.\n\n**Medium findings, both with proof tests under `test/scratch/` that fail today:**\n- **Exact-balance pull is griefable.** The contract pulls the user's live balance, while the website approves the exact displayed balance. Anyone who sends the user 1 wei of a listed token before the sale makes the whole call revert with an insufficient-allowance error. Reflection tokens trigger it with no attacker.\n- **Refund path can itself revert.** The catch block calls approve on the token whose sale just failed. If that token's approve reverts, which is often why the swap failed, the whole sale reverts instead of refunding that token and selling the rest. The pull is also outside the try, so a token blocking transferFrom has the same effect, and the website's dry run then gives no per-token outcome.\n\n**Low findings, each reproduced with a scratch test or a traced input:**\n- A sale returning zero IMD settles as Sold because the dust floor is only checked in preview.\n- A failed sale of a fee-on-transfer token refunds about 10% less than pulled.\n- Unregistering a token leaves its route in storage, uncleanable, and re-registering resurrects it.\n- One registered address without code makes previewAll revert, blanking the website for everyone.\n- A zero bridge amount on the second leg hits SwapRouter02's contract-balance sentinel, selling the router's own stray WETH as the user's proceeds.\n\n**Info:** the real IMD token on mainnet has 18 decimals and symbol IMD, so the decimal assumptions hold, but it is a 23 KB owned contract whose transfer rules could not be verified and the suite only tests against a plain ERC20 stand-in.\n\n**Coverage:** all 16 entry points have rows. Access control, two-step ownership, pause, route parsing, path reversal, tax conservation and allowance cleanup were traced and hold. Nothing in my area was left unreached.","treeHash":null,"usage":{"cachedInputTokens":1103788,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":48149,"runtime":"claude","turns":28,"wallClockMs":661130}},{"artifacts":[],"attempt":1,"bundleHash":"f19e27025680a4ec0caf32d3cf4bc17c6c0d440fd6de4fca3eb91cf449b39a1f","device":"ddcecd7483f623f7","findings":[],"hash":"9c0bdd2e9a491964c4c26d6744ed6b6657cc30b8876b1fd704af200b08096140","nodeId":"5d03ad33-6a9b-48ab-a748-d383a9583860","outcome":"completed","summary":"All 115 tests pass with the verifier's fuzz seed, and the only changes are under `test/`.\n\n**What was wrong.** The builder's revision changed three behaviours that my earlier edge suite had pinned: every token is now quoted before it is pulled, so predictable failures are skipped instead of refunded; the pull is capped by the allowance and never reverts the batch; and a default dust floor of 1e15 IMD is enforced on chain. Nine of my tests asserted the old semantics and failed against the accepted code.\n\n**What I changed in test/SwarmExitEdges.t.sol.**\n- Bridge-route-missing, stale-minimum and over-reporting-balance cases now assert the token is skipped or marked failed with nothing pulled and the rest of the batch still sold.\n- Partial approval now asserts exactly the allowance is sold and the unapproved wei stays in the wallet. A second test covers an allowance above the balance.\n- The one-wei and tax-rounding fuzz tests disable the floor through the owner first, so the rounding arithmetic is still exercised down to one wei of proceeds.\n- The honeypot fuzz bound starts above the floor. The fee-on-transfer refund test now forces an execution-time failure with a minimum the pre-fee quote clears, which is the only way a fee is still paid.\n- Added a false-returning token mock and seven new tests: a pull returning false is a failed sale, a refund returning false is held and later claimable, holds accumulate across two sales, strangers and the owner cannot claim, rescue is bounded to the stray part even after unregistering, and ETH rescue to a rejecting recipient reverts.\n\n**What I changed in test/SwarmExitInvariant.t.sol.** The contract now holds value as refunds a token refused, so the handler gained a seventh token whose transfers can return false, plus actions to refuse the pull, the venue transfer or the refund, and to claim. Every successful sale now checks that an unsold token is either back in the wallet or held, never both lost. New invariants tie each user's held entry to a ghost, the per-token total to the sum over users and to the exit's balance, and prove the owner can never rescue a held refund. I grounded the harness with a scratch test that reached the hold, the refused claim, the successful claim and the refused pull before deleting it.\n\n**Findings.** I read the revised implementation adversarially and found no defect with a concrete failing input, so no findings file was written. One behaviour worth knowing is by design: a sale where the only token ends up held reverts with NothingSold, which rolls the hold back and leaves the user's tokens untouched.","treeHash":"494d217c634487ab1d691d3434504c138e5dad58","usage":{"cachedInputTokens":2742312,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":46194,"runtime":"claude","turns":56,"wallClockMs":3163422}},{"artifacts":[],"attempt":1,"bundleHash":"ed19cb0387bbf191e6088f59520cb0719c5724e9e75a3f4496287c5e585f39ca","device":"ef5038c1bdac3372","findings":[],"hash":"9da411f133da0de82c8947b4db2d7be5d6e8c52a8ebf65f7dbc1a73674b295d7","nodeId":"32c833d4-7acc-4db8-9e43-826b544036d4","outcome":"completed","summary":"Done. The work is committed on `main` in two commits with a clean tree.\n\n**What was built**\n\n- `src/SwarmExit.sol`: the single launched contract, constructor takes only the owner (`$owner`). IMD token, treasury and the 2% tax are constants from the brief. The owner registers swarm tokens and a sell route per token (Uniswap V2 path or V3 packed path) plus one global WETH to IMD route. `sellAll` pulls each token's full balance, sells it in an atomic self-call, refunds any token whose sale reverts (honeypot, no liquidity, slippage, missing route) inside the same transaction, pools the IMD, sends 2% to the treasury and the rest to the caller. `previewAll` reports sellability and expected IMD per token for the site. Chainlink ETH/USD with staleness checks and a reversed-route IMD quote drive USD display.\n- `web/`: static site with vendored ethers. Connect wallet, list held swarm tokens with balance, expected IMD and USD, totals, highlight unsellable ones, approve exact balances, dry-run the sale, exclude tokens that would fail, then send one transaction.\n- `test/`: 34 Foundry tests with mock V2 and V3 venues, honeypot, fee-on-transfer, no-liquidity and reentrant tokens, plus two fuzz tests. All pass with the verifier's fuzz seed. `forge build` and `forge fmt --check` pass on solc 0.8.26 with `bytecode_hash = \"none\"`.\n- `README.md`: fixed values, chosen constants, the \"After launch\" setter table, trust assumptions and limits.\n\n**Verified facts from mainnet**\nIMD has 18 decimals and trades in a Uniswap V3 1% pool with WETH, not V2, which is why the contract supports both venues. The Uniswap V2 Router02, SwapRouter02, QuoterV2, WETH and the Chainlink ETH/USD feed addresses listed in the README were checked with code and read calls on 2026-10-13.\n\n**End-to-end rehearsal**\nA scratch anvil run deployed the mocks and contract, then drove the site's exact ABI through ethers: pricing, preview, approvals, dry run catching the honeypot, the real sale with correct 2% / 98% split, and a honeypot-only sale reverting with balances intact. It surfaced one real issue, now fixed in the site: sequential sales share the WETH/IMD pool, so isolated quotes overstate later tokens. The site now takes the zero-minimum simulation as the quote and sets minimums under it.\n\n**Open items for the owner after launch**\nSet the IMD route, register each swarm token with its route, set the ETH/USD feed, and paste the deployed address into `web/config.js`. Uniswap V4 pools are not supported in this version. Slither and Mythril were not available here, and tests are not an audit.","treeHash":"3352ff6e38e597ec4e380b93dc2b3607140e0885","usage":{"cachedInputTokens":3532544,"inputTokens":1122,"model":"claude-fable-5-1","outputTokens":81641,"runtime":"claude","turns":59,"wallClockMs":1264082}},{"artifacts":[],"attempt":1,"bundleHash":"f0fc9c1d2e0589f89947acda6b966a9e4e564135e2d0874fa85b75ed664e7e16","device":"8a05ec1ea94411ea","findings":[{"description":"The contract's own sellability rule (preview: expectedImd >= minImdOut, default 1e15) is not enforced on chain. executeSale only checks imdOut < minOut, where minOut is whatever the caller passed. With a zero minimum, a token whose swap produces 0 IMD (a pool skewed so the output rounds to zero, an owner-set route into a router that returns nothing, or a sandwich that drains the pool in the same block) is marked Status.Sold with imdOut == 0: the user's full balance is pulled and swapped away for nothing, and the refund path never runs. NothingSold only protects the case where every token in the batch yields zero. The brief requires selling only tokens that are sellable with enough liquidity, and refunding a sale that fails; the website's dry run hides this for users of the frontend, but any direct caller, integrator or a frontend that passes zero minimums loses the tokens. Suggested fix: in executeSale revert when imdOut == 0 or imdOut < minImdOut (the storage threshold), so the sale is refunded like any other failed one; keep the caller's per-token minimum on top.","line":344,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\n\ncontract PToken is ERC20 {\n    constructor() ERC20(\"T\", \"T\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Minimal constant-product V2 router: one pair, pulls tokenIn, pays tokenOut from its own balance.\ncontract PRouter {\n    using SafeERC20 for IERC20;\n\n    mapping(address => uint256) public reserve;\n\n    function add(address token, uint256 amount) external {\n        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);\n        reserve[token] += amount;\n    }\n\n    function _out(address tIn, address tOut, uint256 amountIn) internal view returns (uint256) {\n        uint256 inWithFee = amountIn * 997;\n        return (inWithFee * reserve[tOut]) / (reserve[tIn] * 1000 + inWithFee);\n    }\n\n    function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory a) {\n        a = new uint256[](2);\n        a[0] = amountIn;\n        a[1] = _out(path[0], path[1], amountIn);\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);\n        uint256 out = _out(path[0], path[1], amountIn);\n        reserve[path[0]] += amountIn;\n        reserve[path[1]] -= out;\n        IERC20(path[1]).safeTransfer(to, out);\n    }\n}\n\n/// @notice `sellAll` settles a token whose sale yields zero IMD as `Sold` and keeps the tokens.\n/// Expected: a sale that produces nothing (below the contract's own `minImdOut` dust threshold, and\n/// in particular exactly zero) is refunded like any other failed sale, so the user never gives up\n/// tokens for nothing. Actual: status `Sold`, `imdOut == 0`, user balance 0, tokens sit in the pool.\ncontract ZeroProceedsProofTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    PRouter router;\n    PToken dust; // pool so skewed that any sale rounds to 0 IMD\n    PToken good; // normal pool\n    address user = makeAddr(\"user\");\n\n    function setUp() public {\n        vm.etch(IMD_ADDR, address(new PToken()).code);\n        router = new PRouter();\n        dust = new PToken();\n        good = new PToken();\n        exit = new SwarmExit(OWNER);\n\n        // dust/IMD pool: 1e30 dust vs 1 wei IMD -> output always rounds to 0\n        dust.mint(address(this), 1e30);\n        PToken(IMD_ADDR).mint(address(this), 1);\n        dust.approve(address(router), 1e30);\n        IERC20(IMD_ADDR).approve(address(router), 1);\n        router.add(address(dust), 1e30);\n        router.add(IMD_ADDR, 1);\n\n        // good token: separate router instance for an honest pool\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(dust), true);\n        exit.setTokenRoute(address(dust), SwarmExit.Venue.UniswapV2, address(router), address(0), _path(address(dust), IMD_ADDR));\n        exit.setSwarmToken(address(good), true);\n        vm.stopPrank();\n\n        PRouter goodRouter = new PRouter();\n        good.mint(address(this), 1_000e18);\n        PToken(IMD_ADDR).mint(address(this), 1_000e18);\n        good.approve(address(goodRouter), 1_000e18);\n        IERC20(IMD_ADDR).approve(address(goodRouter), 1_000e18);\n        goodRouter.add(address(good), 1_000e18);\n        goodRouter.add(IMD_ADDR, 1_000e18);\n        vm.prank(OWNER);\n        exit.setTokenRoute(\n            address(good), SwarmExit.Venue.UniswapV2, address(goodRouter), address(0), _path(address(good), IMD_ADDR)\n        );\n\n        dust.mint(user, 1_000e18);\n        good.mint(user, 10e18);\n        vm.startPrank(user);\n        dust.approve(address(exit), type(uint256).max);\n        good.approve(address(exit), type(uint256).max);\n        vm.stopPrank();\n    }\n\n    function _path(address a, address b) internal pure returns (bytes memory) {\n        address[] memory p = new address[](2);\n        p[0] = a;\n        p[1] = b;\n        return abi.encode(p);\n    }\n\n    function test_zeroProceedsSaleIsRefundedNotSettled() public {\n        // The contract itself says this token is not sellable.\n        address[] memory tokens = new address[](2);\n        tokens[0] = address(dust);\n        tokens[1] = address(good);\n        SwarmExit.Preview[] memory p = exit.preview(user, tokens);\n        assertFalse(p[0].sellable, \"preview: dust token is not sellable\");\n        assertTrue(p[0].status == SwarmExit.Status.BelowMinimum);\n        assertEq(p[0].expectedImd, 0);\n\n        uint256[] memory mins = new uint256[](2); // zero minimums, as a direct caller may pass\n        vm.prank(user);\n        (SwarmExit.Outcome[] memory out,,,) = exit.sellAll(tokens, mins, block.timestamp);\n\n        // Expected: zero-proceeds sale refunded, user keeps the dust tokens.\n        assertTrue(out[0].status != SwarmExit.Status.Sold, \"a sale yielding 0 IMD must not be settled as Sold\");\n        assertEq(dust.balanceOf(user), 1_000e18, \"user must keep tokens that fetched nothing\");\n    }\n}","reproduction":"Register token D with a V2 route D -> IMD whose pool holds 1e30 D and 1 wei IMD (so getAmountOut returns 0), plus a healthy token E. preview(user,[D,E]) reports D as BelowMinimum / not sellable with expectedImd 0. User holds 1000e18 D, approves, calls sellAll([D,E],[0,0],now). Expected: D refunded (status != Sold), user still holds 1000e18 D. Actual: outcomes[0].status == Sold, outcomes[0].imdOut == 0, user's D balance is 0 and the tokens sit in the pool. See test/scratch/ZeroProceeds.t.sol: test_zeroProceedsSaleIsRefundedNotSettled fails on the current code.","severity":"medium","title":"sellAll settles a sale that yields zero (or dust) IMD as Sold and keeps the user's tokens"},{"description":"The catch block of _sellOne calls erc.forceApprove(route.router, 0), optionally IERC20(route.tokenOut).forceApprove(_imdRoute.router, 0), and erc.safeTransfer(user, leftover). All three are external calls into the token being refunded (or the bridge token). A token that reverts on approve for a blocked spender (some honeypots block approving the router), or that blacklists the user after the pull, makes the catch block itself revert, so the whole sellAll reverts instead of refunding that token and selling the rest. The approve resets are also redundant: executeSale's state (including its allowances) was already rolled back by the revert. No funds are lost (nothing moves), but item 6 of the brief, refund the failed token and continue, is not met for these tokens and the frontend's dry run is the only thing keeping such a token out of the batch. Suggested fix: drop the approve resets from the catch block (they are no-ops after the revert) and consider wrapping the refund transfer so one bad token cannot block the batch, or document that such tokens must be excluded by the dry run.","line":382,"path":"src/SwarmExit.sol","reproduction":"ApproveBlockingToken (reverts on approve when spender == v2 router) registered with a V2 route to IMD and liquidity; user holds 100e18 of it and 1000e18 of a good token C; sellAll([ABL, C],[0,0],now). Expected per the brief: ABL refunded with status SaleFailed, C sold. Actual: the call reverts with 'ABL: approve blocked' from the catch block, nothing is sold. Same with RefundBlockingToken that blocks transfers to the user and a broken pool: revert 'RBL: recipient blocked'. test/SwarmExitEdges.t.sol test_approveBlockingTokenNeverCostsTheUser and test_refundBlockedTokenNeverCostsTheUser only assert that no tokens are lost either way; they do not assert the revert as correct.","severity":"low","title":"Refund path makes external calls that can revert, turning a per-token refund into a whole-batch revert"},{"description":"setSwarmToken(token, false) does not delete _tokenRoutes[token]. If the owner later registers the token again (for example after it was removed because its pool was drained or replaced), the stale route is immediately live again, pointing at the old router/pool, without a TokenRouteSet event. tokenRoute(token) also keeps returning a route for an unregistered token. Selling is still gated by isSwarmToken, so no funds are at risk; the risk is an unexpected route becoming active on re-registration. Suggested fix: delete the route when unregistering, or emit TokenRouteSet on re-registration.","line":148,"path":"src/SwarmExit.sol","reproduction":"setTokenRoute(A, V2, routerOld, ...); setSwarmToken(A, false); tokenRoute(A).venue is still UniswapV2. setSwarmToken(A, true): A is sellable through routerOld with no route event emitted. Expected: no route until the owner sets one.","severity":"info","title":"Unregistering a swarm token leaves its route in storage; re-registering silently restores the old route"},{"description":"_previewOne decides sellability from quotes alone. A token that quotes fine but reverts on transfer into the pool (HoneypotToken) or on approving the router is reported sellable == true. The README acknowledges this and relies on the website's eth_call dry run of sellAll to exclude such tokens; integrators who use preview/previewAll directly (as item 5 of the brief describes) will list honeypots as sellable and must dry-run sellAll themselves. test_previewStatuses in the existing suite asserts this behaviour ('honeypot looks sellable from quotes alone'); it is recorded here so the limitation is explicit rather than blessed.","line":415,"path":"src/SwarmExit.sol","reproduction":"preview(user,[honey]) with honey.setBlocked(pool) returns sellable == true, status Sold. sellAll([honey],[0],now) reverts NothingSold. Expected from the brief: the unsellable token is flagged and no transaction is initiated.","severity":"info","title":"preview reports honeypots and approve-blocking tokens as sellable"}],"hash":"a251d6872dca81e4d205ba0771cb73943361cdb843243178340f536b323a9961","nodeId":"5d03ad33-6a9b-48ab-a748-d383a9583860","outcome":"completed","summary":"Done. `forge build` and `forge test` pass with the verifier's fuzz seed: 81 tests across three suites, all green. One defect is reported rather than tested around.\n\n**What I added, all under `test/`:**\n- `test/SwarmExitFixture.sol` is a shared deployment base mirroring the existing fixture so the new suites are comparable with the old one.\n- `test/mocks/EdgeMocks.sol` adds hostile tokens: approve-blocking, refund-blocking, balance over-reporting, all-burning, gas-burning, and a reverting Chainlink feed.\n- `test/SwarmExitEdges.t.sol` has 46 adversarial tests. They cover ownership handover misuse, registry removal at first, middle, last and missing positions, V3 path length boundaries including a fuzzed length rule, multi-hop V2 and V3 routes ending in IMD, routes ending in another swarm token, IMD and WETH registered as swarm tokens, one-wei sales with zero tax, exactly 50 tokens in one call, minimum equal to output, deadline at the boundary, repeated sales, shared-liquidity ordering, partial approvals, stray balances in the contract, rescue limits, feed edge cases, pause behavior, and two oracle fuzz tests that check the reversed-route quoting against a hand-reversed path.\n- `test/SwarmExitInvariant.t.sol` drives the contract with three actors through random subsets, random minimums, broken pools, pauses, route clears, unregister cycles, stray deposits and liquidity changes. Invariants: the exit holds only stray deposits, treasury equals the sum of taxes, users hold exactly their net proceeds, gross equals tax plus net, no router allowance survives, registry list and mapping agree, and the brief's constants never change. A scratch check confirmed the handler actually settles, refunds and reverts, so the invariants are not vacuous.\n\n**Findings in `.imd-findings.json`:**\n- **Medium, with proof.** `sellAll` settles a sale that yields zero IMD as `Sold` and keeps the user's tokens. The on-chain path never enforces the contract's own `minImdOut` threshold, so with a zero caller minimum a skewed or drained pool consumes the balance for nothing. The proof under `test/scratch/ZeroProceeds.t.sol` fails on the current code.\n- **Low.** The refund catch block makes external calls into the failing token, so a token that reverts on approve or blacklists the user turns a per-token refund into a whole-batch revert. My tests for these tokens assert only that the user loses nothing either way.\n- **Info.** Unregistering a token leaves its route in storage, so re-registering silently revives the old route.\n- **Info.** `preview` reports honeypots as sellable, which the existing suite asserts as intended. Recorded so the limitation is explicit.\n\nNo configuration files were touched and nothing outside `test/` and the findings file was created.","treeHash":"125f76e8a954cee08382e82d243437b2769575bd","usage":{"cachedInputTokens":1437178,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":58135,"runtime":"claude","turns":28,"wallClockMs":885421}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9506ecec995bdbf9","findings":[{"citation":"resolved","description":"Economic Security guide, 'cheapest griefing vector that blocks other users'; Flow Gap seam execution x periphery x first principles. _sellOne reads the seller's live balance (line 362) and pulls exactly that amount (line 368: erc.safeTransferFrom(user, address(this), balance)). The website approves the exact balance it read at refresh time (web/app.js:158 `erc.approve(exit.target, r.balance)`), which the README advertises as a safety feature. Any third party who transfers 1 wei of any registered swarm token to the seller between approval and execution makes balance exceed allowance, so the pull reverts with ERC20InsufficientAllowance and, because the pull is outside the try/catch, the entire sellAll reverts: no token is sold, the user pays gas for a failed transaction, and must re-approve. An attacker watching the mempool can repeat this for every attempt at the cost of 1 wei plus gas; it also happens naturally whenever a wallet receives any swarm token after approving. The brief's one-click sale is therefore blockable by anyone for any targeted user, and the SaleFailed/refund design (requirement 6) never engages. Minimal fix preserving the design: pull min(balanceOf(user), allowance(user, this)) (or accept an explicit amounts[] argument) so an unexpected inbound transfer cannot block the sale. The existing suite only tests a missing approval (test_missingApprovalRevertsWholeCall); it never changes the balance between approval and sale.","line":362,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\n\ncontract PlainToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice Minimal constant-product V2 router: pulls tokenIn, pays tokenOut from its own reserves.\ncontract TinyV2Router {\n    function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts) {\n        amounts = new uint256[](path.length);\n        amounts[0] = amountIn;\n        amounts[1] = _out(path[0], path[1], amountIn);\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        uint256 out = _out(path[0], path[1], amountIn);\n        IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);\n        IERC20(path[1]).transfer(to, out);\n    }\n\n    function _out(address tokenIn, address tokenOut, uint256 amountIn) internal view returns (uint256) {\n        uint256 rIn = IERC20(tokenIn).balanceOf(address(this));\n        uint256 rOut = IERC20(tokenOut).balanceOf(address(this));\n        return (amountIn * 997 * rOut) / (rIn * 1000 + amountIn * 997);\n    }\n}\n\n/// @dev Fails on the current code: a third party sending 1 wei of a swarm token to the seller after the\n/// seller approved their exact balance makes the whole `sellAll` revert (ERC20InsufficientAllowance),\n/// because `_sellOne` pulls `balanceOf(user)` instead of the approved amount. Passes once `sellAll`\n/// pulls `min(balance, allowance)` (or an explicit amount) and the approved tokens are sold.\ncontract GriefProofTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    PlainToken imd;\n    PlainToken tokenA;\n    TinyV2Router router;\n    address user = makeAddr(\"user\");\n    address attacker = makeAddr(\"attacker\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        vm.etch(IMD_ADDR, address(new PlainToken(\"IMD\")).code);\n        imd = PlainToken(IMD_ADDR);\n        tokenA = new PlainToken(\"A\");\n        router = new TinyV2Router();\n        tokenA.mint(address(router), 1_000_000e18);\n        imd.mint(address(router), 1_000_000e18);\n\n        exit = new SwarmExit(OWNER);\n        address[] memory path = new address[](2);\n        path[0] = address(tokenA);\n        path[1] = IMD_ADDR;\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(tokenA), true);\n        exit.setTokenRoute(address(tokenA), SwarmExit.Venue.UniswapV2, address(router), address(0), abi.encode(path));\n        vm.stopPrank();\n\n        tokenA.mint(user, 10_000e18);\n    }\n\n    function test_oneWeiFromThirdPartyMustNotBlockTheSale() public {\n        uint256 approved = tokenA.balanceOf(user);\n        vm.prank(user);\n        tokenA.approve(address(exit), approved); // exact-balance approval, as web/app.js does\n\n        // Attacker front-runs the sale with 1 wei.\n        tokenA.mint(attacker, 1);\n        vm.prank(attacker);\n        tokenA.transfer(user, 1);\n\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(tokenA);\n        uint256[] memory mins = new uint256[](1);\n\n        vm.prank(user);\n        (SwarmExit.Outcome[] memory out,,, uint256 net) = exit.sellAll(tokens, mins, block.timestamp);\n\n        assertTrue(out[0].status == SwarmExit.Status.Sold, \"approved balance should still sell\");\n        assertGt(net, 0, \"user should receive IMD\");\n        assertEq(imd.balanceOf(user), net);\n    }\n}","reproduction":"State: tokenA registered with a working route; user holds 10_000e18 tokenA and approves exactly 10_000e18 to SwarmExit (as web/app.js does). Attacker (any address) transfers 1 wei tokenA to user. User calls sellAll([tokenA],[0],block.timestamp). Expected: the approved 10_000e18 is sold (or at worst tokenA is refunded/skipped and other tokens still sell). Actual: revert ERC20InsufficientAllowance(SwarmExit, 10000000000000000000000, 10000000000000000000001); nothing sold, gas lost; repeatable every block. See test/scratch/GriefProof.t.sol (fails on current code).","severity":"medium","snippet":"        uint256 balance = erc.balanceOf(user);","title":"sellAll pulls balanceOf(user) instead of an approved amount: 1 wei sent by anyone reverts the whole sale (griefing / DoS of the core flow)"},{"citation":"resolved","description":"Invariant guide, 'Diverge view from write' / Flow Gap seam execution x periphery x first principles. The only on-chain acceptance check for a sale is the caller's per-token minOut (line 344). With minOut = 0 (the value the website uses in its dry run, and the value the website signs for any token whose simulated output is 0, since min = 0 * 9700 / 10000), a swap that legitimately returns amountOut == 0 passes: Uniswap V3 exactInput with amountOutMinimum = 0 succeeds for dust inputs whose output rounds to zero, and SwapRouter02 does not revert on a zero output. executeSale then returns imdOut = 0, _sellOne records Status.Sold, emits TokenSold(..., 0) and the pulled tokens are gone (sent into the pool for nothing) with no refund. Meanwhile _previewOne reports the same token as BelowMinimum / sellable = false because the quote is under minImdOut (line 449). The brief requires selling only sellable tokens and refunding failed sales; the README states a token below the dust threshold is not sellable, but sellAll never enforces minImdOut. Impact is bounded to dust-valued balances (and to callers passing minOut = 0), so low. Fix: in executeSale revert when imdOut == 0 (or imdOut < minImdOut) so the catch path refunds the token, keeping the view and the write consistent.","line":344,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {IUniswapV3SwapRouter} from \"src/interfaces/IDexVenues.sol\";\n\ncontract PlainToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice Constant-product stand-in for a V3 pool + SwapRouter02: a tiny input rounds to 0 output and,\n/// as on Uniswap V3 with amountOutMinimum = 0, the swap succeeds with amountOut == 0.\ncontract TinyV3Router {\n    function exactInput(IUniswapV3SwapRouter.ExactInputParams calldata p) external payable returns (uint256 out) {\n        address tokenIn = address(bytes20(p.path[0:20]));\n        address tokenOut = address(bytes20(p.path[p.path.length - 20:]));\n        out = _out(tokenIn, tokenOut, p.amountIn);\n        IERC20(tokenIn).transferFrom(msg.sender, address(this), p.amountIn);\n        require(out >= p.amountOutMinimum, \"Too little received\");\n        IERC20(tokenOut).transfer(p.recipient, out);\n    }\n\n    function quoteExactInput(bytes memory path, uint256 amountIn)\n        external\n        view\n        returns (uint256 amountOut, uint160[] memory a, uint32[] memory b, uint256 c)\n    {\n        address tokenIn;\n        address tokenOut;\n        assembly {\n            tokenIn := shr(96, mload(add(path, 32)))\n            tokenOut := shr(96, mload(add(add(path, 32), sub(mload(path), 20))))\n        }\n        amountOut = _out(tokenIn, tokenOut, amountIn);\n        return (amountOut, a, b, c);\n    }\n\n    function _out(address tokenIn, address tokenOut, uint256 amountIn) internal view returns (uint256) {\n        uint256 rIn = IERC20(tokenIn).balanceOf(address(this));\n        uint256 rOut = IERC20(tokenOut).balanceOf(address(this));\n        return (amountIn * 990 * rOut) / (rIn * 1000 + amountIn * 990);\n    }\n}\n\n/// @dev Fails on the current code: a sale whose swap returns 0 IMD is recorded as `Sold` and the pulled\n/// tokens are not refunded, although `preview` reports the same token as not sellable (BelowMinimum).\n/// Passes once `executeSale` / `_sellOne` treats `imdOut == 0` (or `imdOut < minImdOut`) as a failed\n/// sale and refunds the token.\ncontract ZeroOutProofTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    PlainToken imd;\n    PlainToken tokenB; // direct V3 route B -> IMD, 1 B worth far less than 1 wei of IMD\n    PlainToken tokenC; // direct V3 route C -> IMD, healthy\n    TinyV3Router v3;\n    address user = makeAddr(\"user\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        vm.etch(IMD_ADDR, address(new PlainToken(\"IMD\")).code);\n        imd = PlainToken(IMD_ADDR);\n        tokenB = new PlainToken(\"B\");\n        tokenC = new PlainToken(\"C\");\n        v3 = new TinyV3Router();\n        // One pool contract holds both pairs' reserves (constant-product over balances).\n        tokenB.mint(address(v3), 1_000_000e18);\n        tokenC.mint(address(v3), 50_000e18);\n        imd.mint(address(v3), 100_000e18);\n\n        exit = new SwarmExit(OWNER);\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(tokenB), true);\n        exit.setSwarmToken(address(tokenC), true);\n        exit.setTokenRoute(\n            address(tokenB), SwarmExit.Venue.UniswapV3, address(v3), address(v3), abi.encodePacked(address(tokenB), uint24(10_000), IMD_ADDR)\n        );\n        exit.setTokenRoute(\n            address(tokenC), SwarmExit.Venue.UniswapV3, address(v3), address(v3), abi.encodePacked(address(tokenC), uint24(10_000), IMD_ADDR)\n        );\n        vm.stopPrank();\n\n        tokenB.mint(user, 5); // dust: quotes to 0 IMD\n        tokenC.mint(user, 1_000e18);\n    }\n\n    function test_zeroOutputSaleIsRefundedNotSettled() public {\n        address[] memory tokens = new address[](2);\n        tokens[0] = address(tokenB);\n        tokens[1] = address(tokenC);\n        uint256[] memory mins = new uint256[](2);\n\n        vm.startPrank(user);\n        tokenB.approve(address(exit), type(uint256).max);\n        tokenC.approve(address(exit), type(uint256).max);\n\n        SwarmExit.Preview[] memory p = exit.preview(user, tokens);\n        assertFalse(p[0].sellable, \"preview says B is not sellable\");\n        assertTrue(p[0].status == SwarmExit.Status.BelowMinimum);\n\n        (SwarmExit.Outcome[] memory out,,,) = exit.sellAll(tokens, mins, block.timestamp);\n        vm.stopPrank();\n\n        assertTrue(out[1].status == SwarmExit.Status.Sold, \"C sells\");\n        // The brief: only sellable tokens are sold; a failed sale is refunded.\n        assertTrue(out[0].status != SwarmExit.Status.Sold, \"a 0-IMD sale must not be recorded as Sold\");\n        assertEq(tokenB.balanceOf(user), 5, \"B must be refunded, not consumed for 0 IMD\");\n    }\n}","reproduction":"State: tokenB registered with a V3 route to IMD through a pool where 5 wei of B quotes to 0 IMD; user holds 5 wei B and 1_000e18 of a healthy token C; both approved. preview(user,[B,C]) returns B: sellable=false, status=BelowMinimum. sellAll([B,C],[0,0],block.timestamp). Expected: B refunded (status SaleFailed) or skipped, C sold. Actual: outcomes[0].status == Sold, imdOut == 0, user's B balance == 0, no TokenRefunded event; C sold. Reproduced with the repository mocks too (test/scratch/Leads.t.sol test_lead2: 1 wei tokenB -> status 0, imdOut 0, user balance 0). See test/scratch/ZeroOutProof.t.sol (fails on current code).","severity":"low","snippet":"        if (imdOut < minOut) revert SlippageExceeded(imdOut, minOut);","title":"A sale that returns 0 IMD settles as Sold: tokens are consumed without refund while preview reports the same token as not sellable (BelowMinimum)"},{"citation":"resolved","description":"Economic Security / Trust documentation. setTokenRoute and setImdRoute accept any non-zero router and quoter address (src/SwarmExit.sol:481 `if (router == address(0)) revert ZeroAddress();` is the only check). A router that quotes well in getAmountsOut / quoteExactInput and pulls the approved tokens in the swap but returns nothing makes executeSale return imdOut = 0 without reverting. preview marks the token sellable (the quote clears minImdOut), the website's dry run reports Sold with imdOut 0, sets the signed minimum to 0 * 9700 / 10000 = 0, and the real transaction hands the whole balance to the router with status Sold. No refund path is reached. This requires the owner (or a compromised owner key) to act, so it is a privileged power rather than a bypass; it is reported because the README states the opposite guarantee to users and reviewers, and because the website's zero-minimum fallback is the unprivileged amplifier that turns a bad quote into a signed zero-minimum sale. Fix options that keep the agreed design: document the power honestly (routes are fully trusted and can divert tokens), and/or have the website refuse to sign any token whose simulated output is 0 and have executeSale reject imdOut == 0 (see finding 2).","line":60,"path":"README.md","reproduction":"State: owner calls setTokenRoute(tokenA, UniswapV2, thiefRouter, 0, abi.encode([tokenA, IMD])) where thiefRouter.getAmountsOut returns amountIn and its swap function only transferFrom()s the tokens to itself. User holds 10_000e18 tokenA, approves, preview says sellable=true. User calls sellAll([tokenA, tokenC],[0,0],deadline). Expected per README: the sale fails and tokenA is refunded. Actual: outcomes[0].status == Sold, imdOut == 0, user tokenA balance 0, thiefRouter holds 10_000e18 tokenA. Reproduced in test/scratch/Leads.t.sol test_lead3_maliciousRouteStealsTokens.","severity":"low","snippet":"- The owner decides which tokens count as swarm-launched and which pools sell them. A wrong route cannot steal funds (a failed sale refunds) but can make a token unsellable. The owner can pause selling.","title":"Trust assumption misdocumented: a wrong (malicious) route can take user tokens, the sale does not fail and nothing is refunded"},{"citation":"resolved","description":"Flow Gap seam periphery x first principles ('break dependencies', Economic Security). _previewOne calls balanceOf on every registered token without try/catch, and previewAll iterates the whole registry. If any registered token's balanceOf reverts (paused or upgradeable token, a token that self-destructed or was never a contract, a token that reverts for a blacklisted or zero holder), previewAll(user) reverts for all users, and the website (web/app.js:105 relies solely on previewAll) shows an error instead of the list, so requirement 1 and 2 (list tokens, show totals) and the sell button are unavailable to everyone until the owner calls setSwarmToken(token,false). The per-token quote legs are already wrapped in try/catch; the balance read is the one periphery call that is not. Fix: wrap the balanceOf read (and ideally the whole _previewOne) in try/catch and report a status such as QuoteFailed for that token.","line":421,"path":"src/SwarmExit.sol","reproduction":"State: owner registers token BRK (setSwarmToken(BRK,true)); later BRK.balanceOf reverts with 'paused'. Any caller runs previewAll(user). Expected: a Preview for every token with BRK marked unsellable. Actual: the whole call reverts with 'paused' and no token is listed. Reproduced in test/scratch/Leads.t.sol test_lead4_revertingBalanceOfBricksPreviewAll.","severity":"low","snippet":"        p.balance = IERC20(token).balanceOf(user);","title":"One registered token whose balanceOf reverts makes previewAll revert for every user (website listing bricked until the owner unregisters it)"},{"citation":"resolved","description":"Flow gap between the contract's loop bound (src/SwarmExit.sol:301 reverts TooManyTokens above 50 tokens) and the frontend, which passes every sellable row in one call (dry run at line 178 and real call at line 198). With 51 or more sellable tokens the staticCall reverts, the website logs an error and nothing can be sold; the user has no way to sell a subset from the UI. No funds at risk. Fix: split `selected` into batches of at most 50 and send one sellAll per batch (or expose selection).","line":198,"path":"web/app.js","reproduction":"State: 51 registered tokens with routes, all held by the user. Click Sell. Expected: sales proceed (in batches). Actual: exit.sellAll.staticCall reverts with TooManyTokens(); the log shows 'Error: ...' and the sale never starts.","severity":"info","snippet":"      const tx = await exit.sellAll(","title":"Website does not chunk at MAX_TOKENS_PER_SALE: a wallet holding more than 50 sellable swarm tokens cannot sell at all"}],"hash":"b057d3d9b185f02a1c148b2d48b11adf6964fca092891777b55f30bc3b781dac","nodeId":"24d1b421-4d03-4661-8d03-d48a9d9a0aae","outcome":"completed","summary":"The findings file is written and every quoted snippet matches the tree. Final state:\n\n**Deliverable:** `.imd-findings.json` with 5 findings and 19 coverage rows (all 16 entry points plus 3 invariants). Baseline suite passes (34 tests); my scratch tests reproduce every finding, and the two proof files fail on the current code for the stated reasons.\n\n**Findings (economics / invariant / flow-gap area):**\n\n1. **Medium. `sellAll` pulls `balanceOf(user)`, not an approved amount** (`src/SwarmExit.sol:362`). The website approves the exact balance. Anyone sending 1 wei of a swarm token to the seller makes the pull revert with `ERC20InsufficientAllowance`, and because the pull sits outside the try/catch the whole sale reverts. Cheap, repeatable griefing of the core one-click flow. Proof: `test/scratch/GriefProof.t.sol`. Fix: pull `min(balance, allowance)` or take explicit amounts.\n\n2. **Low. Zero-output sale settles as `Sold`** (`src/SwarmExit.sol:344`). The only acceptance check is the caller's `minOut`. A dust input whose V3 swap returns 0 IMD consumes the tokens with no refund, while `preview` reports the same token as `BelowMinimum`. `minImdOut` is never enforced on-chain. Proof: `test/scratch/ZeroOutProof.t.sol`.\n\n3. **Low. README trust claim is false** (`README.md:60`). \"A wrong route cannot steal funds\" does not hold: a router that quotes well but keeps the tokens makes the sale succeed with 0 IMD, and the website then signs a zero minimum. Owner-privileged, so reported as a documented power with its unprivileged amplifier, not a bypass.\n\n4. **Low. One reverting `balanceOf` bricks `previewAll`** (`src/SwarmExit.sol:421`). The balance read is the one periphery call not wrapped in try/catch, so the website lists nothing for anyone until the owner unregisters the token.\n\n5. **Info. Website never chunks at 50 tokens** (`web/app.js:198`), so a wallet with more than 50 sellable tokens cannot sell from the UI.\n\n**What held:** 2%/98% tax split and IMD conservation (delta-based, zero residue), no swarm token resting in the contract, refund path atomicity, reentrancy guards, route reversal arithmetic for the USD quote, registry mapping/array consistency. Slither's reentrancy and strict-equality leads were traced and are not exploitable; aderyn's storage-array lead is a false positive on a storage-to-memory copy. Not reached: live-mainnet behaviour of the real IMD token and of SwapRouter02's zero-amount `CONTRACT_BALANCE` semantics beyond reasoning (benign, no victim).","treeHash":null,"usage":{"cachedInputTokens":1281967,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":41000,"runtime":"claude","turns":27,"wallClockMs":593005}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7e929507773df661","findings":[],"hash":"c6cc8892dec1333746e2a80460f8f6d4a8ef5e21b72473c9616efe8e5802d2d0","nodeId":"649d41fa-5838-4f17-80ba-30d3ebe41463","outcome":"completed","summary":"The review is complete. The findings file holds an empty list and a 24-row coverage record.\n\n**My earlier finding is fixed and dropped.** The fee-on-transfer v4 finding (id 59ff49b74242) no longer reproduces. The proof copied to test/scratch passed on the revised tree. The callback in `src/SwarmExit.sol` now pays the input first, reverts on a zero credit, swaps the amount the manager credited, and takes the output. That is the design-preserving fix I proposed, and it matches the real PoolManager's settle semantics, where settle returns the balance delta since sync.\n\n**Round-one specialist proofs also pass.** The four other attached proofs (allowance-capped pull, pull failure as a sale outcome, no approve calls in the catch block, held refunds for refused transfers) all pass on this tree, so none reopen.\n\n**Checks run on this tree:**\n- Full suite with the verifier's fuzz seed: 128 tests pass.\n- Runtime size is 24,519 bytes, 57 bytes under the EIP-170 limit, so the launch can deploy it. This margin is tight. Any further code growth will break deployment.\n- Formatting check passes on tracked source and tests.\n- The manifest has only the schema keys, one contract, and one constructor argument.\n\n**Coverage.** All 20 entry points are marked `holds` with a one-line trace each. Owner powers (pause, dust floor, routes, renounce) are recorded as trust assumptions, which the README now documents honestly.\n\nNo new defects were found. Scratch copies of the proofs were removed, so the working tree is unchanged apart from the findings file.","treeHash":null,"usage":{"cachedInputTokens":868696,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":14124,"runtime":"claude","turns":19,"wallClockMs":250977}},{"artifacts":[],"attempt":1,"bundleHash":"18b1a698f53fddbe2465384b14d8dd5c6f70ac29c0555a964e023883a7f65323","device":"5d667e4b0751bcb5","findings":[],"hash":"cd1b901a36ffef3dfb8ebd2c8c6b3a1d1bce67bb8fc6a53771938cc9c5abad0a","nodeId":"5d03ad33-6a9b-48ab-a748-d383a9583860","outcome":"completed","summary":"The revision is complete and verified. The full suite passes with the verifier's fuzz seed, and I found no defect in the builder's revised v4 callback, so there is no findings file.\n\n**What changed since my last accepted round.** The builder reordered the Uniswap v4 sale callback: it now pays the input into the PoolManager first and swaps only the amount the manager credited, so fee-on-transfer tokens sell their measured amount. My earlier suites covered only V2 and V3 venues, so this revision adds adversarial v4 coverage around exactly that change. Existing files were left alone except for one appended mock.\n\n**New test files, all under `test/`:**\n- `SwarmExitV4Fixture.sol`: shared v4 deployment with WETH at its mainnet address, direct and bridged v4 routes, two fee-on-transfer tokens, a honeypot that reverts inside the manager, a V2 leg unwrapped into the native v4 IMD route, and a V2-only token that sells when v4 is broken.\n- `SwarmExitV4Edges.t.sol`: ten tests on the new ordering. A token that credits nothing to the manager is refunded via the callback's new zero-credit guard rather than sold for zero. A honeypot reverting inside the manager is refunded and the in-flight manager marker is rolled back. A fee token bridged through native ETH sells the twice-measured amount. A minimum copied from the pre-fee preview fails and refunds every wei received. A swap failing after settle rolls the credit back. A fuzz over any fee from 0 to 99.99% and any amount proves the outcome is always one of three exact cases: skipped with nothing moved, sold for the measured quote, or refunded in full.\n- `SwarmExitV4Invariant.t.sol`: handler with three actors, random subsets and minimums, partial-fill toggles, liquidity changes, floor changes, stray ETH and expired deadlines. Invariants: the exit holds nothing but stray ETH, the manager is never left locked or with an unsettled delta, the callback is closed between transactions, treasury equals the tax sum, users hold exactly their net proceeds.\n- `mocks/EdgeMocks.sol`: appended a silent-drop token that burns transfers to one sink while returning true.\n\n**Results with the verifier's seed:**\n\n| Suite | Tests | Result |\n|---|---|---|\n| Whole repository | 128 | all pass |\n| New v4 edge tests | 10 | all pass, fuzz at 512 runs |\n| New v4 invariants | 6 invariants, 96 runs, 0 handler reverts | all pass |\n\nA scratch probe confirmed the handler is not vacuous: fee-token sales, refunds and reverts all occur. One note for the reviewer rather than a finding: the builder's own test comment claims the user pays one fee on a v4 fee token, but the assertions and the README correctly reflect two fees, one on the pull and one into the manager.","treeHash":"277dec2d73aa195f1b72b0425ab393d73c1592f2","usage":{"cachedInputTokens":2056341,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":43638,"runtime":"claude","turns":44,"wallClockMs":600702}},{"artifacts":[],"attempt":2,"bundleHash":"1332a18a71ce852902688079f77853e02b496690e1110296b1377db8d7941a0c","device":"8109087d965ddf6c","findings":[],"hash":"d13f19cb1f97cc68bfcdfdd97f13601b6053ba7e98536614ac4ea2f28385502e","nodeId":"32c833d4-7acc-4db8-9e43-826b544036d4","outcome":"completed","summary":"The work is complete and verified. Summary of this revision:\n\n**Outcome**\n\n- All 65 tests pass with the verifier's fuzz seed, `forge build` and `forge fmt --check` are clean, and the four reviewer proofs pass unchanged when copied under `test/scratch/`.\n- The runtime is 24,463 bytes, inside the EIP-170 limit with the pinned compiler settings.\n- `.imd-responses.json` answers all 18 findings (16 fixed, 2 disputed as not code defects).\n\n**What changed in `src/SwarmExit.sol`**\n\n- **Uniswap v4 venue** for token routes and the IMD route. Swaps run through the PoolManager's unlock callback with flash accounting, native ETH as a currency, and 1:1 WETH/ETH bridging between legs. Quotes run the hops inside an unlock and revert with the result, the way Uniswap's own quoter works, so no quoter contract is needed.\n- **No arbitrary-send pattern.** The only `transferFrom` is a non-reverting low-level call from the `sellAll` loop with `msg.sender` as the source. `executeSale` no longer takes a user argument.\n- **Pull capped by allowance**, zero allowance is a `NoAllowance` outcome, and a refused pull is a `SaleFailed` outcome rather than a batch revert.\n- **Refund path hardened.** The catch block no longer calls `forceApprove`. Refunds are best effort; what a token refuses is held in `heldRefunds` and claimed through `claimRefund`, which `rescue` can never touch.\n- **On-chain floor.** A sale that yields nothing or less than the larger of the caller's minimum and `minImdOut` reverts inside the self-call and is refunded.\n- **Pre-pull quote.** Predictable failures are skipped before anything is pulled, so fee-on-transfer tokens pay no fee for a sale that cannot work.\n- Registry refuses addresses without code, unregistering clears the route, a reverting `balanceOf` no longer bricks `previewAll`, and `preview` applies an owner-settable price-impact bound.\n\n**Website and docs**\n\n- Slippage is rounded and validated before any approval, symbols are rendered with `textContent`, sales are batched at the contract's limit, a reverted dry run is bisected token by token, and held refunds get a claim button.\n- The README documents the v4 route the owner must set, the empty V3 WETH/IMD pool, the real trust bound on routes, and the fork rehearsal against the real IMD as an open launch item.\n\n**Not done here:** Slither is not installed on this machine, so the static analysis could not be rerun locally, and no mainnet RPC was available for a fork rehearsal.","treeHash":"0cbdfaeb0262b6e4b9bfdea641aafe272c020791","usage":{"cachedInputTokens":3430260,"inputTokens":738,"model":"claude-fable-5-1","outputTokens":79780,"runtime":"claude","turns":33,"wallClockMs":995507}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2dc755dfe7bd177c","findings":[{"citation":"resolved","description":"The contract can only swap and quote through Uniswap V2 style routers and Uniswap V3 SwapRouter02/QuoterV2 (enum Venue, _swap, _quote). The README tells the owner to set the IMD route to the Uniswap V3 1% WETH/IMD pool. Verified against Ethereum mainnet at block 26188227 (2026-10-14, publicnode RPC): that pool (0xD6A822D028bbf7b6EDfA1533e110Ee40c08551d9) has liquidity() == 0 and holds 6612 wei WETH and 0.436 IMD; QuoterV2.quoteExactInput reverts with 'Unexpected error' for both WETH->IMD and IMD->WETH; there is no WETH/IMD V3 pool at the 0.01%, 0.05% or 0.3% tiers, no Uniswap V2 pair and no SushiSwap pair. IMD's liquidity (222,322 IMD of a 4.17M supply) sits in the Uniswap v4 PoolManager (0x000000000004444c5dc75cB358380D2e3dE08A90), which the README itself says this version cannot route. Consequence on chain: with any configurable IMD route, _quote(second, out) fails, so previewAll reports QuoteFailed for every token that bridges through WETH (BridgeRouteMissing while unset), executeSale reverts in the second _swap so sellAll refunds every such token and reverts NothingSold when the batch holds only such tokens, and quoteImdInBridge reverts NotConfigured('imdRoute quote') so the site shows USD values as unavailable. Requirement 3 (receive IMD for the sale) cannot be met for any swarm token that does not have its own direct V2/V3 pool against IMD, and the IMD launch factory opens its pools on v4 so the swarm tokens' own venues are likely v4 as well (lead). This is a scope decision for the author: add a Uniswap v4 venue (PoolManager unlock/settle or the v4 Universal Router, quoted via the v4 Quoter) for the IMD leg and token legs, or obtain an agreed V2/V3 WETH/IMD pool with liquidity before launch. Merged from audit_math; a no-fork Foundry proof cannot express an on-chain liquidity fact, so the reproduction is the on-chain state.","line":49,"path":"src/SwarmExit.sol","reproduction":"Against any Ethereum mainnet RPC (observed at block 26188227): (1) cast call 0x1F98431c8aD98523631AE4a59f267346ea31F984 'getPool(address,address,uint24)(address)' 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 10000 -> 0xD6A822D028bbf7b6EDfA1533e110Ee40c08551d9; with 100, 500, 3000 -> 0x0. (2) cast call 0xD6A822D028bbf7b6EDfA1533e110Ee40c08551d9 'liquidity()(uint128)' -> 0; WETH.balanceOf(pool) -> 6612; IMD.balanceOf(pool) -> 436143244290069553. (3) cast call 0x61fFE014bA17989E743c5F6cB21bF9697530B21e 'quoteExactInput(bytes,uint256)' <WETH|0x002710|IMD> 10000000000000000 -> 'execution reverted: Unexpected error' (same for IMD->WETH with 1e18). (4) UniswapV2 factory getPair(WETH, IMD) -> 0x0; SushiSwap factory -> 0x0. (5) IMD.balanceOf(0x000000000004444c5dc75cB358380D2e3dE08A90) -> 222322296301092556046803. Expected: the owner can configure setImdRoute so previewAll marks WETH-bridged holdings sellable and sellAll settles them in IMD. Actual: with the README's route (venue 2, SwapRouter02, QuoterV2, WETH|10000|IMD) _quote returns (false,0) and _swap reverts; every bridged token is QuoteFailed in preview and SaleFailed in sellAll; sellAll of only such tokens reverts NothingSold.","severity":"high","snippet":"    enum Venue {","title":"No supported venue can reach IMD liquidity on mainnet: the only WETH/IMD V2/V3 pool is empty, so every WETH-bridged sale is refunded and the product cannot settle in IMD"},{"citation":"resolved","description":"_sellOne reads the seller's live balance (line 362) and pulls exactly that amount with safeTransferFrom (line 368). The amount is not a parameter and the pull is outside the try/catch, so it must succeed for every token or the entire sellAll reverts. web/app.js ensureApprovals (lines 155-158) approves exactly r.balance as read by previewAll at refresh time and skips re-approval when the allowance already covers that stale balance (the README advertises this exact-balance approval). Any increase of the user's balance between approval and inclusion makes the pull exceed the allowance and the whole batch reverts with ERC20InsufficientAllowance: nothing is sold, gas is lost, the refund design never engages, and the site cannot tell which token caused it. A third party can cause it on purpose for 1 wei per attempt (watch the mempool, send 1 wei of any registered token to the target), and reflection or rebasing meme tokens trigger it on every other holder's trade without an attacker. Reported by all four specialists (economics, permissions, flow, math); merged. Minimal fix that keeps the design: pull min(balanceOf(user), allowance(user, address(this))) or accept an explicit amounts[] argument, and/or have the site approve a margin or type(uint256).max. The existing suite only tests a missing approval (test_missingApprovalRevertsWholeCall, test_partialApprovalRevertsWholeCallNothingMoves); it never grows the balance between approval and sale.","line":362,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {IUniswapV2Router} from \"src/interfaces/IDexVenues.sol\";\n\ncontract PlainToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Minimal V2-style router: a fixed 1:2 rate, pulls exactly amountIn, pays tokenOut it holds.\ncontract FixedRateRouter is IUniswapV2Router {\n    using SafeERC20 for IERC20;\n\n    function getAmountsOut(uint256 amountIn, address[] calldata path)\n        external\n        pure\n        returns (uint256[] memory amounts)\n    {\n        amounts = new uint256[](path.length);\n        amounts[0] = amountIn;\n        amounts[path.length - 1] = amountIn * 2;\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);\n        IERC20(path[path.length - 1]).safeTransfer(to, amountIn * 2);\n    }\n}\n\n/// @notice `sellAll` pulls `balanceOf(user)` at execution time, not the amount the user approved.\n/// The website approves the exact balance it displayed. Anyone who sends the user one wei of a\n/// listed token between approval and execution makes the whole sale revert.\ncontract ExactBalancePullGriefTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    PlainToken tokenA;\n    FixedRateRouter router;\n    address user = makeAddr(\"user\");\n    address griefer = makeAddr(\"griefer\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        vm.etch(IMD_ADDR, address(new PlainToken(\"IMD\")).code);\n        router = new FixedRateRouter();\n        PlainToken(IMD_ADDR).mint(address(router), 1_000_000e18);\n        tokenA = new PlainToken(\"A\");\n        exit = new SwarmExit(OWNER);\n\n        address[] memory path = new address[](2);\n        path[0] = address(tokenA);\n        path[1] = IMD_ADDR;\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(tokenA), true);\n        exit.setTokenRoute(address(tokenA), SwarmExit.Venue.UniswapV2, address(router), address(0), abi.encode(path));\n        vm.stopPrank();\n\n        tokenA.mint(user, 10_000e18);\n        tokenA.mint(griefer, 1);\n    }\n\n    function test_oneWeiDonationAfterExactApprovalDoesNotBlockTheSale() public {\n        // The website approves exactly the balance it showed (web/app.js ensureApprovals).\n        vm.prank(user);\n        tokenA.approve(address(exit), 10_000e18);\n\n        // Anyone front-runs the sale with a 1 wei transfer to the user.\n        vm.prank(griefer);\n        tokenA.transfer(user, 1);\n\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(tokenA);\n        uint256[] memory mins = new uint256[](1);\n\n        // Expected: the approved 10_000e18 is sold (or at least the sale does not revert).\n        // Actual: the contract pulls balanceOf(user) = 10_000e18 + 1 and reverts with\n        // ERC20InsufficientAllowance, so the user cannot sell anything.\n        vm.prank(user);\n        (SwarmExit.Outcome[] memory out,,, uint256 net) = exit.sellAll(tokens, mins, block.timestamp);\n        assertTrue(out[0].status == SwarmExit.Status.Sold);\n        assertGt(net, 0);\n        assertEq(IERC20(IMD_ADDR).balanceOf(user), net);\n    }\n}","reproduction":"State: tokenA registered with a working route; user holds 10_000e18 tokenA and calls tokenA.approve(exit, 10_000e18) exactly as the site does; griefer holds 1 wei tokenA. Sequence: griefer.transfer(user, 1); user.sellAll([tokenA], [0], block.timestamp). Expected: the approved 10_000e18 (or all 10_000e18+1) is sold and the user receives IMD. Actual: revert ERC20InsufficientAllowance(exit, 10000000000000000000000, 10000000000000000000001); nothing sold. Confirmed by running the attached proof on this tree: '[FAIL: ERC20InsufficientAllowance(..., 10000000000000000000000, 10000000000000000000001)] test_oneWeiDonationAfterExactApprovalDoesNotBlockTheSale'.","severity":"medium","snippet":"        uint256 balance = erc.balanceOf(user);","title":"sellAll pulls balanceOf(user) at execution time while the site approves the exact balance, so a 1 wei transfer to the user by anyone (or a reflection credit) reverts the whole sale"},{"citation":"resolved","description":"executeSale is an external self-call, so when it reverts every allowance it set inside _swap is already discarded by the EVM; the two forceApprove(…, 0) resets in the catch block (lines 382-385) never change state in any reachable path. They are, however, external calls into the failing token (and the bridge token) made outside the try. A token whose approve reverts for the router spender (anti-bot 'approvals closed' / blocked-spender honeypots, which is exactly why its sale failed inside _swap's forceApprove) reverts again here, and the entire sellAll reverts instead of refunding that token and selling the others. The site's dry run then reverts as a whole with no per-token outcome, so it cannot highlight or exclude the token and the user cannot sell any of their other tokens from the page while holding one such token (requirements 5 and 6). No funds are lost. Reported by flow (medium), math (low) and the test writer (low); merged. Fix: delete the forceApprove calls from the catch block. The existing test test_approveBlockingTokenNeverCostsTheUser accepts either outcome and so does not pin the intended behaviour.","line":382,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {IUniswapV2Router} from \"src/interfaces/IDexVenues.sol\";\n\ncontract PlainToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Honeypot variant: holders can transfer freely, but `approve` reverts for every caller\n/// except the ones the token's owner whitelisted (a common \"trading not open\" pattern).\ncontract ApproveBlockedToken is ERC20 {\n    mapping(address => bool) public allowedApprover;\n\n    constructor() ERC20(\"Trap\", \"TRAP\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n\n    function setAllowedApprover(address who, bool ok) external {\n        allowedApprover[who] = ok;\n    }\n\n    function _approve(address owner_, address spender, uint256 value, bool emitEvent) internal override {\n        require(allowedApprover[owner_], \"TRAP: approvals closed\");\n        super._approve(owner_, spender, value, emitEvent);\n    }\n}\n\n/// @dev Minimal V2-style router: a fixed 1:2 rate, pulls exactly amountIn, pays tokenOut it holds.\ncontract FixedRateRouter is IUniswapV2Router {\n    using SafeERC20 for IERC20;\n\n    function getAmountsOut(uint256 amountIn, address[] calldata path)\n        external\n        pure\n        returns (uint256[] memory amounts)\n    {\n        amounts = new uint256[](path.length);\n        amounts[0] = amountIn;\n        amounts[path.length - 1] = amountIn * 2;\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);\n        IERC20(path[path.length - 1]).safeTransfer(to, amountIn * 2);\n    }\n}\n\n/// @notice The refund path in `_sellOne`'s catch block calls `forceApprove` on the token whose\n/// sale just failed. If that token's `approve` reverts (which is exactly why the sale failed),\n/// the catch block reverts too and the whole `sellAll` reverts instead of refunding that token\n/// and selling the others.\ncontract RefundPathRevertsTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    PlainToken good;\n    ApproveBlockedToken trap;\n    FixedRateRouter router;\n    address user = makeAddr(\"user\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        vm.etch(IMD_ADDR, address(new PlainToken(\"IMD\")).code);\n        router = new FixedRateRouter();\n        PlainToken(IMD_ADDR).mint(address(router), 1_000_000e18);\n        good = new PlainToken(\"GOOD\");\n        trap = new ApproveBlockedToken();\n        trap.setAllowedApprover(user, true); // the user can approve SwarmExit; SwarmExit cannot approve the router\n        exit = new SwarmExit(OWNER);\n\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(good), true);\n        exit.setSwarmToken(address(trap), true);\n        exit.setTokenRoute(\n            address(good), SwarmExit.Venue.UniswapV2, address(router), address(0), _path(address(good), IMD_ADDR)\n        );\n        exit.setTokenRoute(\n            address(trap), SwarmExit.Venue.UniswapV2, address(router), address(0), _path(address(trap), IMD_ADDR)\n        );\n        vm.stopPrank();\n\n        good.mint(user, 1_000e18);\n        trap.mint(user, 1_000e18);\n        vm.startPrank(user);\n        good.approve(address(exit), type(uint256).max);\n        trap.approve(address(exit), type(uint256).max);\n        vm.stopPrank();\n    }\n\n    function _path(address a, address b) internal pure returns (bytes memory) {\n        address[] memory p = new address[](2);\n        p[0] = a;\n        p[1] = b;\n        return abi.encode(p);\n    }\n\n    function test_tokenWhoseApproveRevertsIsRefundedAndOthersStillSell() public {\n        address[] memory tokens = new address[](2);\n        tokens[0] = address(trap);\n        tokens[1] = address(good);\n        uint256[] memory mins = new uint256[](2);\n\n        // Expected (README and NatSpec): trap is refunded with status SaleFailed, good is sold.\n        // Actual: `erc.forceApprove(route.router, 0)` inside the catch block reverts with\n        // \"TRAP: approvals closed\", so the entire sellAll reverts and nothing can be sold.\n        vm.prank(user);\n        (SwarmExit.Outcome[] memory out,,,) = exit.sellAll(tokens, mins, block.timestamp);\n        assertTrue(out[0].status == SwarmExit.Status.SaleFailed);\n        assertEq(trap.balanceOf(user), 1_000e18, \"trap refunded\");\n        assertTrue(out[1].status == SwarmExit.Status.Sold);\n        assertEq(good.balanceOf(user), 0);\n        assertGt(IERC20(IMD_ADDR).balanceOf(user), 0);\n    }\n}","reproduction":"State: two registered tokens with working routes direct to IMD: GOOD (plain ERC20) and TRAP, whose _approve reverts with 'TRAP: approvals closed' for every owner except the user (so the user can approve SwarmExit but SwarmExit cannot approve the router). User holds 1_000e18 of each, both approved; calls sellAll([TRAP, GOOD], [0, 0], block.timestamp). Expected (NatSpec lines 19-20, README step 6): outcomes[0].status == SaleFailed with TRAP refunded in full, outcomes[1].status == Sold and IMD paid for GOOD. Actual: _swap's forceApprove reverts inside executeSale (caught), then the catch block's erc.forceApprove(route.router, 0) reverts with 'TRAP: approvals closed' and the whole sellAll reverts. Confirmed by running the attached proof on this tree: '[FAIL: TRAP: approvals closed] test_tokenWhoseApproveRevertsIsRefundedAndOthersStillSell'.","severity":"medium","snippet":"            erc.forceApprove(route.router, 0);","title":"The refund catch block calls forceApprove on the token whose sale just failed; those calls are no-ops after the revert but turn a refundable failure into a whole-batch revert when the token's approve "},{"citation":"resolved","description":"The documented guarantee (NatSpec lines 19-20, README step 6, brief requirement 6) is that a token whose sale fails is refunded and the remaining tokens are still sold. The refund erc.safeTransfer(user, leftover) at line 387 runs outside the try on an owner-registered token the contract does not control, and the pull at line 368 is outside it as well. A token whose transfer back to the user reverts after its sale reverted makes sellAll revert entirely. A realistic case is the common 'transfer delay' anti-bot pattern keyed on tx.origin (one transfer per block, exempting only the router/pair): the pull user->SwarmExit records the origin's block, the sale then fails for any reason (slippage below the user's minimum, dead pool), and the refund to the user in the same block reverts. The same whole-batch revert happens for a token that blacklists the user or reverts transferFrom out of the wallet (the README acknowledges the latter). Because web/app.js line 178 relies on one sellAll.staticCall over every token previewAll marked sellable, and previewAll only quotes pools, such a token passes preview, the single simulation rejects, the catch at the end of sellAll() logs a generic error, no row is highlighted and 'selected' is never narrowed: the user can sell nothing from the page while holding one such token (requirement 5). No funds are lost (the transaction reverts). Reported by math (medium), flow (medium, combined with the forceApprove mechanism), permissions (low, site side) and the test writer; merged. Design-preserving fix: make the refund best-effort (low-level call) and, when it fails, hold the pulled amount in a refunds[user][token] balance with a claimRefund(token) function while still recording SaleFailed; move the pull inside the self-call so a failing transferFrom becomes a SaleFailed outcome; on the site, bisect the simulation on a whole-call revert so the culprit is marked and the rest still sell. The existing test test_refundBlockedTokenNeverCostsTheUser accepts either outcome and so does not pin the intended behaviour.","line":387,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {IUniswapV2Router} from \"src/interfaces/IDexVenues.sol\";\n\n/// @dev Minimal constant-product pool shared by the mock router.\ncontract Pool {\n    using SafeERC20 for IERC20;\n\n    mapping(bytes32 => uint256) public rA;\n    mapping(bytes32 => uint256) public rB;\n\n    function key(address a, address b) public pure returns (bytes32, bool) {\n        return a < b ? (keccak256(abi.encode(a, b)), false) : (keccak256(abi.encode(b, a)), true);\n    }\n\n    function add(address a, address b, uint256 x, uint256 y) external {\n        IERC20(a).safeTransferFrom(msg.sender, address(this), x);\n        IERC20(b).safeTransferFrom(msg.sender, address(this), y);\n        (bytes32 k, bool f) = key(a, b);\n        if (f) (x, y) = (y, x);\n        rA[k] += x;\n        rB[k] += y;\n    }\n\n    function out(address i, address o, uint256 amt) public view returns (uint256) {\n        (bytes32 k, bool f) = key(i, o);\n        uint256 ri = f ? rB[k] : rA[k];\n        uint256 ro = f ? rA[k] : rB[k];\n        require(ri > 0 && ro > 0, \"no liquidity\");\n        uint256 w = amt * 997;\n        return (w * ro) / (ri * 1000 + w);\n    }\n\n    function swap(address i, address o, uint256 amt, address to) external returns (uint256 got) {\n        uint256 b = IERC20(i).balanceOf(address(this));\n        IERC20(i).safeTransferFrom(msg.sender, address(this), amt);\n        uint256 recv = IERC20(i).balanceOf(address(this)) - b;\n        got = out(i, o, recv);\n        (bytes32 k, bool f) = key(i, o);\n        if (f) {\n            rB[k] += recv;\n            rA[k] -= got;\n        } else {\n            rA[k] += recv;\n            rB[k] -= got;\n        }\n        IERC20(o).safeTransfer(to, got);\n    }\n}\n\ncontract Router is IUniswapV2Router {\n    using SafeERC20 for IERC20;\n\n    Pool public immutable pool;\n\n    constructor(Pool p) {\n        pool = p;\n    }\n\n    function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory a) {\n        a = new uint256[](path.length);\n        a[0] = amountIn;\n        for (uint256 i; i + 1 < path.length; ++i) {\n            a[i + 1] = pool.out(path[i], path[i + 1], a[i]);\n        }\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256 amountOutMin,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);\n        uint256 amt = IERC20(path[0]).balanceOf(address(this));\n        for (uint256 i; i + 1 < path.length; ++i) {\n            IERC20(path[i]).forceApprove(address(pool), amt);\n            address rcpt = i + 2 == path.length ? to : address(this);\n            amt = pool.swap(path[i], path[i + 1], amt, rcpt);\n        }\n        require(amt >= amountOutMin, \"INSUFFICIENT_OUTPUT_AMOUNT\");\n    }\n}\n\ncontract Plain is ERC20 {\n    constructor() ERC20(\"P\", \"P\") {}\n\n    function mint(address to, uint256 a) external {\n        _mint(to, a);\n    }\n}\n\n/// @dev \"Transfer delay\" anti-bot token (one transfer per block per tx.origin, exempting the router\n/// and the pair). The user's pull sets the origin's block; the sale's transfer into the pool then\n/// reverts, and so does the refund back to the user.\ncontract TransferDelayToken is ERC20 {\n    address public router;\n    mapping(address => uint256) public lastBlock;\n\n    constructor() ERC20(\"D\", \"D\") {}\n\n    function setRouter(address r) external {\n        router = r;\n    }\n\n    function mint(address to, uint256 a) external {\n        _mint(to, a);\n    }\n\n    function _update(address from, address to, uint256 value) internal override {\n        if (from != address(0) && to != router) {\n            require(lastBlock[tx.origin] < block.number, \"Only one transfer per block allowed.\");\n            lastBlock[tx.origin] = block.number;\n        }\n        super._update(from, to, value);\n    }\n}\n\ncontract RefundRevertTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    Plain imd;\n    Plain good;\n    TransferDelayToken bad;\n    Pool pool;\n    Router router;\n    address user = makeAddr(\"user\");\n\n    function setUp() public {\n        vm.etch(IMD_ADDR, address(new Plain()).code);\n        imd = Plain(IMD_ADDR);\n        pool = new Pool();\n        router = new Router(pool);\n        exit = new SwarmExit(OWNER);\n        good = new Plain();\n        bad = new TransferDelayToken();\n        bad.setRouter(address(router));\n\n        _liq(address(good), 1_000_000e18, 100_000e18);\n        _liq(address(bad), 1_000_000e18, 100_000e18);\n\n        address[] memory pGood = new address[](2);\n        pGood[0] = address(good);\n        pGood[1] = IMD_ADDR;\n        address[] memory pBad = new address[](2);\n        pBad[0] = address(bad);\n        pBad[1] = IMD_ADDR;\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(good), true);\n        exit.setSwarmToken(address(bad), true);\n        exit.setTokenRoute(address(good), SwarmExit.Venue.UniswapV2, address(router), address(0), abi.encode(pGood));\n        exit.setTokenRoute(address(bad), SwarmExit.Venue.UniswapV2, address(router), address(0), abi.encode(pBad));\n        vm.stopPrank();\n\n        good.mint(user, 1_000e18);\n        bad.mint(user, 1_000e18);\n        vm.startPrank(user);\n        good.approve(address(exit), type(uint256).max);\n        bad.approve(address(exit), type(uint256).max);\n        vm.stopPrank();\n    }\n\n    function _liq(address t, uint256 x, uint256 y) internal {\n        address lp = makeAddr(\"lp\");\n        Plain(t).mint(lp, x);\n        imd.mint(lp, y);\n        vm.startPrank(lp);\n        IERC20(t).approve(address(pool), x);\n        imd.approve(address(pool), y);\n        pool.add(t, IMD_ADDR, x, y);\n        vm.stopPrank();\n    }\n\n    /// Expected: `bad` fails to sell and is refunded (or held for the user), `good` is sold.\n    /// Actual on the current code: the refund transfer in the catch block reverts and the whole\n    /// `sellAll` reverts, so `good` cannot be sold either.\n    function test_refundThatRevertsMustNotBlockOtherSales() public {\n        address[] memory tokens = new address[](2);\n        tokens[0] = address(bad);\n        tokens[1] = address(good);\n        uint256[] memory mins = new uint256[](2);\n        uint256 badBefore = bad.balanceOf(user);\n\n        vm.prank(user, user);\n        (SwarmExit.Outcome[] memory out,,,) = exit.sellAll(tokens, mins, block.timestamp);\n\n        assertTrue(out[1].status == SwarmExit.Status.Sold, \"good token sold\");\n        assertGt(imd.balanceOf(user), 0, \"user received IMD for the good token\");\n        assertTrue(out[0].status == SwarmExit.Status.SaleFailed, \"bad token not sold\");\n        assertEq(bad.balanceOf(user) + bad.balanceOf(address(exit)), badBefore, \"bad token not lost\");\n    }\n}","reproduction":"State: SwarmExit with two registered V2 routes direct to IMD: good (plain ERC20) and bad, an ERC20 whose _update requires lastBlock[tx.origin] < block.number for every transfer not addressed to the router, then records block.number. User holds 1_000e18 of each, both approved. Call: vm.prank(user, user); exit.sellAll([bad, good], [0, 0], block.timestamp). Trace: _sellOne(bad) pulls 1_000e18 (records the block), executeSale -> router -> pool transfer reverts (delay) and is caught; the catch block's erc.safeTransfer(user, 1_000e18) reverts (delay) -> whole sellAll reverts. Expected: outcomes[0].status == SaleFailed with bad refunded or held for the user, outcomes[1].status == Sold and IMD paid for good. Actual: revert 'Only one transfer per block allowed.', nothing sold. Confirmed by running the attached proof on this tree: '[FAIL: Only one transfer per block allowed.] test_refundThatRevertsMustNotBlockOtherSales'.","severity":"medium","snippet":"            if (leftover > 0) erc.safeTransfer(user, leftover);","title":"The refund transfer and the pull are outside the try: a token whose transfer to or from the user reverts escalates one failed sale into reverting the whole batch, and the site cannot exclude it"},{"citation":"resolved","description":"preview marks a token unsellable when its quote is below minImdOut (line 449), but sellAll/executeSale only compare the measured output with the caller-supplied minImdOuts[i] (line 344). With a zero minimum, a swap that legitimately returns 0 IMD (a pool skewed so the output rounds to zero, a token that burns its whole transfer into the pair, or an owner-set route into a router that returns nothing) passes the check: executeSale returns 0, _sellOne records Status.Sold, emits TokenSold(.., 0), and the pulled tokens are gone with no refund. The view and the write therefore disagree about the same token, and the brief's 'sell only tokens that are sellable / refund a failed sale' is not enforced on chain. The site is also exposed: its dry run uses zero minimums and takes the simulated imdOut as the quote, so a simulated outcome of Sold with imdOut == 0 is accepted and the real transaction is signed with minimum 0 * 9700 / 10000 = 0 for that token. NothingSold only protects the case where every token in the batch yields zero. Impact is bounded to holdings whose current pool output is zero (dust-valued or drained pools) and to callers passing zero minimums, hence medium. Related consequence at line 341: when the first leg of a WETH-bridged V3 route returns 0 WETH for a dust input, the second leg calls SwapRouter02.exactInput with amountIn == 0, which SwapRouter02 treats as Constants.CONTRACT_BALANCE (swap the router's own tokenIn balance, paid by the router). Confirmed by mainnet simulation: exactInput with amountIn 0 from an EOA without allowance reverts 'AS' (zero router balance swapped) whereas amountIn 1 reverts 'STF' (pull attempted); if the router ever holds stray WETH it is sold and credited to the caller as proceeds of a token that fetched nothing. Reported by economics (low), flow (low, plus the sentinel as a separate low), test writer (medium); merged. Fix: in executeSale revert when imdOut == 0 (and when bridgeOut == 0 before the second leg), or when imdOut < minImdOut, so the token takes the refund path; on the site, treat a simulated outcome with imdOut below minImdOut as unsellable.","line":344,"path":"src/SwarmExit.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {SwarmExit} from \"src/SwarmExit.sol\";\nimport {IUniswapV2Router} from \"src/interfaces/IDexVenues.sol\";\n\ncontract PlainToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Constant-product V2-style router with Uniswap's 0.3% fee formula. Reserves are set per pair.\ncontract CpRouter is IUniswapV2Router {\n    using SafeERC20 for IERC20;\n\n    mapping(address => mapping(address => uint256)) public reserve; // reserve[tokenIn][tokenOut] = reserve of tokenIn in that pair\n\n    function seed(address a, address b, uint256 ra, uint256 rb) external {\n        IERC20(a).safeTransferFrom(msg.sender, address(this), ra);\n        IERC20(b).safeTransferFrom(msg.sender, address(this), rb);\n        reserve[a][b] += ra;\n        reserve[b][a] += rb;\n    }\n\n    function _out(address i, address o, uint256 amt) internal view returns (uint256) {\n        uint256 ri = reserve[i][o];\n        uint256 ro = reserve[o][i];\n        require(ri > 0 && ro > 0, \"no liquidity\");\n        uint256 w = amt * 997;\n        return (w * ro) / (ri * 1000 + w);\n    }\n\n    function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory a) {\n        a = new uint256[](path.length);\n        a[0] = amountIn;\n        for (uint256 k; k + 1 < path.length; ++k) {\n            a[k + 1] = _out(path[k], path[k + 1], a[k]);\n        }\n    }\n\n    function swapExactTokensForTokensSupportingFeeOnTransferTokens(\n        uint256 amountIn,\n        uint256 amountOutMin,\n        address[] calldata path,\n        address to,\n        uint256\n    ) external {\n        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);\n        uint256 amt = amountIn;\n        for (uint256 k; k + 1 < path.length; ++k) {\n            uint256 got = _out(path[k], path[k + 1], amt);\n            reserve[path[k]][path[k + 1]] += amt;\n            reserve[path[k + 1]][path[k]] -= got;\n            amt = got;\n        }\n        require(amt >= amountOutMin, \"INSUFFICIENT_OUTPUT_AMOUNT\");\n        IERC20(path[path.length - 1]).safeTransfer(to, amt);\n    }\n}\n\n/// @notice `executeSale` only enforces the caller's `minOut`; the contract's own sellability floor\n/// `minImdOut` (which `preview` reports as BelowMinimum) is never enforced on chain. A swap that\n/// yields 0 IMD is therefore settled as `Sold`, the user's tokens are consumed and nothing is refunded.\ncontract ZeroProceedsProofTest is Test {\n    address constant OWNER = 0x0acE1DBE8AAb293cd58D5e3Ea38814f0ccb74782;\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n\n    SwarmExit exit;\n    PlainToken dust; // pool so skewed that any sale rounds to 0 IMD\n    PlainToken good;\n    CpRouter router;\n    address user = makeAddr(\"user\");\n    address lp = makeAddr(\"lp\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        vm.etch(IMD_ADDR, address(new PlainToken(\"IMD\")).code);\n        router = new CpRouter();\n        dust = new PlainToken(\"DUST\");\n        good = new PlainToken(\"GOOD\");\n        exit = new SwarmExit(OWNER);\n\n        // DUST/IMD pool: 1e30 DUST against 1 wei IMD -> getAmountOut(1000e18) == 0.\n        _seed(address(dust), 1e30, 1);\n        _seed(address(good), 1_000_000e18, 100_000e18);\n\n        vm.startPrank(OWNER);\n        exit.setSwarmToken(address(dust), true);\n        exit.setSwarmToken(address(good), true);\n        exit.setTokenRoute(address(dust), SwarmExit.Venue.UniswapV2, address(router), address(0), _path(address(dust)));\n        exit.setTokenRoute(address(good), SwarmExit.Venue.UniswapV2, address(router), address(0), _path(address(good)));\n        vm.stopPrank();\n\n        dust.mint(user, 1_000e18);\n        good.mint(user, 1_000e18);\n        vm.startPrank(user);\n        dust.approve(address(exit), type(uint256).max);\n        good.approve(address(exit), type(uint256).max);\n        vm.stopPrank();\n    }\n\n    function _seed(address t, uint256 rt, uint256 rImd) internal {\n        PlainToken(t).mint(lp, rt);\n        PlainToken(IMD_ADDR).mint(lp, rImd);\n        vm.startPrank(lp);\n        IERC20(t).approve(address(router), rt);\n        IERC20(IMD_ADDR).approve(address(router), rImd);\n        router.seed(t, IMD_ADDR, rt, rImd);\n        vm.stopPrank();\n    }\n\n    function _path(address t) internal pure returns (bytes memory) {\n        address[] memory p = new address[](2);\n        p[0] = t;\n        p[1] = IMD_ADDR;\n        return abi.encode(p);\n    }\n\n    function test_zeroProceedsSaleIsNotSettledAsSold() public {\n        address[] memory tokens = new address[](2);\n        tokens[0] = address(dust);\n        tokens[1] = address(good);\n\n        // preview says DUST is not sellable (quote 0 < minImdOut).\n        SwarmExit.Preview[] memory p = exit.preview(user, tokens);\n        assertFalse(p[0].sellable);\n        assertTrue(p[0].status == SwarmExit.Status.BelowMinimum);\n        assertEq(p[0].expectedImd, 0);\n\n        // The website's dry run (and any direct caller) passes zero minimums.\n        uint256[] memory mins = new uint256[](2);\n        vm.prank(user);\n        (SwarmExit.Outcome[] memory out,,,) = exit.sellAll(tokens, mins, block.timestamp);\n\n        // Expected: DUST is refunded (not Sold) and the user still holds it; GOOD is sold.\n        // Actual on this tree: out[0].status == Sold with imdOut == 0 and the user's DUST is gone.\n        assertTrue(out[1].status == SwarmExit.Status.Sold, \"good token sold\");\n        assertTrue(out[0].status != SwarmExit.Status.Sold, \"a sale yielding zero IMD must not settle as Sold\");\n        assertEq(dust.balanceOf(user), 1_000e18, \"zero-proceeds token refunded to the user\");\n    }\n}","reproduction":"State: token DUST registered with a V2 route DUST -> IMD through a constant-product pool holding 1e30 DUST and 1 wei IMD (getAmountOut(1000e18) == 0); token GOOD with a healthy pool; user holds 1_000e18 of each, both approved. preview(user, [DUST, GOOD])[0] -> sellable == false, status == BelowMinimum, expectedImd == 0. User calls sellAll([DUST, GOOD], [0, 0], block.timestamp) (what the site sends when the dry run returned imdOut 0). Expected: DUST is refunded (status SaleFailed or skipped) and the user still holds 1_000e18 DUST; GOOD sold. Actual: outcomes[0].status == Sold, outcomes[0].imdOut == 0, DUST.balanceOf(user) == 0 (tokens sit in the pool), no TokenRefunded event; GOOD sold. Confirmed by running the attached proof on this tree: '[FAIL: a sale yielding zero IMD must not settle as Sold] test_zeroProceedsSaleIsNotSettledAsSold'.","severity":"medium","snippet":"        if (imdOut < minOut) revert SlippageExceeded(imdOut, minOut);","title":"A sale that yields zero IMD settles as Sold and consumes the user's tokens: executeSale enforces only the caller's minOut, never the contract's own minImdOut floor that preview reports as BelowMinimum"},{"citation":"resolved","description":"_sellOne transfers the full balance in (line 368), tries the sale, and on failure transfers the measured delta back (line 387). The README explicitly routes fee-on-transfer tokens through this path ('Fee-on-transfer tokens sell through V2 routes'). For such a token a failed sale burns one fee on the pull and a second on the refund although nothing was sold, which contradicts requirement 6 ('refund the failed txn token back to the user'). The failure can be triggered by a price move or a sandwich that pushes the output under the user's minImdOuts[i], a missing bridge route, or a dead pool. The site's dry run avoids this only for failures that are reproducible in simulation; a sale that fails on chain because liquidity moved after the simulation still burns the fee twice. Reported by permissions (medium) and flow (low); merged. Design-preserving fix: before pulling, run _quote(route, balance) and the bridge quote and skip the token with a new status when the quote already fails or is below minOut, so predictable failures never pull; and document the residual cost for fee-on-transfer tokens on genuine same-block moves.","line":368,"path":"src/SwarmExit.sol","reproduction":"State: FeeOnTransferToken with a 5% burn on every transfer (test/mocks/MockTokens.sol) registered with a V2 route to WETH; tokenC registered with a working route so the batch settles; user holds 10_000e18 FEE, both approved. Call: user.sellAll([FEE, tokenC], [type(uint256).max, 0], block.timestamp) (or break the FEE/WETH pool as the existing test test_feeOnTransferRefundReturnsEverythingTheExitHolds does). Expected: FEE refunded in full (10_000e18) or not pulled. Actual: outcomes[0].status == SaleFailed, TokenRefunded emitted, FEE.balanceOf(user) == 9_025e18 (500e18 burned on the pull, 475e18 on the refund), 0 IMD for it. Reproduced in test/scratch/MyChecks.t.sol::test_feeTokenFailedSaleBurnsTwoFees (passes asserting 9_025e18) and by the repository's own test_feeOnTransferRefundReturnsEverythingTheExitHolds, which asserts the 9_025e18 outcome.","severity":"medium","snippet":"        erc.safeTransferFrom(user, address(this), balance);","title":"A failed sale of a fee-on-transfer token refunds net of two transfer fees: the token is pulled before the sale is known to be possible, so the user loses about twice the fee and receives no IMD"},{"citation":"resolved","description":"setTokenRoute and setImdRoute accept any non-zero router and quoter (src/SwarmExit.sol:481 'if (router == address(0)) revert ZeroAddress();' is the only check); _swap approves the full amountIn to the router and swaps with amountOutMinimum 0. The only bound on what the user receives is the per-token minImdOuts[i] the caller passes, so a router that quotes well, pulls the tokens and pays exactly the caller's minimum (or nothing when the minimum is 0) completes with status Sold and no refund. Because route changes take effect immediately and sellAll does not bind the caller to the routes it simulated, the owner can also change a route between the site's simulation and inclusion and keep up to the slippage tolerance (3% default) of every sale. This is an owner power and the design asks for an owner, so it is a trust assumption rather than a bypass; it is reported because the README states the opposite guarantee to users and reviewers, and because the site's zero-minimum fallback is the unprivileged amplifier that turns a zero simulated output into a signed zero-minimum sale. Reported by economics and permissions (both low); merged. Fix: state the actual bound in the README (routes are fully trusted; the owner can redirect up to the caller's slippage tolerance and all of a zero-minimum sale), and optionally let sellAll take a hash of the routes it was simulated against and refund when they changed, or add a delay to route changes.","line":60,"path":"README.md","reproduction":"State: owner calls setTokenRoute(tokenA, UniswapV2, thiefRouter, 0, abi.encode([tokenA, IMD])) where thiefRouter.getAmountsOut quotes amountIn/10 and its swap only transferFrom()s the tokens to itself. User holds 10_000e18 tokenA and 1_000e18 tokenC, approves both; preview(user,[tokenA])[0].sellable == true. User calls sellAll([tokenA, tokenC], [0, 0], block.timestamp). Expected per README line 60: the sale fails and tokenA is refunded. Actual: outcomes[0].status == Sold, outcomes[0].imdOut == 0, tokenA.balanceOf(user) == 0, thiefRouter holds 10_000e18 tokenA. Reproduced in test/scratch/MyChecks.t.sol::test_thiefRouteSettlesAsSold.","severity":"low","snippet":"- The owner decides which tokens count as swarm-launched and which pools sell them. A wrong route cannot steal funds (a failed sale refunds) but can make a token unsellable. The owner can pause selling.","title":"Trust assumption misdocumented: an owner-set route is arbitrary code that receives the user's full balance with amountOutMinimum 0, so a wrong or malicious route can keep the tokens while the sale rep"},{"citation":"resolved","description":"_previewOne calls balanceOf on every registered token before any route check and outside any try/catch, and setSwarmToken accepts any non-zero address without checking it has code. One wrong entry (typo, EOA, a token that self-destructed, or a token whose balanceOf reverts for some holders or while paused) makes previewAll revert for all users; web/app.js refresh() depends solely on previewAll, so no token is listed, totals are not shown and the sell button stays disabled until the owner unregisters the entry (requirements 1, 2). The per-token quote legs are already wrapped in try/catch; the balance read is the one periphery call that is not. Reported by economics and flow (both low); merged. Fix: check token.code.length > 0 in setSwarmToken and wrap the balanceOf read in a try so the entry is reported as unsellable (for example QuoteFailed) instead of aborting the whole preview.","line":421,"path":"src/SwarmExit.sol","reproduction":"State: the fixture's registry plus owner.setSwarmToken(0x0000000000000000000000000000000000001234, true) (an address without code). Any caller: previewAll(user). Expected: an array with that entry marked unsellable and the other tokens quoted. Actual: the call reverts (empty return data decoded as uint256) while preview(user, [tokenC]) still works. Reproduced in test/scratch/MyChecks.t.sol::test_noCodeRegistryEntryBricksPreviewAll.","severity":"low","snippet":"        p.balance = IERC20(token).balanceOf(user);","title":"previewAll reverts for every user when any registered address has no code or a reverting balanceOf, so one bad registry entry blanks the website's token list"},{"citation":"resolved","description":"isSwarmToken and _tokenRoutes are coupled (setTokenRoute refuses unregistered tokens at line 172) but the unregister branch of setSwarmToken never touches _tokenRoutes[token]. An owner who unregisters a token because its route is wrong or its pool was drained cannot delete the route (setTokenRoute(token, None, ...) reverts NotSwarmToken), tokenRoute(token) keeps reporting a live route for a non-swarm token, and the next setSwarmToken(token, true) makes the stale route live again with no TokenRouteSet event, so users can be routed through a venue the owner had retired. Selling is still gated by isSwarmToken, so no funds are at risk on their own. Reported by permissions, flow (low) and the test writer (info); merged. Fix: delete _tokenRoutes[token] in the unregister branch and emit TokenRouteSet(token, None, ...), or allow setTokenRoute(token, Venue.None, ...) for unregistered tokens.","line":151,"path":"src/SwarmExit.sol","reproduction":"Owner: setSwarmToken(tokenA, false); tokenRoute(tokenA).venue -> expected None, actual UniswapV2; setTokenRoute(tokenA, None, 0, 0, '') -> reverts NotSwarmToken(tokenA); setSwarmToken(tokenA, true); preview(user, [tokenA])[0].sellable -> expected false (NoRoute until the owner sets a route again), actual true through the stale route. Reproduced in test/scratch/MyChecks.t.sol::test_unregisterKeepsRoute.","severity":"low","snippet":"        isSwarmToken[token] = registered;","title":"setSwarmToken(token, false) leaves the token's route in storage, the route cannot be cleared while unregistered, and re-registering silently resurrects it"},{"citation":"resolved","description":"_previewOne quotes p.balance (the user's wallet balance) while _sellOne sells 'received' (the balance delta after the pull). For a fee-on-transfer token, which the README says is supported through V2 routes, the sale amount is smaller than the quoted amount by the transfer fee on the pull (and the pool receives less again on the swap), so expectedImd overstates the real proceeds; the site shows that number as the token's IMD and USD value and in the totals (requirements 1, 2), and the NatSpec of minImdOuts describes minimums derived from it. A direct caller who sets the minimum 3% under the preview, as the README's step 5 describes, is refunded with SlippageExceeded every time (the site avoids this only because it re-quotes from a zero-minimum sellAll.staticCall). Reported by math (low). Fix: state in the Preview struct/README that the figure is pre-fee, show it as an estimate on the site, and derive minimums only from the simulation (which the site already does).","line":431,"path":"src/SwarmExit.sol","reproduction":"State: feeTok burns 5% on every transfer, V2 route feeTok -> WETH then the V3 WETH -> IMD route, user holds 10_000e18. preview(user, [feeTok])[0].expectedImd == 8960114241323618637858 (8,960 IMD). sellAll([feeTok], [0], block.timestamp) -> outcomes[0].imdOut == 7791738712179540407553 (7,792 IMD), about 13% less in this mock. Expected: preview within slippage of execution. Actual: 8,960 * 0.97 = 8,691 > 7,792, so a minimum 3% under the preview is refunded with SlippageExceeded. Reproduced in test/scratch/MyChecks.t.sol::test_previewOverstatesFeeToken.","severity":"low","snippet":"        (bool ok, uint256 out) = _quote(first, p.balance);","title":"preview quotes the pre-fee wallet balance for fee-on-transfer tokens, so expectedImd and the USD value are overstated and a minimum derived from the preview is always refunded"},{"citation":"resolved","description":"Requirement 5 asks to sell only tokens that 'have enough liquidity to be sold'. The implementation's definition is that every quote leg succeeds and the quoted IMD is at least minImdOut (default 1e15, an absolute amount independent of the holding's size). A quote succeeds for any pool with non-zero reserves, however thin, so a holding whose reserve-ratio value is 1 IMD but which the pool can only pay 0.4977 IMD for (50% impact) is sellable == true. The site then takes the simulated output as the quote and derives the minimum from it, so the 3% slippage guard only protects against movement after the simulation, never against the pool being too thin: one click sells the whole holding into the thin pool at whatever it pays. No funds are stolen, but the 'enough liquidity' guarantee is not implemented and minImdOut cannot express it. Reported by math (low). Fix: in _previewOne compare the full quote with a small-amount quote scaled up (or the pool's spot) and mark the token with a new status above an owner-set maxImpactBps; or state in the README that only the absolute floor is enforced and show the impact on the site.","line":449,"path":"src/SwarmExit.sol","reproduction":"State: token thin with a V2 pool of 1e24 thin against 1e15 wei WETH (0.001 WETH), WETH/IMD at 1,000 IMD per WETH, user holds 1e24 thin (reserve-ratio value 0.001 WETH = 1 IMD). preview(user, [thin])[0] -> expectedImd == 497748649140524926 (0.4977 IMD), sellable == true, status Sold. Expected per requirement 5: flagged as insufficient liquidity (50% price impact). Actual: listed as sellable and included in the one-click sale. Reproduced in test/scratch/MyChecks.t.sol::test_thinPoolSellable.","severity":"low","snippet":"        if (out < minImdOut) {","title":"Sellability has no price-impact bound: the only liquidity gate is the absolute minImdOut floor, so a holding that would move its pool by 50% is still marked sellable"},{"citation":"resolved","description":"The slippage field is <input type=\"number\" min=\"0\" max=\"5000\"> (web/index.html:55) with no step attribute, so the browser returns a decimal string such as '2.5'. Number('2.5') is 2.5, the clamp keeps it, and BigInt(2.5) throws RangeError. slippageBps() is called in sellAll() after ensureApprovals() has already sent the approval transactions (line 169-171), so the user pays for approvals, the catch logs 'Error: The number 2.5 cannot be converted to a BigInt because it is not an integer' and no sellAll transaction is sent. Reported by math (low). Fix: BigInt(Math.round(...)) or Math.trunc, set step=\"1\" on the input, and compute the slippage before sending approvals.","line":148,"path":"web/app.js","reproduction":"Open the page, connect, type 2.5 (or 0.5) in the Slippage (bps) field, click Sell. Expected: minimums 2 or 3 bps under the simulated outputs and the transaction sent. Actual: after the approvals, the log shows 'Error: The number 2.5 cannot be converted to a BigInt because it is not an integer' and no sellAll transaction is sent. Checked: node -e 'BigInt(2.5)' -> RangeError: The number 2.5 cannot be converted to a BigInt because it is not an integer.","severity":"low","snippet":"    const v = BigInt(Math.max(0, Math.min(5000, Number($(\"slippage\").value) || 0)));","title":"A non-integer slippage value throws in BigInt() after the approvals were already sent, and no sale transaction is sent"},{"citation":"resolved","description":"render() builds each row with tr.innerHTML and interpolates r.symbol, which refresh() reads from the token contract's symbol() (line 111) without escaping. Every other interpolated value is an address, a formatted number or a static string, so the symbol is the only injection point, but it is enough: a registered token whose symbol() returns markup such as <img src=x onerror=\"...\"> executes in the page context where window.ethereum and the connected signer are available, and can for example prompt the wallet for an approve() of the user's tokens to an attacker or rewrite the displayed balances and totals. The token contracts are chosen by the owner (registered swarm launches), so the precondition is a swarm token with a malicious or compromised symbol string; the page must not trust token metadata. Fix: set symbol with textContent (or escape it) and never interpolate contract-returned strings into innerHTML.","line":134,"path":"web/app.js","reproduction":"State: a registered token whose symbol() returns '<img src=x onerror=\"alert(document.title)\">' (any ERC20 whose constructor takes the symbol string), held by the user. Connect and refresh. Expected: the symbol is shown as text. Actual: the img element is inserted into the table row and its onerror handler runs (alert fires) in the connected page; the same handler could call window.ethereum.request({method:'eth_sendTransaction', ...}).","severity":"low","snippet":"        <td><strong>${r.symbol}</strong><br/><span class=\"addr muted\">${r.token}</span></td>","title":"Token symbol() is rendered as HTML without escaping: a registered token with a crafted symbol runs script in the wallet-connected page"},{"citation":"resolved","description":"sellAll reverts TooManyTokens above 50 tokens (src/SwarmExit.sol:301), while the site passes every sellable row in one call, both for the dry run (line 178) and the real call (line 198). With 51 or more sellable tokens the staticCall reverts, the site logs an error and nothing can be sold; there is no way to sell a subset from the UI. No funds at risk. Reported by economics (info). Fix: split 'selected' into batches of at most 50 and send one sellAll per batch, or expose selection.","line":178,"path":"web/app.js","reproduction":"State: 51 registered tokens with routes, all held by the user. Click Sell. Expected: sales proceed in batches. Actual: exit.sellAll.staticCall reverts with TooManyTokens(); the log shows 'Error: ...' and the sale never starts.","severity":"info","snippet":"      const sim = await exit.sellAll.staticCall(selected.map((r) => r.token), selected.map(() => 0n), deadline());","title":"Website does not chunk at MAX_TOKENS_PER_SALE: a wallet holding more than 50 sellable swarm tokens cannot sell at all from the page"},{"citation":"resolved","description":"The check reads a flag the same onlyOwner role writes, so the owner can always setSwarmToken(token, false) then rescue(token, to, amount) and re-register. The README presents the guard as the reason the owner cannot claim a user's tokens in flight; the real reason is that sellAll is nonReentrant and settles or refunds within one transaction, so swarm tokens never rest in the contract. No funds are at risk today. Reported by permissions (info). Either document it as a convenience guard or make it meaningful (refuse to rescue any token that currently has a route or was ever registered).","line":222,"path":"src/SwarmExit.sol","reproduction":"tokenA.mint(exit, 1e18); owner.rescue(tokenA, owner, 1e18) -> reverts CannotRescueSwarmToken; owner.setSwarmToken(tokenA, false); owner.rescue(tokenA, owner, 1e18) -> succeeds; owner.setSwarmToken(tokenA, true) restores the registry. The repository's own test_rescueSwarmTokenOnlyAfterUnregistering asserts this sequence.","severity":"info","snippet":"        if (isSwarmToken[token]) revert CannotRescueSwarmToken();","title":"rescue's swarm-token guard is bypassable by the same role through setSwarmToken(token, false); the README's stronger claim rests on nonReentrant and same-transaction settlement instead"},{"citation":"resolved","description":"tax = grossImd * 200 / 10000 truncates, so each sellAll pays the treasury at most 1 wei of IMD less than 2%, and a call with grossImd < 50 pays zero. Economically negligible (1 wei per call, not farmable for value); grossImd * 200 cannot overflow for IMD's 4.17e24 supply. Reported by math (info). If the 2% is meant as a floor, use Math.ceilDiv.","line":318,"path":"src/SwarmExit.sol","reproduction":"grossImd = 49 -> tax 0 (1 with ceiling); grossImd = 149 -> tax 2 (2.98 truncated). Reproduced in test/scratch/MyChecks.t.sol::test_taxRoundsDown and by the repository's test_oneWeiBalanceSellsWithZeroTax.","severity":"info","snippet":"        tax = (grossImd * SELL_TAX_BPS) / BPS;","title":"Sell tax rounds down, so the treasury receives up to 1 wei per call less than 2% and gross proceeds below 50 wei pay no tax"},{"citation":"resolved","description":"Checked on mainnet at block 26188227: IMD symbol 'IMD', total supply 4,174,192.949 IMD (18 decimals), runtime code about 23 KB. The fixture etches MockERC20 at 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7, so the IMD.safeTransfer(TREASURY, tax) and IMD.safeTransfer(user, netImd) at lines 320-321, and the balance-delta measurement at line 343, are only exercised against a plain ERC20. If the real token applies a fee, a max-wallet or a blacklist on transfers from a non-pair contract, the emitted SoldAll amounts would overstate what arrives or the whole sale would revert. Not a code defect on its own; an open item for the launch (reported by flow as info): run the happy path on a mainnet fork against the real IMD before release.","line":32,"path":"src/SwarmExit.sol","reproduction":"On a mainnet fork with a WETH/IMD venue that has liquidity: configure the IMD route, sell any swarm token, and assert IMD.balanceOf(user) == netImd and IMD.balanceOf(TREASURY) increased by tax. The repository suite only asserts this against the etched MockERC20.","severity":"info","snippet":"    IERC20 public constant IMD = IERC20(0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7);","title":"The real IMD token is a custom contract with an owner; the suite etches a plain ERC20 at its address, so the settlement transfers are untested against the token's real transfer rules"},{"citation":"resolved","description":"_previewOne decides sellability from quotes only. A token that quotes fine but reverts on transfer into the pool, or on approving the router, is reported sellable == true. The README acknowledges this and relies on the website's eth_call dry run of sellAll to exclude such tokens; integrators who use preview/previewAll directly (as requirement 5 describes) will list honeypots as sellable. The existing test test_previewStatuses asserts this behaviour ('honeypot looks sellable from quotes alone'). Reported by the test writer (info); recorded so the limitation is explicit.","line":453,"path":"src/SwarmExit.sol","reproduction":"preview(user, [honey]) with honey.setBlocked(pool) returns sellable == true, status Sold. sellAll([honey], [0], block.timestamp) reverts NothingSold. Expected from requirement 5: the unsellable token is flagged and no transaction initiated.","severity":"info","snippet":"        p.sellable = true;","title":"preview reports honeypots and approve-blocking tokens as sellable because sellability is decided from quotes alone; integrators using preview directly must dry-run sellAll themselves"}],"hash":"d25fc421df0c226d9546a9d7bfaba11f18ace13af019554488b074076d1e5b82","nodeId":"649d41fa-5838-4f17-80ba-30d3ebe41463","outcome":"completed","summary":"Review complete. The findings file is written and verified: every cited snippet matches the tree, all 16 entry points have a coverage row, and the four attached proofs fail on the current code for the stated reasons.\n\n**Outcome: 18 findings (1 high, 5 medium, 7 low, 5 info), written to `.imd-findings.json`.**\n\nThe specialists' reports merged into these findings after reproduction:\n\n1. **High: no supported venue reaches IMD liquidity on mainnet.** I verified on chain at block 26188227 that the only WETH/IMD V3 pool has zero liquidity, QuoterV2 reverts both directions, no V2 or Sushi pair exists, and IMD's liquidity sits in the Uniswap v4 PoolManager, which the contract cannot route. Every WETH-bridged sale would be refunded and the product cannot settle in IMD. No Foundry proof is possible without a fork, so the reproduction is the set of cast calls.\n2. **Medium: exact-balance pull.** Any 1 wei transfer to the user after the site's exact approval reverts the whole sale. Four specialists reported it; merged, proof attached and failing.\n3. **Medium: redundant forceApprove calls in the catch block** turn a refundable failure into a whole-batch revert. Proof attached and failing.\n4. **Medium: refund transfer and pull sit outside the try**, so a token whose transfer reverts blocks the batch and the site cannot exclude it. Proof attached and failing.\n5. **Medium: a zero-IMD swap settles as Sold** and consumes the tokens; preview calls the same token unsellable. I wrote a self-contained proof that fails on this tree. The SwapRouter02 zero-amount sentinel lead was folded in after a mainnet simulation confirmed the behaviour.\n6. **Medium: failed sale of a fee-on-transfer token burns two fees.** Reproduced with the repository's own mocks.\n\nLower findings cover the misdocumented route trust assumption, previewAll bricking on a bad registry entry, stale routes after unregistering, pre-fee preview quotes, no price-impact bound, and the website's decimal-slippage crash and token-chunking gap. One finding is mine alone: the site renders token symbol() into innerHTML unescaped, which lets a crafted token run script in the wallet-connected page.\n\nDropped nothing outright; the fourth proof file and all specialist leads reproduced. The duplicate \"website cannot exclude a transfer-reverting token\" report was folded into finding 4 rather than kept separately.","treeHash":null,"usage":{"cachedInputTokens":2602470,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":45153,"runtime":"claude","turns":38,"wallClockMs":609478}},{"artifacts":[],"attempt":1,"bundleHash":"babbfc9181fce1db02349bf8017d146c3c795c4fb6c1370f5375778b7f6a8ba3","device":"8df16409e9706163","findings":[],"hash":"e4ba6e120a71929eb229d03e375d15acc68b7b7579dbc1ae7c40a0b220de84a8","nodeId":"bcb1d52e-ab77-4c09-98ce-eb5a124eaaad","outcome":"completed","summary":"Created [launch.json](/root/.identitymd/work/35d16e4a-e1e6-43b7-9efe-3b7b1fe99404/bcb1d52e-ab77-4c09-98ce-eb5a124eaaad/launch.json) for `SwarmExit` with `[\"$owner\"]`.\n\nManifest structure and constructor ABI checks passed. `forge build` succeeded; all 34 tests passed with the specified fuzz seed.\n\nOnly `launch.json` is submitted. Notes document the required owner configuration of tokens, routes, and price feed.","treeHash":"875d812923e8d291ee74d5c37e03622ef63bd1f7","usage":{"cachedInputTokens":143872,"inputTokens":55957,"model":"gpt-6-astra","outputTokens":2721,"runtime":"codex","turns":3,"wallClockMs":82965}}],"verification":[{"checks":[{"durationMs":4702,"exitCode":0,"name":"build","output":"Compiling 42 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.57s\nCompiler run successful!\n","passed":true},{"durationMs":270,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 36 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 439217, ~: 480038)\nLogs:\n  Bound result 99999000000000000004494\n  Bound result 1061922874\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 847350, ~: 847505)\nLogs:\n  Bound result 99999000000000000000377\n  Bound result 9999000000000000002492\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1032129)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47417)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 10685064)\n[PASS] test_ethUsdPriceChecks() (gas: 126571)\n[PASS] test_executeSaleOnlySelf() (gas: 32750)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 508551)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1103705)\n[PASS] test_inputValidation() (gas: 736668)\n[PASS] test_missingApprovalIsSkippedAndAloneRevertsNothingSold() (gas: 420598)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 453394)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 479119)\n[PASS] test_nothingSoldReverts() (gas: 486557)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203322)\n[PASS] test_pauseBlocksSelling() (gas: 723451)\n[PASS] test_previewAllCoversRegistry() (gas: 466181)\n[PASS] test_previewBridgeRouteMissing() (gas: 408995)\n[PASS] test_previewStatuses() (gas: 797669)\n[PASS] test_pullIsCappedByAllowanceNotBalance() (gas: 469840)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28693)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523310)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190689)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97881)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1938588)\n[PASS] test_registryAddRemove() (gas: 304662)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 204488)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16291967)\n[PASS] test_sellAllEmitsEvents() (gas: 429100)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1287757)\n[PASS] test_setImdRouteValidation() (gas: 201698)\n[PASS] test_setTokenRouteValidation() (gas: 321092)\n[PASS] test_slippageAtExecutionRefundsAndRollsBackFirstLeg() (gas: 926407)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 622818)\n[PASS] test_twoStepOwnership() (gas: 103168)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 614829)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 120.45ms (208.23ms CPU time)\n\nRan 27 tests for test/SwarmExitRevision.t.sol:SwarmExitRevisionTest\n[PASS] testFuzz_v4SaleConservesImdAndLeavesNothingBehind(uint96,uint96) (runs: 256, μ: 1456837, ~: 1456985)\nLogs:\n  Bound result 965816929523717477493\n  Bound result 99999000034779453901284\n\n[PASS] test_bridgeMismatchIsNotSellable() (gas: 480751)\n[PASS] test_contractFloorIsEnforcedOnChain() (gas: 523549)\n[PASS] test_feeOnTransferTokenPaysNoFeeForAPredictableFailure() (gas: 367951)\n[PASS] test_feeOnTransferTokenSellsThroughV4WithMeasuredAmount() (gas: 2823948)\n[PASS] test_honeypotIsStillRefundedOnChain() (gas: 609999)\n[PASS] test_oneWeiBalanceSellsNothingBelowTheFloor() (gas: 209052)\n[PASS] test_priceImpactBoundInPreview() (gas: 2076718)\n[PASS] test_pullThatRevertsIsASaleFailureNotABatchRevert() (gas: 375346)\n[PASS] test_quoteImdInBridgeReversesMultiHopV4Route() (gas: 1393285)\n[PASS] test_quoteImdInBridgeReversesV4Route() (gas: 234078)\n[PASS] test_refundRefusedByTokenIsHeldAndClaimable() (gas: 965308)\n[PASS] test_registryRefusesAddressesWithoutCode() (gas: 37961)\n[PASS] test_rescueEthAndStrayTokens() (gas: 750877)\n[PASS] test_revertingBalanceDoesNotBrickPreviewAll() (gas: 3119636)\n[PASS] test_routeThatPaysNothingIsRefundedNotSettled() (gas: 1114018)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16291923)\n[PASS] test_unlockCallbackOnlyFromTheManagerInFlight() (gas: 64131)\n[PASS] test_unregisterClearsTheRoute() (gas: 232941)\n[PASS] test_v2ToWethThenV4NativeImdRoute() (gas: 1129358)\n[PASS] test_v4DirectRouteQuoteMatchesExecution() (gas: 1049314)\n[PASS] test_v4NativeLegSettlesFirstAndSellsEverything() (gas: 1565215)\n[PASS] test_v4PartialFillFailsTheSaleAndRefunds() (gas: 779878)\n[PASS] test_v4QuoteFailsOnMissingPool() (gas: 211225)\n[PASS] test_v4RouteValidation() (gas: 209322)\n[PASS] test_v4ToNativeThenV4NativeImdRoute() (gas: 1189474)\n[PASS] test_v4ToNativeThenWrapIntoV3ImdRoute() (gas: 1276015)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 124.31ms (199.69ms CPU time)\n\nRan 2 test suites in 128.50ms (244.76ms CPU time): 63 tests passed, 0 failed, 0 skipped (63 total tests)\n","passed":true},{"durationMs":68,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.claimRefund(address)\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.receive()\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMaxImpactBps(uint256)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unlockCallback(bytes)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":86,\"foundry.toml\":20,\"launch.json\":10,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":851,\"src/interfaces/IDexVenues.sol\":89,\"test/SwarmExit.t.sol\":751,\"test/SwarmExitRevision.t.sol\":666,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":336,\"web/app.js\":318,\"web/config.js\":13,\"web/index.html\":96,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"00880417221d37dc709ecd7616980560492c632dd1876ebaa9684668e118562a","verifiedTreeHash":"ea53fa66d92277850af51fb5b89c6ef61ced8e4f","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":4426,"exitCode":0,"name":"build","output":"Compiling 42 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.32s\nCompiler run successful!\n","passed":true},{"durationMs":212,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 25 tests for test/SwarmExitRevision.t.sol:SwarmExitRevisionTest\n[PASS] testFuzz_v4SaleConservesImdAndLeavesNothingBehind(uint96,uint96) (runs: 256, μ: 1456758, ~: 1456905)\nLogs:\n  Bound result 9999000000000000007794\n  Bound result 99999000000000000003857\n\n[PASS] test_bridgeMismatchIsNotSellable() (gas: 480773)\n[PASS] test_contractFloorIsEnforcedOnChain() (gas: 523549)\n[PASS] test_feeOnTransferTokenPaysNoFeeForAPredictableFailure() (gas: 367951)\n[PASS] test_honeypotIsStillRefundedOnChain() (gas: 610024)\n[PASS] test_oneWeiBalanceSellsNothingBelowTheFloor() (gas: 209052)\n[PASS] test_priceImpactBoundInPreview() (gas: 2076705)\n[PASS] test_pullThatRevertsIsASaleFailureNotABatchRevert() (gas: 375257)\n[PASS] test_quoteImdInBridgeReversesMultiHopV4Route() (gas: 1393259)\n[PASS] test_quoteImdInBridgeReversesV4Route() (gas: 234059)\n[PASS] test_refundRefusedByTokenIsHeldAndClaimable() (gas: 965308)\n[PASS] test_registryRefusesAddressesWithoutCode() (gas: 37983)\n[PASS] test_rescueEthAndStrayTokens() (gas: 750877)\n[PASS] test_revertingBalanceDoesNotBrickPreviewAll() (gas: 3119672)\n[PASS] test_routeThatPaysNothingIsRefundedNotSettled() (gas: 1113981)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16249234)\n[PASS] test_unlockCallbackOnlyFromTheManagerInFlight() (gas: 64153)\n[PASS] test_unregisterClearsTheRoute() (gas: 232941)\n[PASS] test_v2ToWethThenV4NativeImdRoute() (gas: 1129335)\n[PASS] test_v4DirectRouteQuoteMatchesExecution() (gas: 1049370)\n[PASS] test_v4PartialFillFailsTheSaleAndRefunds() (gas: 722189)\n[PASS] test_v4QuoteFailsOnMissingPool() (gas: 211187)\n[PASS] test_v4RouteValidation() (gas: 209300)\n[PASS] test_v4ToNativeThenV4NativeImdRoute() (gas: 1189350)\n[PASS] test_v4ToNativeThenWrapIntoV3ImdRoute() (gas: 1275896)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 90.74ms (151.29ms CPU time)\n\nRan 36 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 437278, ~: 480038)\nLogs:\n  Bound result 99999000000000000004806\n  Bound result 19680\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 847350, ~: 847505)\nLogs:\n  Bound result 99999000000000000012069\n  Bound result 9999000000000000000019\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1032129)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47417)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 10661727)\n[PASS] test_ethUsdPriceChecks() (gas: 126571)\n[PASS] test_executeSaleOnlySelf() (gas: 32750)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 508551)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1103705)\n[PASS] test_inputValidation() (gas: 736668)\n[PASS] test_missingApprovalIsSkippedAndAloneRevertsNothingSold() (gas: 420598)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 453394)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 479119)\n[PASS] test_nothingSoldReverts() (gas: 486557)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203322)\n[PASS] test_pauseBlocksSelling() (gas: 723451)\n[PASS] test_previewAllCoversRegistry() (gas: 466181)\n[PASS] test_previewBridgeRouteMissing() (gas: 408995)\n[PASS] test_previewStatuses() (gas: 797669)\n[PASS] test_pullIsCappedByAllowanceNotBalance() (gas: 469840)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28693)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523310)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190689)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97881)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1938588)\n[PASS] test_registryAddRemove() (gas: 304662)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 204488)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16249278)\n[PASS] test_sellAllEmitsEvents() (gas: 429100)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1287757)\n[PASS] test_setImdRouteValidation() (gas: 201698)\n[PASS] test_setTokenRouteValidation() (gas: 321092)\n[PASS] test_slippageAtExecutionRefundsAndRollsBackFirstLeg() (gas: 926407)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 622818)\n[PASS] test_twoStepOwnership() (gas: 103168)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 614829)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 90.77ms (225.23ms CPU time)\n\nRan 2 test suites in 91.99ms (181.51ms CPU time): 61 tests passed, 0 failed, 0 skipped (61 total tests)\n","passed":true},{"durationMs":60,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.claimRefund(address)\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.receive()\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMaxImpactBps(uint256)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unlockCallback(bytes)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":86,\"foundry.toml\":20,\"launch.json\":10,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":844,\"src/interfaces/IDexVenues.sol\":89,\"test/SwarmExit.t.sol\":751,\"test/SwarmExitRevision.t.sol\":612,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":336,\"web/app.js\":318,\"web/config.js\":13,\"web/index.html\":96,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"029b7a17d5261472784819c0adb6bca856ce2a783ade201b60c6e29d53266004","verifiedTreeHash":"9f8edfea648305375e3385e82ecdc631340e8640","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":4673,"exitCode":0,"name":"build","output":"Compiling 43 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.54s\nCompiler run successful!\n","passed":true},{"durationMs":209,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 15 tests for test/SwarmExitV4.t.sol:SwarmExitV4Test\n[PASS] test_feeOnTransferTokenOnV4IsRolledBack() (gas: 1861863)\n[PASS] test_partialFillIsQuoteFailed() (gas: 197789)\n[PASS] test_partialFillIsRolledBackOthersSell() (gas: 573265)\n[PASS] test_previewQuotesV2ThenV4ImdLeg() (gas: 239981)\n[PASS] test_previewQuotesV4MultiHop() (gas: 285285)\n[PASS] test_quoteImdInBridgeReversesMultiHopV4Route() (gas: 525242)\n[PASS] test_quoteImdInBridgeReversesV4Route() (gas: 175670)\n[PASS] test_quoteImdInBridgeUnliquidV4PoolIsNotConfigured() (gas: 1186091)\n[PASS] test_sellV2TokenThenV4ImdLegUnwrapsWeth() (gas: 688081)\n[PASS] test_sellV4TokenEndingInNativeEthWrapsForTheImdLeg() (gas: 952424)\n[PASS] test_sellV4TokenThroughEthIntoImd() (gas: 661165)\n[PASS] test_slippageOnV4LegRollsBack() (gas: 619275)\n[PASS] test_strayEthIsRefused() (gas: 71218)\n[PASS] test_unlockCallbackOnlyFromActivePoolManager() (gas: 65796)\n[PASS] test_v4RouteValidation() (gas: 265825)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 70.43ms (13.87ms CPU time)\n\nRan 44 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 464949, ~: 460552)\nLogs:\n  Bound result 99999000000000000000042\n  Bound result 1\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 815760, ~: 815917)\nLogs:\n  Bound result 10964816929523717477494\n  Bound result 9999000034779453901284\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1027166)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47461)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 9775937)\n[PASS] test_ethUsdPriceChecks() (gas: 126325)\n[PASS] test_executeSaleOnlySelf() (gas: 35174)\n[PASS] test_failedFeeOnTransferSaleChargesNoFee() (gas: 765684)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 486423)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1031671)\n[PASS] test_inputValidation() (gas: 708928)\n[PASS] test_missingApprovalIsSkippedNotReverted() (gas: 408872)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 568607)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 467924)\n[PASS] test_nothingSoldReverts() (gas: 588289)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203104)\n[PASS] test_partialApprovalSellsTheApprovedPart() (gas: 447738)\n[PASS] test_pauseBlocksSelling() (gas: 680254)\n[PASS] test_previewAllCoversRegistry() (gas: 378656)\n[PASS] test_previewAllSurvivesAnUnreadableToken() (gas: 1058164)\n[PASS] test_previewBridgeRouteMissing() (gas: 411497)\n[PASS] test_previewStatuses() (gas: 715453)\n[PASS] test_proceedsBelowFloorAreRolledBackNotSold() (gas: 1004937)\n[PASS] test_pullIsCappedByAllowanceSoADonationCannotBlockTheSale() (gas: 406926)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28627)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 525870)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 193290)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97881)\n[PASS] test_reRegisteringDoesNotResurrectAnOldRoute() (gas: 239360)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1919002)\n[PASS] test_registryAddRemove() (gas: 328658)\n[PASS] test_registryRefusesAddressesWithoutCode() (gas: 98463)\n[PASS] test_rescueGuardIsAConvenienceNotALimit() (gas: 231397)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 201046)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 14771931)\n[PASS] test_sellAllEmitsEvents() (gas: 409670)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1215525)\n[PASS] test_setImdRouteValidation() (gas: 207039)\n[PASS] test_setTokenRouteValidation() (gas: 326225)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 862061)\n[PASS] test_tokenBlockingThePullIsSaleFailedOthersSell() (gas: 469167)\n[PASS] test_twoStepOwnership() (gas: 103234)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 591705)\n[PASS] test_zeroProceedsAreRolledBackEvenWithZeroFloor() (gas: 1800890)\nSuite result: ok. 44 passed; 0 failed; 0 skipped; finished in 70.52ms (194.50ms CPU time)\n\nRan 2 test suites in 72.65ms (140.95ms CPU time): 59 tests passed, 0 failed, 0 skipped (59 total tests)\n","passed":true},{"durationMs":80,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.executeSale(address,address,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.receive()\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unlockCallback(bytes)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":95,\"foundry.toml\":20,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":726,\"src/interfaces/IDexVenues.sol\":86,\"test/SwarmExit.t.sol\":867,\"test/SwarmExitV4.t.sol\":359,\"test/mocks/MockTokens.sol\":108,\"test/mocks/MockV4.sol\":135,\"test/mocks/MockVenues.sol\":178,\"web/app.js\":240,\"web/config.js\":13,\"web/index.html\":95,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2293,"exitCode":0,"name":"slither","output":"[high/high] arbitrary-send-erc20 at src/SwarmExit.sol:380: SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415) uses arbitrary from in transferFrom: erc.safeTransferFrom(user,address(this),amount) (src/SwarmExit.sol#393)\n[high/medium] reentrancy-balance at src/SwarmExit.sol:380: Reentrancy in SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:380: Reentrancy in SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:467: Reentrancy in SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#467-495):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:380: Reentrancy in SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:467: Reentrancy in SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#467-495):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:380: Reentrancy in SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:380: Reentrancy in SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:380: Reentrancy in SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:467: Reentrancy in SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#467-495):\n[medium/high] incorrect-equality at src/SwarmExit.sol:380: SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:467: SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#467-495) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:467: SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#467-495) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:380: SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:380: SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415) uses a dangerous strict equality:\n[medium/medium] unused-return at src/SwarmExit.sol:467: SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#467-495) ignores return value by IUniswapV3SwapRouter(route.router).exactInput(IUniswapV3SwapRouter.ExactInputParams({path:route.data,recipient:address(this),amountIn:amountIn,amountOutMinimum:0})) (src/SwarmExit.sol#484-489)\n[medium/medium] unused-return at src/SwarmExit.sol:618: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#618-636) ignores return value by (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#629-635)\n[medium/medium] unused-return at src/SwarmExit.sol:299: SwarmExit.ethUsdPrice() (src/SwarmExit.sol#299-305) ignores return value by (None,answer,None,updatedAt,None) = ethUsdFeed.latestRoundData() (src/SwarmExit.sol#301)\n[medium/medium] unused-return at src/SwarmExit.sol:467: SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#467-495) ignores return value by IUniswapV4PoolManager(route.router).unlock(abi.encode(false,route.data,amountIn)) (src/SwarmExit.sol#475)\n[medium/medium] unused-return at src/SwarmExit.sol:418: SwarmExit.unlockCallback(bytes) (src/SwarmExit.sol#418-439) ignores return value by pm.settle() (src/SwarmExit.sol#434)\n[medium/medium] unused-return at src/SwarmExit.sol:418: SwarmExit.unlockCallback(bytes) (src/SwarmExit.sol#418-439) ignores return value by pm.settle{value: amountIn}() (src/SwarmExit.sol#430)\n[low/medium] calls-loop at src/SwarmExit.sol:535: SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#535-548) has external calls inside a loop: (success,reason) = manager.call(abi.encodeCall(IUniswapV4PoolManager.unlock,(abi.encode(true,data,amountIn)))) (src/SwarmExit.sol#540-541)\n[low/medium] calls-loop at src/SwarmExit.sol:445: SwarmExit._sellOne(address,address,uint256,uint256) (src/SwarmExit.sol#445-464) has external calls inside a loop: (status,amountIn,imdOut) = this.executeSale(user,token,minOut,deadline) (src/SwarmExit.sol#454-463)\n[low/medium] calls-loop at src/SwarmExit.sol:565: SwarmExit._tryBalanceOf(address,address) (src/SwarmExit.sol#565-569) has external calls inside a loop: (success,ret) = token.staticcall(abi.encodeCall(IERC20.balanceOf,(account))) (src/SwarmExit.sol#566)\n[low/medium] calls-loop at src/SwarmExit.sol:618: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#618-636) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#629-635)\n[low/medium] calls-loop at src/SwarmExit.sol:618: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#618-636) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#623-627)\n[low/medium] calls-loop at src/SwarmExit.sol:618: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#618-636) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#629-635)\n[low/medium] calls-loop at src/SwarmExit.sol:499: SwarmExit._swapV4Hops(address,address,SwarmExit.V4Hop[],uint256) (src/SwarmExit.sol#499-533) has external calls inside a loop: delta = IUniswapV4PoolManager(manager).swap(key,IUniswapV4PoolManager.SwapParams({zeroForOne:zeroForOne,amountSpecified:- int256(amount),sqrtPriceLimitX96:V4_MIN_PRICE_LIMIT}),) (src/SwarmExit.sol#516-525)\n[low/medium] calls-loop at src/SwarmExit.sol:499: SwarmExit._swapV4Hops(address,address,SwarmExit.V4Hop[],uint256) (src/SwarmExit.sol#499-533) has external calls inside a loop: delta = IUniswapV4PoolManager(manager).swap(key,IUniswapV4PoolManager.SwapParams({zeroForOne:zeroForOne,amountSpecified:- int256(amount),sqrtPriceLimitX96:V4_MAX_PRICE_LIMIT}),) (src/SwarmExit.sol#516-525)\n[low/medium] calls-loop at src/SwarmExit.sol:565: SwarmExit._tryBalanceOf(address,address) (src/SwarmExit.sol#565-569) has external calls inside a loop: (success,ret) = token.staticcall(abi.encodeCall(IERC20.balanceOf,(account))) (src/SwarmExit.sol#566)\n[low/medium] calls-loop at src/SwarmExit.sol:535: SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#535-548) has external calls inside a loop: (success,reason) = manager.call(abi.encodeCall(IUniswapV4PoolManager.unlock,(abi.encode(true,data,amountIn)))) (src/SwarmExit.sol#540-541)\n[low/medium] calls-loop at src/SwarmExit.sol:618: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#618-636) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#623-627)\n[low/medium] timestamp at src/SwarmExit.sol:347: SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#347-376) uses timestamp for comparisons\n[low/medium] timestamp at src/SwarmExit.sol:299: SwarmExit.ethUsdPrice() (src/SwarmExit.sol#299-305) uses timestamp for comparisons","passed":false}],"detail":"static analysis found arbitrary-send-erc20 at src/SwarmExit.sol:380: SwarmExit.executeSale(address,address,uint256,uint256) (src/SwarmExit.sol#380-415) uses arbitrary from in transferFrom: erc.safeTransferFrom(user,address(this),amount) (src/SwarmExit.sol#393)","evaluation":"checks","profile":"foundry","status":"rejected","submissionHash":"634bf29025e47f67fb71a3e6abb92aceb74da5af7d59b931d3c6d3f92e8de466","verifiedTreeHash":"8a2e35d4a808bf78dcaaf0fa72eae158aa7cc0e3","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":4619,"exitCode":0,"name":"build","output":"Compiling 42 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.51s\nCompiler run successful!\n","passed":true},{"durationMs":218,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 36 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 439217, ~: 480038)\nLogs:\n  Bound result 99999000000000000004494\n  Bound result 1061922874\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 847350, ~: 847505)\nLogs:\n  Bound result 99999000000000000004494\n  Bound result 9999000000001061922875\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1032129)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47417)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 10685064)\n[PASS] test_ethUsdPriceChecks() (gas: 126571)\n[PASS] test_executeSaleOnlySelf() (gas: 32750)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 508551)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1103705)\n[PASS] test_inputValidation() (gas: 736668)\n[PASS] test_missingApprovalIsSkippedAndAloneRevertsNothingSold() (gas: 420598)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 453394)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 479119)\n[PASS] test_nothingSoldReverts() (gas: 486557)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203322)\n[PASS] test_pauseBlocksSelling() (gas: 723451)\n[PASS] test_previewAllCoversRegistry() (gas: 466181)\n[PASS] test_previewBridgeRouteMissing() (gas: 408995)\n[PASS] test_previewStatuses() (gas: 797669)\n[PASS] test_pullIsCappedByAllowanceNotBalance() (gas: 469840)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28693)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523310)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190689)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97881)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1938588)\n[PASS] test_registryAddRemove() (gas: 304662)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 204488)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16291967)\n[PASS] test_sellAllEmitsEvents() (gas: 429100)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1287757)\n[PASS] test_setImdRouteValidation() (gas: 201698)\n[PASS] test_setTokenRouteValidation() (gas: 321092)\n[PASS] test_slippageAtExecutionRefundsAndRollsBackFirstLeg() (gas: 926407)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 622818)\n[PASS] test_twoStepOwnership() (gas: 103168)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 614829)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 91.74ms (207.67ms CPU time)\n\nRan 27 tests for test/SwarmExitRevision.t.sol:SwarmExitRevisionTest\n[PASS] testFuzz_v4SaleConservesImdAndLeavesNothingBehind(uint96,uint96) (runs: 256, μ: 1456837, ~: 1456985)\nLogs:\n  Bound result 9999000000000000007717\n  Bound result 99999000000000000003857\n\n[PASS] test_bridgeMismatchIsNotSellable() (gas: 480751)\n[PASS] test_contractFloorIsEnforcedOnChain() (gas: 523549)\n[PASS] test_feeOnTransferTokenPaysNoFeeForAPredictableFailure() (gas: 367951)\n[PASS] test_feeOnTransferTokenSellsThroughV4WithMeasuredAmount() (gas: 2823948)\n[PASS] test_honeypotIsStillRefundedOnChain() (gas: 609999)\n[PASS] test_oneWeiBalanceSellsNothingBelowTheFloor() (gas: 209052)\n[PASS] test_priceImpactBoundInPreview() (gas: 2076718)\n[PASS] test_pullThatRevertsIsASaleFailureNotABatchRevert() (gas: 375346)\n[PASS] test_quoteImdInBridgeReversesMultiHopV4Route() (gas: 1393285)\n[PASS] test_quoteImdInBridgeReversesV4Route() (gas: 234078)\n[PASS] test_refundRefusedByTokenIsHeldAndClaimable() (gas: 965308)\n[PASS] test_registryRefusesAddressesWithoutCode() (gas: 37961)\n[PASS] test_rescueEthAndStrayTokens() (gas: 750877)\n[PASS] test_revertingBalanceDoesNotBrickPreviewAll() (gas: 3119636)\n[PASS] test_routeThatPaysNothingIsRefundedNotSettled() (gas: 1114018)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16291923)\n[PASS] test_unlockCallbackOnlyFromTheManagerInFlight() (gas: 64131)\n[PASS] test_unregisterClearsTheRoute() (gas: 232941)\n[PASS] test_v2ToWethThenV4NativeImdRoute() (gas: 1129358)\n[PASS] test_v4DirectRouteQuoteMatchesExecution() (gas: 1049314)\n[PASS] test_v4NativeLegSettlesFirstAndSellsEverything() (gas: 1565215)\n[PASS] test_v4PartialFillFailsTheSaleAndRefunds() (gas: 779878)\n[PASS] test_v4QuoteFailsOnMissingPool() (gas: 211225)\n[PASS] test_v4RouteValidation() (gas: 209322)\n[PASS] test_v4ToNativeThenV4NativeImdRoute() (gas: 1189474)\n[PASS] test_v4ToNativeThenWrapIntoV3ImdRoute() (gas: 1276015)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 103.94ms (209.94ms CPU time)\n\nRan 2 test suites in 104.95ms (195.68ms CPU time): 63 tests passed, 0 failed, 0 skipped (63 total tests)\n","passed":true},{"durationMs":67,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.claimRefund(address)\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.receive()\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMaxImpactBps(uint256)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unlockCallback(bytes)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":86,\"foundry.toml\":20,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":851,\"src/interfaces/IDexVenues.sol\":89,\"test/SwarmExit.t.sol\":751,\"test/SwarmExitRevision.t.sol\":666,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":336,\"web/app.js\":318,\"web/config.js\":13,\"web/index.html\":96,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2203,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/SwarmExit.sol:822: SwarmExit._send(address,address,uint256) (src/SwarmExit.sol#822-829) sends eth to arbitrary user\n[high/medium] reentrancy-balance at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:371: Reentrancy in SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#371-399):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[medium/high] incorrect-equality at src/SwarmExit.sol:839: SwarmExit._callToken(address,bytes) (src/SwarmExit.sol#839-850) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:371: SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#371-399) uses a dangerous strict equality:\n[medium/medium] unused-return at src/SwarmExit.sol:323: SwarmExit.ethUsdPrice() (src/SwarmExit.sol#323-329) ignores return value by (None,answer,None,updatedAt,None) = ethUsdFeed.latestRoundData() (src/SwarmExit.sol#325)\n[medium/medium] unused-return at src/SwarmExit.sol:556: SwarmExit._swapV4(SwarmExit.Route,uint256) (src/SwarmExit.sol#556-561) ignores return value by IUniswapV4PoolManager(route.router).unlock(abi.encode(false,currencyIn,hops,amountIn)) (src/SwarmExit.sol#559)\n[medium/medium] unused-return at src/SwarmExit.sol:532: SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#532-554) ignores return value by IUniswapV3SwapRouter(route.router).exactInput(IUniswapV3SwapRouter.ExactInputParams({path:route.data,recipient:address(this),amountIn:amountIn,amountOutMinimum:0})) (src/SwarmExit.sol#544-549)\n[medium/medium] unused-return at src/SwarmExit.sol:683: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#683-703) ignores return value by (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#696-702)\n[low/medium] calls-loop at src/SwarmExit.sol:683: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#683-703) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#696-702)\n[low/medium] calls-loop at src/SwarmExit.sol:470: SwarmExit._sellOne(address,uint256,uint256) (src/SwarmExit.sol#470-512) has external calls inside a loop: leftover = IERC20(token).balanceOf(address(this)) - before (src/SwarmExit.sol#507)\n[low/medium] calls-loop at src/SwarmExit.sol:832: SwarmExit._staticUint(address,bytes) (src/SwarmExit.sol#832-836) has external calls inside a loop: (success,ret) = target.staticcall(data) (src/SwarmExit.sol#833)\n[low/medium] calls-loop at src/SwarmExit.sol:707: SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#707-719) has external calls inside a loop: (success,reason) = manager.call(abi.encodeCall(IUniswapV4PoolManager.unlock,(abi.encode(true,currencyIn,hops,amountIn)))) (src/SwarmExit.sol#710-711)\n[low/medium] calls-loop at src/SwarmExit.sol:470: SwarmExit._sellOne(address,uint256,uint256) (src/SwarmExit.sol#470-512) has external calls inside a loop: imdOut = this.executeSale(token,received,minOut,deadline) (src/SwarmExit.sol#501-511)\n[low/medium] calls-loop at src/SwarmExit.sol:565: SwarmExit._swapV4Hops(address,address,SwarmExit.V4Hop[],uint256) (src/SwarmExit.sol#565-600) has external calls inside a loop: delta = IUniswapV4PoolManager(manager).swap(key,IUniswapV4PoolManager.SwapParams({zeroForOne:zeroForOne,amountSpecified:- int256(amount),sqrtPriceLimitX96:V4_MAX_PRICE_LIMIT}),) (src/SwarmExit.sol#582-591)\n[low/medium] calls-loop at src/SwarmExit.sol:839: SwarmExit._callToken(address,bytes) (src/SwarmExit.sol#839-850) has external calls inside a loop: (success,ret) = token.call(data) (src/SwarmExit.sol#844)\n[low/medium] calls-loop at src/SwarmExit.sol:832: SwarmExit._staticUint(address,bytes) (src/SwarmExit.sol#832-836) has external calls inside a loop: (success,ret) = target.staticcall(data) (src/SwarmExit.sol#833)\n[low/medium] calls-loop at src/SwarmExit.sol:470: SwarmExit._sellOne(address,uint256,uint256) (src/SwarmExit.sol#470-512) has external calls inside a loop: received = IERC20(token).balanceOf(address(this)) - before (src/SwarmExit.sol#497)\n[low/medium] calls-loop at src/SwarmExit.sol:683: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#683-703) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#687-691)\n[low/medium] calls-loop at src/SwarmExit.sol:707: SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#707-719) has external calls inside a loop: (success,reason) = manager.call(abi.encodeCall(IUniswapV4PoolManager.unlock,(abi.encode(true,currencyIn,hops,amountIn)))) (src/SwarmExit.sol#710-711)\n[low/medium] calls-loop at src/SwarmExit.sol:683: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#683-703) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#687-691)\n[low/medium] calls-loop at src/SwarmExit.sol:832: SwarmExit._staticUint(address,bytes) (src/SwarmExit.sol#832-836) has external calls inside a loop: (success,ret) = target.staticcall(data) (src/SwarmExit.sol#833)\n[low/medium] calls-loop at src/SwarmExit.sol:683: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#683-703) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#696-702)\n[low/medium] calls-loop at src/SwarmExit.sol:470: SwarmExit._sellOne(address,uint256,uint256) (src/SwarmExit.sol#470-512) has external calls inside a loop: before = IERC20(token).balanceOf(address(this)) (src/SwarmExit.sol#487)\n[low/medium] calls-loop at src/SwarmExit.sol:707: SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#707-719) has external calls inside a loop: (success,reason) = manager.call(abi.encodeCall(IUniswapV4PoolManager.unlock,(abi.encode(true,currencyIn,hops,amountIn)))) (src/SwarmExit.sol#710-711)\n[low/medium] calls-loop at src/SwarmExit.sol:683: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#683-703) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#696-702)\n[low/medium] calls-loop at src/SwarmExit.sol:683: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#683-703) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#687-691)\n[low/medium] calls-loop at src/SwarmExit.sol:565: SwarmExit._swapV4Hops(address,address,SwarmExit.V4Hop[],uint256) (src/SwarmExit.sol#565-600) has external calls inside a loop: delta = IUniswapV4PoolManager(manager).swap(key,IUniswapV4PoolManager.SwapParams({zeroForOne:zeroForOne,amountSpecified:- int256(amount),sqrtPriceLimitX96:V4_MIN_PRICE_LIMIT}),) (src/SwarmExit.sol#582-591)\n[low/medium] reentrancy-benign at src/SwarmExit.sol:622: Reentrancy in SwarmExit._previewOne(address,address) (src/SwarmExit.sol#622-654):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:556: Reentrancy in SwarmExit._swapV4(SwarmExit.Route,uint256) (src/SwarmExit.sol#556-561):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:522: Reentrancy in SwarmExit._refund(address,uint256) (src/SwarmExit.sol#522-529):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:707: Reentrancy in SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#707-719):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:657: Reentrancy in SwarmExit._quoteToImd(SwarmExit.Route,uint256) (src/SwarmExit.sol#657-670):\n[low/medium] reentrancy-events at src/SwarmExit.sol:294: Reentrancy in SwarmExit.rescue(address,address,uint256) (src/SwarmExit.sol#294-300):\n[low/medium] timestamp at src/SwarmExit.sol:371: SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#371-399) uses timestamp for comparisons\n[low/medium] timestamp at src/SwarmExit.sol:323: SwarmExit.ethUsdPrice() (src/SwarmExit.sol#323-329) uses timestamp for comparisons","passed":true},{"durationMs":563,"exitCode":0,"name":"aderyn","output":"[high] storage-array-memory-edit at src/SwarmExit.sol:356: Storage Array Edited with Memory\n[low] centralization-risk at src/SwarmExit.sol:37: Centralization Risk (10 places)\n[low] costly-loop at src/SwarmExit.sol:218: Costly operations inside loop (4 places)\n[low] large-numeric-literal at src/SwarmExit.sol:54: Large Numeric Literal\n[low] literal-instead-of-constant at src/SwarmExit.sol:745: Literal Instead of Constant (14 places)\n[low] require-revert-in-loop at src/SwarmExit.sol:383: Loop Contains `require`/`revert` (5 places)\n[low] state-change-without-event at src/SwarmExit.sol:336: State Change Without Event (4 places)\n[low] state-no-address-check at src/SwarmExit.sol:558: Address State Variable Set Without Checks (2 places)\n[low] unchecked-return at src/SwarmExit.sol:421: Unchecked Return (4 places)\n[low] uninitialized-local-variable at src/SwarmExit.sol:386: Uninitialized Local Variable (5 places)\n[low] unsafe-erc20-operation at src/SwarmExit.sol:490: Unsafe ERC20 Operation (2 places)","passed":true},{"durationMs":3620,"exitCode":0,"name":"proof 59ff49b74242","output":"Compiling 39 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.84s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-13bc0e02/Proof_59ff49b74242.t.sol:FotV4Test\n[PASS] test_feeOnTransferTokenOnV4RouteIsSellableInPreviewButAlwaysFailsAndBurnsTwoFees() (gas: 1369713)\nLogs:\n  feeTok status: 0\n  feeTok balance after: 0\n  tokenV status: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.45ms (1.09ms CPU time)\n\nRan 1 test suite in 6.31ms (3.45ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"6c565e8becc6abd4a1fdc822db77f09db632a5f3e753ff86eacdf36b46aaa37c","verifiedTreeHash":"036ae8c63f27f4fa6d27c6d8431657214d241d13","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":7874,"exitCode":0,"name":"build","output":"Compiling 46 files with Solc 0.8.26\nSolc 0.8.26 finished in 7.75s\nCompiler run successful!\n","passed":true},{"durationMs":8591,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 25 tests for test/SwarmExitRevision.t.sol:SwarmExitRevisionTest\n[PASS] testFuzz_v4SaleConservesImdAndLeavesNothingBehind(uint96,uint96) (runs: 256, μ: 1456758, ~: 1456905)\nLogs:\n  Bound result 9999000000000977744859\n  Bound result 99999000000000000024250\n\n[PASS] test_bridgeMismatchIsNotSellable() (gas: 480773)\n[PASS] test_contractFloorIsEnforcedOnChain() (gas: 523549)\n[PASS] test_feeOnTransferTokenPaysNoFeeForAPredictableFailure() (gas: 367951)\n[PASS] test_honeypotIsStillRefundedOnChain() (gas: 610024)\n[PASS] test_oneWeiBalanceSellsNothingBelowTheFloor() (gas: 209052)\n[PASS] test_priceImpactBoundInPreview() (gas: 2076705)\n[PASS] test_pullThatRevertsIsASaleFailureNotABatchRevert() (gas: 375257)\n[PASS] test_quoteImdInBridgeReversesMultiHopV4Route() (gas: 1393259)\n[PASS] test_quoteImdInBridgeReversesV4Route() (gas: 234059)\n[PASS] test_refundRefusedByTokenIsHeldAndClaimable() (gas: 965308)\n[PASS] test_registryRefusesAddressesWithoutCode() (gas: 37983)\n[PASS] test_rescueEthAndStrayTokens() (gas: 750877)\n[PASS] test_revertingBalanceDoesNotBrickPreviewAll() (gas: 3119672)\n[PASS] test_routeThatPaysNothingIsRefundedNotSettled() (gas: 1113981)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16249234)\n[PASS] test_unlockCallbackOnlyFromTheManagerInFlight() (gas: 64153)\n[PASS] test_unregisterClearsTheRoute() (gas: 232941)\n[PASS] test_v2ToWethThenV4NativeImdRoute() (gas: 1129335)\n[PASS] test_v4DirectRouteQuoteMatchesExecution() (gas: 1049370)\n[PASS] test_v4PartialFillFailsTheSaleAndRefunds() (gas: 722189)\n[PASS] test_v4QuoteFailsOnMissingPool() (gas: 211187)\n[PASS] test_v4RouteValidation() (gas: 209300)\n[PASS] test_v4ToNativeThenV4NativeImdRoute() (gas: 1189350)\n[PASS] test_v4ToNativeThenWrapIntoV3ImdRoute() (gas: 1275896)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 192.83ms (289.32ms CPU time)\n\nRan 36 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 440510, ~: 480038)\nLogs:\n  Bound result 10964816929523717477494\n  Bound result 32689294845249262496316\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 847350, ~: 847505)\nLogs:\n  Bound result 99999000000000000012069\n  Bound result 9999000000000000000019\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1032129)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47417)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 10661727)\n[PASS] test_ethUsdPriceChecks() (gas: 126571)\n[PASS] test_executeSaleOnlySelf() (gas: 32750)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 508551)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1103705)\n[PASS] test_inputValidation() (gas: 736668)\n[PASS] test_missingApprovalIsSkippedAndAloneRevertsNothingSold() (gas: 420598)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 453394)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 479119)\n[PASS] test_nothingSoldReverts() (gas: 486557)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203322)\n[PASS] test_pauseBlocksSelling() (gas: 723451)\n[PASS] test_previewAllCoversRegistry() (gas: 466181)\n[PASS] test_previewBridgeRouteMissing() (gas: 408995)\n[PASS] test_previewStatuses() (gas: 797669)\n[PASS] test_pullIsCappedByAllowanceNotBalance() (gas: 469840)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28693)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523310)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190689)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97881)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1938588)\n[PASS] test_registryAddRemove() (gas: 304662)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 204488)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16249278)\n[PASS] test_sellAllEmitsEvents() (gas: 429100)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1287757)\n[PASS] test_setImdRouteValidation() (gas: 201698)\n[PASS] test_setTokenRouteValidation() (gas: 321092)\n[PASS] test_slippageAtExecutionRefundsAndRollsBackFirstLeg() (gas: 926407)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 622818)\n[PASS] test_twoStepOwnership() (gas: 103168)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 614829)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 206.99ms (426.94ms CPU time)\n\nRan 53 tests for test/SwarmExitEdges.t.sol:SwarmExitEdgesTest\n[PASS] testFuzz_anyOrderAndSubsetConservesImd(uint8,uint8) (runs: 512, μ: 1096091, ~: 1032771)\nLogs:\n  Bound result 54\n\n[PASS] testFuzz_honeypotRefundIsExactForAnyAmount(uint128) (runs: 512, μ: 693744, ~: 694081)\nLogs:\n  Bound result 340282366920938463463364608347719846558\n\n[PASS] testFuzz_taxRoundsDownAndNeverExceedsTwoPercent(uint96) (runs: 512, μ: 520120, ~: 521856)\nLogs:\n  Bound result 1123601054\n\n[PASS] testFuzz_v2ReverseMatchesHandReversedPath(uint8,uint96) (runs: 512, μ: 1544133, ~: 1418049)\nLogs:\n  Bound result 3\n  Bound result 2447209287\n\n[PASS] testFuzz_v3PathLengthRule(uint16) (runs: 512, μ: 61707, ~: 54172)\nLogs:\n  Bound result 33\n\n[PASS] testFuzz_v3ReverseMatchesHandReversedPath(uint8,uint96,uint24,uint24,uint24) (runs: 512, μ: 1465473, ~: 1402959)\nLogs:\n  Bound result 1\n  Bound result 1810776432898899953\n\n[PASS] test_acceptOwnershipByStrangerReverts() (gas: 101378)\n[PASS] test_allowanceAboveBalanceSellsTheBalanceOnly() (gas: 485307)\n[PASS] test_approveBlockingTokenNeverCostsTheUser() (gas: 1889388)\n[PASS] test_bridgeTokenRegisteredAsSwarmTokenSellsDirectly() (gas: 738967)\n[PASS] test_deadlineExactlyNowIsAccepted() (gas: 328599)\n[PASS] test_ethUsdPriceBoundaryAndBadFeeds() (gas: 370468)\n[PASS] test_feeOnTransferRefundReturnsEverythingTheExitHolds() (gas: 905431)\n[PASS] test_gasBurningTokenNeverCostsTheUser() (gas: 4229743)\n[PASS] test_heldRefundIsPerUserAndStrangersCannotClaimIt() (gas: 2037494)\n[PASS] test_heldRefundPlusStrayBalanceRescueIsBoundedToTheStray() (gas: 2258226)\n[PASS] test_heldRefundsAccumulateAcrossTwoSalesAndClaimOnce() (gas: 2556532)\n[PASS] test_imdRegisteredAsSwarmTokenIsRefundedNotLost() (gas: 1075872)\n[PASS] test_laterTokenInBatchGetsLessThanIsolatedQuote() (gas: 1573943)\n[PASS] test_maximumTokensPerSaleAllSell() (gas: 71705093)\n[PASS] test_minImdOutExactlyEqualToOutputSettles() (gas: 405971)\n[PASS] test_multiHopV2RouteDirectToImdNeedsNoBridgeRoute() (gas: 856082)\n[PASS] test_multiHopV3RouteToImdSells() (gas: 583754)\n[PASS] test_oneWeiBalanceSellsWithZeroTaxWhenFloorDisabled() (gas: 415182)\n[PASS] test_overReportingBalanceIsASaleFailureNotABatchRevert() (gas: 1830344)\n[PASS] test_partialApprovalOneWeiShortSellsExactlyTheAllowance() (gas: 736243)\n[PASS] test_pauseDoesNotBlockConfigurationOrPreview() (gas: 650685)\n[PASS] test_pendingOwnerCannotConfigureBeforeAccepting() (gas: 82682)\n[PASS] test_previewDoesNotMoveFunds() (gas: 625441)\n[PASS] test_previewEmptyListAndEmptyRegistry() (gas: 60268)\n[PASS] test_previewQuoteFailedWhenBridgePoolIsBroken() (gas: 296521)\n[PASS] test_previewReportsBelowMinimumAtExactThreshold() (gas: 319559)\n[PASS] test_pullReturningFalseIsASaleFailureNotASettledSale() (gas: 1753848)\n[PASS] test_quoteImdInBridgeZeroAmount() (gas: 62593)\n[PASS] test_refundBlockedTokenNeverCostsTheUser() (gas: 1787980)\n[PASS] test_refundReturningFalseIsHeldNotLost() (gas: 2129001)\n[PASS] test_registryRemoveFirstMiddleLastAndMissing() (gas: 581991)\n[PASS] test_repeatedSalesAccumulateTreasuryExactly() (gas: 615544)\n[PASS] test_rescueBridgeAndImdAllowedAndBounded() (gas: 295478)\n[PASS] test_rescueEthToARecipientThatRejectsItReverts() (gas: 79022)\n[PASS] test_rescueSwarmTokenOnlyAfterUnregistering() (gas: 234810)\n[PASS] test_routeEndingInAnotherSwarmTokenIsSkippedBeforeThePull() (gas: 1765320)\n[PASS] test_sellTwiceSecondCallHasNothingToSell() (gas: 371138)\n[PASS] test_setImdRouteZeroRouterOrQuoterReverts() (gas: 101136)\n[PASS] test_setTokenRouteOnUnregisteredAfterRemovalReverts() (gas: 120657)\n[PASS] test_setTokenRouteV3QuoterIsStored() (gas: 125371)\n[PASS] test_strayBalanceRefundIsExactWhenSaleFails() (gas: 624554)\n[PASS] test_strayBalancesAreNeitherSoldNorCountedAsProceeds() (gas: 773259)\n[PASS] test_tokenBurningEverythingOnTransferIsReportedZeroBalance() (gas: 1648987)\n[PASS] test_transferOwnershipToZeroOnlyCancelsPending() (gas: 130511)\n[PASS] test_unpauseAndRescueAndAcceptAreOwnerGated() (gas: 162772)\n[PASS] test_unregisteredTokenCannotBeSoldEvenWithRouteLeft() (gas: 211971)\n[PASS] test_v3PathLengthBoundaries() (gas: 445037)\nSuite result: ok. 53 passed; 0 failed; 0 skipped; finished in 226.49ms (1.28s CPU time)\n\nRan 1 test for test/SwarmExitInvariant.t.sol:SwarmExitInvariantTest\n[PASS]\nSwarmExitInvariantTest invariants:\n[PASS] invariant_constantsFixed\n[PASS] invariant_exitHoldsOnlyStrayBalancesAndHeldRefunds\n[PASS] invariant_heldRefundsAreBackedAndAccounted\n[PASS] invariant_noAllowanceLeftBehind\n[PASS] invariant_ownerCannotRescueHeldRefunds\n[PASS] invariant_registryConsistent\n[PASS] invariant_treasuryEqualsTaxSum\n[PASS] invariant_usersHoldNetProceeds\n SwarmExitInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭------------------+-------------------------+-------+---------+----------╮\n| Contract         | Selector                | Calls | Reverts | Discards |\n+=========================================================================+\n| SwarmExitHandler | addLiquidity            | 337   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | blockPull               | 343   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | blockRefund             | 362   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | blockVenue              | 349   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | breakBridge             | 345   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | breakPool               | 322   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | claim                   | 330   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | clearOrRestoreRoute     | 324   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | mint                    | 326   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | sell                    | 347   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | setMinImdOut            | 324   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | strayDeposit            | 349   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | togglePause             | 345   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | unregisterAndReregister | 341   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | warp                    | 376   | 0       | 0        |\n╰------------------+-------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 117\n  Bound result 18857\n  Bound result 22754\n  Bound result 10001\n  Bound result 99999000000000000000051\n  Bound result 999000000000000011167\n  Bound result 1\n  Bound result 99999000000000000007913\n  Bound result 999000000000000003002\n  Bound result 50\n  Bound result 4004\n  Bound result 287113103799989513041\n  Bound result 13362\n  Bound result 17\n  Bound result 99999000000000000010608\n  Bound result 999000000000000001251\n  Bound result 24485\n  Bound result 1\n  Bound result 11379\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.45s (8.44s CPU time)\n\nRan 4 test suites in 8.45s (9.07s CPU time): 115 tests passed, 0 failed, 0 skipped (115 total tests)\n","passed":true},{"durationMs":100,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.claimRefund(address)\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.receive()\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMaxImpactBps(uint256)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unlockCallback(bytes)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":86,\"foundry.toml\":20,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":844,\"src/interfaces/IDexVenues.sol\":89,\"test/SwarmExit.t.sol\":751,\"test/SwarmExitEdges.t.sol\":1048,\"test/SwarmExitFixture.sol\":157,\"test/SwarmExitInvariant.t.sol\":501,\"test/SwarmExitRevision.t.sol\":612,\"test/mocks/EdgeMocks.sol\":144,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":336,\"web/app.js\":318,\"web/config.js\":13,\"web/index.html\":96,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9c0bdd2e9a491964c4c26d6744ed6b6657cc30b8876b1fd704af200b08096140","verifiedTreeHash":"494d217c634487ab1d691d3434504c138e5dad58","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":1660,"exitCode":0,"name":"build","output":"Compiling 41 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.59s\nCompiler run successful!\n","passed":true},{"durationMs":114,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 34 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 463171, ~: 456042)\nLogs:\n  Bound result 10964816929523717477494\n  Bound result 32689294845249262496316\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 809129, ~: 809288)\nLogs:\n  Bound result 99999000000003685830066\n  Bound result 9999000000000000020878\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1028445)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47594)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 7291684)\n[PASS] test_ethUsdPriceChecks() (gas: 126413)\n[PASS] test_executeSaleOnlySelf() (gas: 32618)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 482091)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1038562)\n[PASS] test_inputValidation() (gas: 703621)\n[PASS] test_missingApprovalRevertsWholeCall() (gas: 63261)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 581735)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 463921)\n[PASS] test_nothingSoldReverts() (gas: 622471)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203117)\n[PASS] test_pauseBlocksSelling() (gas: 675528)\n[PASS] test_previewAllCoversRegistry() (gas: 375633)\n[PASS] test_previewBridgeRouteMissing() (gas: 405742)\n[PASS] test_previewStatuses() (gas: 708547)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28649)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523036)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190601)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97755)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1909547)\n[PASS] test_registryAddRemove() (gas: 265511)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 201024)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 10457717)\n[PASS] test_sellAllEmitsEvents() (gas: 405094)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1205777)\n[PASS] test_setImdRouteValidation() (gas: 201654)\n[PASS] test_setTokenRouteValidation() (gas: 320793)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 847245)\n[PASS] test_twoStepOwnership() (gas: 103211)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 586364)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 37.64ms (100.57ms CPU time)\n\nRan 1 test suite in 38.54ms (37.64ms CPU time): 34 tests passed, 0 failed, 0 skipped (34 total tests)\n","passed":true},{"durationMs":37,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":78,\"foundry.toml\":20,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":540,\"src/interfaces/IDexVenues.sol\":49,\"test/SwarmExit.t.sol\":711,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":178,\"web/app.js\":222,\"web/config.js\":13,\"web/index.html\":95,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":932,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/SwarmExit.sol:294: Reentrancy in SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#294-323):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:326: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#326-345):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:326: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#326-345):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:326: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#326-345):\n[medium/high] incorrect-equality at src/SwarmExit.sol:294: SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#294-323) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:351: SwarmExit._sellOne(address,address,uint256,uint256) (src/SwarmExit.sol#351-391) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:457: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#457-474) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:351: SwarmExit._sellOne(address,address,uint256,uint256) (src/SwarmExit.sol#351-391) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:415: SwarmExit._previewOne(address,address) (src/SwarmExit.sol#415-455) uses a dangerous strict equality:\n[medium/medium] unused-return at src/SwarmExit.sol:394: SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#394-413) ignores return value by IUniswapV3SwapRouter(route.router).exactInput(IUniswapV3SwapRouter.ExactInputParams({path:route.data,recipient:address(this),amountIn:amountIn,amountOutMinimum:0})) (src/SwarmExit.sol#404-409)\n[medium/medium] unused-return at src/SwarmExit.sol:457: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#457-474) ignores return value by (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#467-473)\n[medium/medium] unused-return at src/SwarmExit.sol:248: SwarmExit.ethUsdPrice() (src/SwarmExit.sol#248-254) ignores return value by (None,answer,None,updatedAt,None) = ethUsdFeed.latestRoundData() (src/SwarmExit.sol#250)\n[low/medium] calls-loop at src/SwarmExit.sol:351: SwarmExit._sellOne(address,address,uint256,uint256) (src/SwarmExit.sol#351-391) has external calls inside a loop: balance = erc.balanceOf(user) (src/SwarmExit.sol#362)\n[low/medium] calls-loop at src/SwarmExit.sol:351: SwarmExit._sellOne(address,address,uint256,uint256) (src/SwarmExit.sol#351-391) has external calls inside a loop: leftover = erc.balanceOf(address(this)) - before (src/SwarmExit.sol#386)\n[low/medium] calls-loop at src/SwarmExit.sol:457: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#457-474) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#467-473)\n[low/medium] calls-loop at src/SwarmExit.sol:351: SwarmExit._sellOne(address,address,uint256,uint256) (src/SwarmExit.sol#351-391) has external calls inside a loop: received = erc.balanceOf(address(this)) - before (src/SwarmExit.sol#369)\n[low/medium] calls-loop at src/SwarmExit.sol:351: SwarmExit._sellOne(address,address,uint256,uint256) (src/SwarmExit.sol#351-391) has external calls inside a loop: before = erc.balanceOf(address(this)) (src/SwarmExit.sol#367)\n[low/medium] calls-loop at src/SwarmExit.sol:457: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#457-474) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#461-465)\n[low/medium] calls-loop at src/SwarmExit.sol:415: SwarmExit._previewOne(address,address) (src/SwarmExit.sol#415-455) has external calls inside a loop: p.balance = IERC20(token).balanceOf(user) (src/SwarmExit.sol#421)\n[low/medium] calls-loop at src/SwarmExit.sol:457: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#457-474) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#461-465)\n[low/medium] calls-loop at src/SwarmExit.sol:351: SwarmExit._sellOne(address,address,uint256,uint256) (src/SwarmExit.sol#351-391) has external calls inside a loop: imdOut = this.executeSale(token,received,minOut,deadline) (src/SwarmExit.sol#376-390)\n[low/medium] calls-loop at src/SwarmExit.sol:415: SwarmExit._previewOne(address,address) (src/SwarmExit.sol#415-455) has external calls inside a loop: p.balance = IERC20(token).balanceOf(user) (src/SwarmExit.sol#421)\n[low/medium] calls-loop at src/SwarmExit.sol:457: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#457-474) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#467-473)\n[low/medium] timestamp at src/SwarmExit.sol:248: SwarmExit.ethUsdPrice() (src/SwarmExit.sol#248-254) uses timestamp for comparisons\n[low/medium] timestamp at src/SwarmExit.sol:294: SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#294-323) uses timestamp for comparisons","passed":true},{"durationMs":283,"exitCode":0,"name":"aderyn","output":"[high] storage-array-memory-edit at src/SwarmExit.sol:281: Storage Array Edited with Memory\n[low] centralization-risk at src/SwarmExit.sol:24: Centralization Risk (9 places)\n[low] costly-loop at src/SwarmExit.sol:156: Costly operations inside loop\n[low] large-numeric-literal at src/SwarmExit.sol:37: Large Numeric Literal\n[low] literal-instead-of-constant at src/SwarmExit.sol:496: Literal Instead of Constant (12 places)\n[low] require-revert-in-loop at src/SwarmExit.sol:307: Loop Contains `require`/`revert` (3 places)\n[low] unchecked-return at src/SwarmExit.sol:334: Unchecked Return (3 places)\n[low] uninitialized-local-variable at src/SwarmExit.sol:310: Uninitialized Local Variable (3 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9da411f133da0de82c8947b4db2d7be5d6e8c52a8ebf65f7dbc1a73674b295d7","verifiedTreeHash":"3352ff6e38e597ec4e380b93dc2b3607140e0885","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":5892,"exitCode":0,"name":"build","output":"Compiling 45 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.77s\nCompiler run successful!\n","passed":true},{"durationMs":3232,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 34 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 463064, ~: 456042)\nLogs:\n  Bound result 10964816929523717477494\n  Bound result 32689294845249262496316\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 809129, ~: 809288)\nLogs:\n  Bound result 99999000000000000007982\n  Bound result 9999000000000000000652\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1028445)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47594)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 7291684)\n[PASS] test_ethUsdPriceChecks() (gas: 126413)\n[PASS] test_executeSaleOnlySelf() (gas: 32618)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 482091)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1038562)\n[PASS] test_inputValidation() (gas: 703621)\n[PASS] test_missingApprovalRevertsWholeCall() (gas: 63261)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 581735)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 463921)\n[PASS] test_nothingSoldReverts() (gas: 622471)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203117)\n[PASS] test_pauseBlocksSelling() (gas: 675528)\n[PASS] test_previewAllCoversRegistry() (gas: 375633)\n[PASS] test_previewBridgeRouteMissing() (gas: 405742)\n[PASS] test_previewStatuses() (gas: 708547)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28649)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523036)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190601)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97755)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1909547)\n[PASS] test_registryAddRemove() (gas: 265511)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 201024)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 10457717)\n[PASS] test_sellAllEmitsEvents() (gas: 405094)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1205777)\n[PASS] test_setImdRouteValidation() (gas: 201654)\n[PASS] test_setTokenRouteValidation() (gas: 320793)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 847245)\n[PASS] test_twoStepOwnership() (gas: 103211)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 586364)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 167.59ms (361.41ms CPU time)\n\nRan 46 tests for test/SwarmExitEdges.t.sol:SwarmExitEdgesTest\n[PASS] testFuzz_anyOrderAndSubsetConservesImd(uint8,uint8) (runs: 512, μ: 1107157, ~: 1018807)\nLogs:\n  Bound result 60\n\n[PASS] testFuzz_honeypotRefundIsExactForAnyAmount(uint128) (runs: 512, μ: 656057, ~: 656048)\nLogs:\n  Bound result 915951635102\n\n[PASS] testFuzz_taxRoundsDownAndNeverExceedsTwoPercent(uint96) (runs: 512, μ: 471605, ~: 472469)\nLogs:\n  Bound result 492479648086622791735\n\n[PASS] testFuzz_v2ReverseMatchesHandReversedPath(uint8,uint96) (runs: 512, μ: 1505139, ~: 1417920)\nLogs:\n  Bound result 3\n  Bound result 20178\n\n[PASS] testFuzz_v3PathLengthRule(uint16) (runs: 512, μ: 62772, ~: 56240)\nLogs:\n  Bound result 30\n\n[PASS] testFuzz_v3ReverseMatchesHandReversedPath(uint8,uint96,uint24,uint24,uint24) (runs: 512, μ: 1462880, ~: 1402707)\nLogs:\n  Bound result 3\n  Bound result 20933\n\n[PASS] test_acceptOwnershipByStrangerReverts() (gas: 101363)\n[PASS] test_approveBlockingTokenNeverCostsTheUser() (gas: 1647472)\n[PASS] test_bridgeTokenRegisteredAsSwarmTokenSellsDirectly() (gas: 707180)\n[PASS] test_deadlineExactlyNowIsAccepted() (gas: 315179)\n[PASS] test_ethUsdPriceBoundaryAndBadFeeds() (gas: 370131)\n[PASS] test_feeOnTransferRefundReturnsEverythingTheExitHolds() (gas: 613055)\n[PASS] test_gasBurningTokenNeverCostsTheUser() (gas: 4231787)\n[PASS] test_imdRegisteredAsSwarmTokenIsRefundedNotLost() (gas: 1048074)\n[PASS] test_laterTokenInBatchGetsLessThanIsolatedQuote() (gas: 1643371)\n[PASS] test_maximumTokensPerSaleAllSell() (gas: 70970293)\n[PASS] test_minImdOutExactlyEqualToOutputSettles() (gas: 382128)\n[PASS] test_multiHopV2RouteDirectToImdNeedsNoBridgeRoute() (gas: 822722)\n[PASS] test_multiHopV3RouteToImdSells() (gas: 548493)\n[PASS] test_oneWeiBalanceSellsWithZeroTax() (gas: 375993)\n[PASS] test_overReportingBalanceRevertsWholeCallNothingMoves() (gas: 1677713)\n[PASS] test_partialApprovalRevertsWholeCallNothingMoves() (gas: 427686)\n[PASS] test_pauseDoesNotBlockConfigurationOrPreview() (gas: 562866)\n[PASS] test_pendingOwnerCannotConfigureBeforeAccepting() (gas: 82573)\n[PASS] test_previewDoesNotMoveFunds() (gas: 513485)\n[PASS] test_previewEmptyListAndEmptyRegistry() (gas: 60181)\n[PASS] test_previewQuoteFailedWhenBridgePoolIsBroken() (gas: 285140)\n[PASS] test_previewReportsBelowMinimumAtExactThreshold() (gas: 297857)\n[PASS] test_quoteImdInBridgeZeroAmount() (gas: 62561)\n[PASS] test_refundBlockedTokenNeverCostsTheUser() (gas: 1949335)\n[PASS] test_registryRemoveFirstMiddleLastAndMissing() (gas: 441560)\n[PASS] test_repeatedSalesAccumulateTreasuryExactly() (gas: 588682)\n[PASS] test_rescueBridgeAndImdAllowedAndBounded() (gas: 286683)\n[PASS] test_rescueSwarmTokenOnlyAfterUnregistering() (gas: 194918)\n[PASS] test_routeEndingInAnotherSwarmTokenIsRefunded() (gas: 1749594)\n[PASS] test_sellTwiceSecondCallHasNothingToSell() (gas: 355132)\n[PASS] test_setImdRouteZeroRouterOrQuoterReverts() (gas: 100933)\n[PASS] test_setTokenRouteOnUnregisteredAfterRemovalReverts() (gas: 84185)\n[PASS] test_setTokenRouteV3QuoterIsStored() (gas: 125229)\n[PASS] test_strayBalanceRefundIsExactWhenSaleFails() (gas: 587267)\n[PASS] test_strayBalancesAreNeitherSoldNorCountedAsProceeds() (gas: 725409)\n[PASS] test_tokenBurningEverythingOnTransferIsReportedZeroBalance() (gas: 1292795)\n[PASS] test_transferOwnershipToZeroOnlyCancelsPending() (gas: 130459)\n[PASS] test_unpauseAndRescueAndAcceptAreOwnerGated() (gas: 162312)\n[PASS] test_unregisteredTokenCannotBeSoldEvenWithRouteLeft() (gas: 175435)\n[PASS] test_v3PathLengthBoundaries() (gas: 444357)\nSuite result: ok. 46 passed; 0 failed; 0 skipped; finished in 167.76ms (1.03s CPU time)\n\nRan 1 test for test/SwarmExitInvariant.t.sol:SwarmExitInvariantTest\n[PASS]\nSwarmExitInvariantTest invariants:\n[PASS] invariant_constantsFixed\n[PASS] invariant_exitHoldsOnlyStrayBalances\n[PASS] invariant_noAllowanceLeftBehind\n[PASS] invariant_registryConsistent\n[PASS] invariant_treasuryEqualsTaxSum\n[PASS] invariant_usersHoldNetProceeds\n SwarmExitInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭------------------+-------------------------+-------+---------+----------╮\n| Contract         | Selector                | Calls | Reverts | Discards |\n+=========================================================================+\n| SwarmExitHandler | addLiquidity            | 465   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | breakBridge             | 487   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | breakPool               | 436   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | clearOrRestoreRoute     | 456   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | mint                    | 437   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | sell                    | 471   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | setMinImdOut            | 431   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | strayDeposit            | 478   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | togglePause             | 514   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | unregisterAndReregister | 487   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | warp                    | 458   | 0       | 0        |\n╰------------------+-------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 99\n  Bound result 54803264337593543158043\n  Bound result 489164214810592659120\n  Bound result 2053391438\n  Bound result 60\n  Bound result 274\n  Bound result 15233412613378370\n  Bound result 14782\n  Bound result 169077995\n  Bound result 51\n  Bound result 12676\n  Bound result 500\n  Bound result 99999000000003479986924\n  Bound result 999000000000000015299\n  Bound result 60\n  Bound result 6\n  Bound result 2353\n  Bound result 54\n  Bound result 13201\n  Bound result 18537\n  Bound result 53\n  Bound result 957\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.10s (3.09s CPU time)\n\nRan 3 test suites in 3.10s (3.43s CPU time): 81 tests passed, 0 failed, 0 skipped (81 total tests)\n","passed":true},{"durationMs":80,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":78,\"foundry.toml\":20,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":540,\"src/interfaces/IDexVenues.sol\":49,\"test/SwarmExit.t.sol\":711,\"test/SwarmExitEdges.t.sol\":865,\"test/SwarmExitFixture.sol\":157,\"test/SwarmExitInvariant.t.sol\":371,\"test/mocks/EdgeMocks.sol\":117,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":178,\"web/app.js\":222,\"web/config.js\":13,\"web/index.html\":95,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a251d6872dca81e4d205ba0771cb73943361cdb843243178340f536b323a9961","verifiedTreeHash":"125f76e8a954cee08382e82d243437b2769575bd","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":9844,"exitCode":0,"name":"build","output":"Compiling 49 files with Solc 0.8.26\nSolc 0.8.26 finished in 9.72s\nCompiler run successful!\n","passed":true},{"durationMs":8202,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 27 tests for test/SwarmExitRevision.t.sol:SwarmExitRevisionTest\n[PASS] testFuzz_v4SaleConservesImdAndLeavesNothingBehind(uint96,uint96) (runs: 256, μ: 1456837, ~: 1456985)\nLogs:\n  Bound result 9999000000000000007717\n  Bound result 99999000000000000003857\n\n[PASS] test_bridgeMismatchIsNotSellable() (gas: 480751)\n[PASS] test_contractFloorIsEnforcedOnChain() (gas: 523549)\n[PASS] test_feeOnTransferTokenPaysNoFeeForAPredictableFailure() (gas: 367951)\n[PASS] test_feeOnTransferTokenSellsThroughV4WithMeasuredAmount() (gas: 2823948)\n[PASS] test_honeypotIsStillRefundedOnChain() (gas: 609999)\n[PASS] test_oneWeiBalanceSellsNothingBelowTheFloor() (gas: 209052)\n[PASS] test_priceImpactBoundInPreview() (gas: 2076718)\n[PASS] test_pullThatRevertsIsASaleFailureNotABatchRevert() (gas: 375346)\n[PASS] test_quoteImdInBridgeReversesMultiHopV4Route() (gas: 1393285)\n[PASS] test_quoteImdInBridgeReversesV4Route() (gas: 234078)\n[PASS] test_refundRefusedByTokenIsHeldAndClaimable() (gas: 965308)\n[PASS] test_registryRefusesAddressesWithoutCode() (gas: 37961)\n[PASS] test_rescueEthAndStrayTokens() (gas: 750877)\n[PASS] test_revertingBalanceDoesNotBrickPreviewAll() (gas: 3119636)\n[PASS] test_routeThatPaysNothingIsRefundedNotSettled() (gas: 1114018)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16291923)\n[PASS] test_unlockCallbackOnlyFromTheManagerInFlight() (gas: 64131)\n[PASS] test_unregisterClearsTheRoute() (gas: 232941)\n[PASS] test_v2ToWethThenV4NativeImdRoute() (gas: 1129358)\n[PASS] test_v4DirectRouteQuoteMatchesExecution() (gas: 1049314)\n[PASS] test_v4NativeLegSettlesFirstAndSellsEverything() (gas: 1565215)\n[PASS] test_v4PartialFillFailsTheSaleAndRefunds() (gas: 779878)\n[PASS] test_v4QuoteFailsOnMissingPool() (gas: 211225)\n[PASS] test_v4RouteValidation() (gas: 209322)\n[PASS] test_v4ToNativeThenV4NativeImdRoute() (gas: 1189474)\n[PASS] test_v4ToNativeThenWrapIntoV3ImdRoute() (gas: 1276015)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 215.05ms (316.86ms CPU time)\n\nRan 10 tests for test/SwarmExitV4Edges.t.sol:SwarmExitV4EdgesTest\n[PASS] testFuzz_v4FeeOnTransferAnyFeeSellsMeasuredOrRefundsExactly(uint16,uint96) (runs: 512, μ: 2359308, ~: 2205177)\nLogs:\n  Bound result 493\n  Bound result 25882532620435568232771\n\n[PASS] test_callbackGuardHoldsAfterASuccessfulV4Sale() (gas: 590422)\n[PASS] test_quoteImdInBridgeReversesNativeV4Route() (gas: 333170)\n[PASS] test_v4FeeOnTransferBridgedThroughNativeSellsMeasuredAmounts() (gas: 1826735)\n[PASS] test_v4FeeOnTransferWithPreviewMinimumIsRefundedNotSettledShort() (gas: 1682129)\n[PASS] test_v4HoneypotRefundedAndCallbackGuardRestored() (gas: 1543340)\n[PASS] test_v4MixedBatchSettlesEveryCurrencyAndConservesImd() (gas: 3274122)\n[PASS] test_v4QuoteModeMovesNothingForAnyToken() (gas: 3365243)\n[PASS] test_v4SwapFailingAfterSettleRefundsThePulledFeeToken() (gas: 1605208)\n[PASS] test_v4TokenCreditingNothingIsRefundedNotSoldForZero() (gas: 2976846)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 239.99ms (264.87ms CPU time)\n\nRan 36 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 439208, ~: 480038)\nLogs:\n  Bound result 99999000000000000004494\n  Bound result 1061922874\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 847350, ~: 847505)\nLogs:\n  Bound result 99999000000000000004494\n  Bound result 9999000000001061922875\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1032129)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47417)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 10685064)\n[PASS] test_ethUsdPriceChecks() (gas: 126571)\n[PASS] test_executeSaleOnlySelf() (gas: 32750)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 508551)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1103705)\n[PASS] test_inputValidation() (gas: 736668)\n[PASS] test_missingApprovalIsSkippedAndAloneRevertsNothingSold() (gas: 420598)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 453394)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 479119)\n[PASS] test_nothingSoldReverts() (gas: 486557)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203322)\n[PASS] test_pauseBlocksSelling() (gas: 723451)\n[PASS] test_previewAllCoversRegistry() (gas: 466181)\n[PASS] test_previewBridgeRouteMissing() (gas: 408995)\n[PASS] test_previewStatuses() (gas: 797669)\n[PASS] test_pullIsCappedByAllowanceNotBalance() (gas: 469840)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28693)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523310)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190689)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97881)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1938588)\n[PASS] test_registryAddRemove() (gas: 304662)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 204488)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16291967)\n[PASS] test_sellAllEmitsEvents() (gas: 429100)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1287757)\n[PASS] test_setImdRouteValidation() (gas: 201698)\n[PASS] test_setTokenRouteValidation() (gas: 321092)\n[PASS] test_slippageAtExecutionRefundsAndRollsBackFirstLeg() (gas: 926407)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 622818)\n[PASS] test_twoStepOwnership() (gas: 103168)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 614829)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 261.06ms (545.98ms CPU time)\n\nRan 53 tests for test/SwarmExitEdges.t.sol:SwarmExitEdgesTest\n[PASS] testFuzz_anyOrderAndSubsetConservesImd(uint8,uint8) (runs: 512, μ: 1100080, ~: 1035002)\nLogs:\n  Bound result 39\n\n[PASS] testFuzz_honeypotRefundIsExactForAnyAmount(uint128) (runs: 512, μ: 693761, ~: 694081)\nLogs:\n  Bound result 1156096046242472568558270867511\n\n[PASS] testFuzz_taxRoundsDownAndNeverExceedsTwoPercent(uint96) (runs: 512, μ: 520149, ~: 521856)\nLogs:\n  Bound result 492479648086622791735\n\n[PASS] testFuzz_v2ReverseMatchesHandReversedPath(uint8,uint96) (runs: 512, μ: 1512905, ~: 1418025)\nLogs:\n  Bound result 1\n  Bound result 5882532620435568232771\n\n[PASS] testFuzz_v3PathLengthRule(uint16) (runs: 512, μ: 63580, ~: 55784)\nLogs:\n  Bound result 33\n\n[PASS] testFuzz_v3ReverseMatchesHandReversedPath(uint8,uint96,uint24,uint24,uint24) (runs: 512, μ: 1460922, ~: 1402959)\nLogs:\n  Bound result 1\n  Bound result 240\n\n[PASS] test_acceptOwnershipByStrangerReverts() (gas: 101378)\n[PASS] test_allowanceAboveBalanceSellsTheBalanceOnly() (gas: 485307)\n[PASS] test_approveBlockingTokenNeverCostsTheUser() (gas: 1889388)\n[PASS] test_bridgeTokenRegisteredAsSwarmTokenSellsDirectly() (gas: 738967)\n[PASS] test_deadlineExactlyNowIsAccepted() (gas: 328599)\n[PASS] test_ethUsdPriceBoundaryAndBadFeeds() (gas: 370468)\n[PASS] test_feeOnTransferRefundReturnsEverythingTheExitHolds() (gas: 905431)\n[PASS] test_gasBurningTokenNeverCostsTheUser() (gas: 4229743)\n[PASS] test_heldRefundIsPerUserAndStrangersCannotClaimIt() (gas: 2037494)\n[PASS] test_heldRefundPlusStrayBalanceRescueIsBoundedToTheStray() (gas: 2258226)\n[PASS] test_heldRefundsAccumulateAcrossTwoSalesAndClaimOnce() (gas: 2556532)\n[PASS] test_imdRegisteredAsSwarmTokenIsRefundedNotLost() (gas: 1075872)\n[PASS] test_laterTokenInBatchGetsLessThanIsolatedQuote() (gas: 1573943)\n[PASS] test_maximumTokensPerSaleAllSell() (gas: 71705093)\n[PASS] test_minImdOutExactlyEqualToOutputSettles() (gas: 405971)\n[PASS] test_multiHopV2RouteDirectToImdNeedsNoBridgeRoute() (gas: 856082)\n[PASS] test_multiHopV3RouteToImdSells() (gas: 583754)\n[PASS] test_oneWeiBalanceSellsWithZeroTaxWhenFloorDisabled() (gas: 415182)\n[PASS] test_overReportingBalanceIsASaleFailureNotABatchRevert() (gas: 1830344)\n[PASS] test_partialApprovalOneWeiShortSellsExactlyTheAllowance() (gas: 736243)\n[PASS] test_pauseDoesNotBlockConfigurationOrPreview() (gas: 650685)\n[PASS] test_pendingOwnerCannotConfigureBeforeAccepting() (gas: 82682)\n[PASS] test_previewDoesNotMoveFunds() (gas: 625441)\n[PASS] test_previewEmptyListAndEmptyRegistry() (gas: 60268)\n[PASS] test_previewQuoteFailedWhenBridgePoolIsBroken() (gas: 296521)\n[PASS] test_previewReportsBelowMinimumAtExactThreshold() (gas: 319559)\n[PASS] test_pullReturningFalseIsASaleFailureNotASettledSale() (gas: 1753848)\n[PASS] test_quoteImdInBridgeZeroAmount() (gas: 62593)\n[PASS] test_refundBlockedTokenNeverCostsTheUser() (gas: 1787980)\n[PASS] test_refundReturningFalseIsHeldNotLost() (gas: 2129001)\n[PASS] test_registryRemoveFirstMiddleLastAndMissing() (gas: 581991)\n[PASS] test_repeatedSalesAccumulateTreasuryExactly() (gas: 615544)\n[PASS] test_rescueBridgeAndImdAllowedAndBounded() (gas: 295478)\n[PASS] test_rescueEthToARecipientThatRejectsItReverts() (gas: 79022)\n[PASS] test_rescueSwarmTokenOnlyAfterUnregistering() (gas: 234810)\n[PASS] test_routeEndingInAnotherSwarmTokenIsSkippedBeforeThePull() (gas: 1765320)\n[PASS] test_sellTwiceSecondCallHasNothingToSell() (gas: 371138)\n[PASS] test_setImdRouteZeroRouterOrQuoterReverts() (gas: 101136)\n[PASS] test_setTokenRouteOnUnregisteredAfterRemovalReverts() (gas: 120657)\n[PASS] test_setTokenRouteV3QuoterIsStored() (gas: 125371)\n[PASS] test_strayBalanceRefundIsExactWhenSaleFails() (gas: 624554)\n[PASS] test_strayBalancesAreNeitherSoldNorCountedAsProceeds() (gas: 773259)\n[PASS] test_tokenBurningEverythingOnTransferIsReportedZeroBalance() (gas: 1648987)\n[PASS] test_transferOwnershipToZeroOnlyCancelsPending() (gas: 130511)\n[PASS] test_unpauseAndRescueAndAcceptAreOwnerGated() (gas: 162772)\n[PASS] test_unregisteredTokenCannotBeSoldEvenWithRouteLeft() (gas: 211971)\n[PASS] test_v3PathLengthBoundaries() (gas: 445037)\nSuite result: ok. 53 passed; 0 failed; 0 skipped; finished in 261.04ms (1.50s CPU time)\n\nRan 1 test for test/SwarmExitV4Invariant.t.sol:SwarmExitV4InvariantTest\n[PASS]\nSwarmExitV4InvariantTest invariants:\n[PASS] invariant_callbackClosedBetweenTransactions\n[PASS] invariant_configurationFixed\n[PASS] invariant_exitHoldsNothingButStrayEth\n[PASS] invariant_managerSettledAndIdle\n[PASS] invariant_treasuryEqualsTaxSum\n[PASS] invariant_usersHoldNetProceeds\n SwarmExitV4InvariantTest invariants (runs: 96, calls: 3072, reverts: 0)\n\n╭--------------------+-------------------+-------+---------+----------╮\n| Contract           | Selector          | Calls | Reverts | Discards |\n+=====================================================================+\n| SwarmExitV4Handler | addV4Liquidity    | 462   | 0       | 0        |\n|--------------------+-------------------+-------+---------+----------|\n| SwarmExitV4Handler | mint              | 469   | 0       | 0        |\n|--------------------+-------------------+-------+---------+----------|\n| SwarmExitV4Handler | sell              | 433   | 0       | 0        |\n|--------------------+-------------------+-------+---------+----------|\n| SwarmExitV4Handler | setMinImdOut      | 428   | 0       | 0        |\n|--------------------+-------------------+-------+---------+----------|\n| SwarmExitV4Handler | strayEth          | 413   | 0       | 0        |\n|--------------------+-------------------+-------+---------+----------|\n| SwarmExitV4Handler | togglePartialFill | 436   | 0       | 0        |\n|--------------------+-------------------+-------+---------+----------|\n| SwarmExitV4Handler | warp              | 431   | 0       | 0        |\n╰--------------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 53\n  Bound result 42\n  Bound result 2\n  Bound result 99999000000000000022376\n  Bound result 999000000000000005857\n  Bound result 4264337593543950334\n  Bound result 99999000000011563674093\n  Bound result 999000030605330652111\n  Bound result 103\n  Bound result 1196\n  Bound result 264364\n  Bound result 731\n  Bound result 184\n  Bound result 330\n  Bound result 8799999999999999999999\n  Bound result 22797\n  Bound result 99999000000000000010614\n  Bound result 207199629094084554667\n  Bound result 77\n  Bound result 395908180881387412\n  Bound result 1656069516\n  Bound result 3262596935502715226\n  Bound result 89\n  Bound result 1119\n  Bound result 4887\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.78s (1.77s CPU time)\n\nRan 1 test for test/SwarmExitInvariant.t.sol:SwarmExitInvariantTest\n[PASS]\nSwarmExitInvariantTest invariants:\n[PASS] invariant_constantsFixed\n[PASS] invariant_exitHoldsOnlyStrayBalancesAndHeldRefunds\n[PASS] invariant_heldRefundsAreBackedAndAccounted\n[PASS] invariant_noAllowanceLeftBehind\n[PASS] invariant_ownerCannotRescueHeldRefunds\n[PASS] invariant_registryConsistent\n[PASS] invariant_treasuryEqualsTaxSum\n[PASS] invariant_usersHoldNetProceeds\n SwarmExitInvariantTest invariants (runs: 128, calls: 5120, reverts: 0)\n\n╭------------------+-------------------------+-------+---------+----------╮\n| Contract         | Selector                | Calls | Reverts | Discards |\n+=========================================================================+\n| SwarmExitHandler | addLiquidity            | 337   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | blockPull               | 343   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | blockRefund             | 362   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | blockVenue              | 349   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | breakBridge             | 345   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | breakPool               | 322   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | claim                   | 330   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | clearOrRestoreRoute     | 324   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | mint                    | 326   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | sell                    | 347   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | setMinImdOut            | 324   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | strayDeposit            | 349   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | togglePause             | 345   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | unregisterAndReregister | 341   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| SwarmExitHandler | warp                    | 376   | 0       | 0        |\n╰------------------+-------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 117\n  Bound result 15797\n  Bound result 22965\n  Bound result 9999\n  Bound result 99999000000000000002576\n  Bound result 878389283198852708281\n  Bound result 1\n  Bound result 99999000000000000007992\n  Bound result 999000000000000001575\n  Bound result 50\n  Bound result 3901\n  Bound result 287113103799989513041\n  Bound result 13488\n  Bound result 17\n  Bound result 99999000000000000010809\n  Bound result 999000000000000001339\n  Bound result 24485\n  Bound result 1\n  Bound result 170065573\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.06s (8.05s CPU time)\n\nRan 6 test suites in 8.07s (10.82s CPU time): 128 tests passed, 0 failed, 0 skipped (128 total tests)\n","passed":true},{"durationMs":86,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.claimRefund(address)\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.receive()\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMaxImpactBps(uint256)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unlockCallback(bytes)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":86,\"foundry.toml\":20,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":851,\"src/interfaces/IDexVenues.sol\":89,\"test/SwarmExit.t.sol\":751,\"test/SwarmExitEdges.t.sol\":1048,\"test/SwarmExitFixture.sol\":157,\"test/SwarmExitInvariant.t.sol\":501,\"test/SwarmExitRevision.t.sol\":666,\"test/SwarmExitV4Edges.t.sol\":302,\"test/SwarmExitV4Fixture.sol\":224,\"test/SwarmExitV4Invariant.t.sol\":332,\"test/mocks/EdgeMocks.sol\":170,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":336,\"web/app.js\":318,\"web/config.js\":13,\"web/index.html\":96,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"cd1b901a36ffef3dfb8ebd2c8c6b3a1d1bce67bb8fc6a53771938cc9c5abad0a","verifiedTreeHash":"277dec2d73aa195f1b72b0425ab393d73c1592f2","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":4431,"exitCode":0,"name":"build","output":"Compiling 42 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.33s\nCompiler run successful!\n","passed":true},{"durationMs":210,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 36 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 437278, ~: 480038)\nLogs:\n  Bound result 99999000000000000000934\n  Bound result 10195\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 847350, ~: 847505)\nLogs:\n  Bound result 99999000000000000000934\n  Bound result 9999000000000000010196\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1032129)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47417)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 10661727)\n[PASS] test_ethUsdPriceChecks() (gas: 126571)\n[PASS] test_executeSaleOnlySelf() (gas: 32750)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 508551)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1103705)\n[PASS] test_inputValidation() (gas: 736668)\n[PASS] test_missingApprovalIsSkippedAndAloneRevertsNothingSold() (gas: 420598)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 453394)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 479119)\n[PASS] test_nothingSoldReverts() (gas: 486557)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203322)\n[PASS] test_pauseBlocksSelling() (gas: 723451)\n[PASS] test_previewAllCoversRegistry() (gas: 466181)\n[PASS] test_previewBridgeRouteMissing() (gas: 408995)\n[PASS] test_previewStatuses() (gas: 797669)\n[PASS] test_pullIsCappedByAllowanceNotBalance() (gas: 469840)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28693)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523310)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190689)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97881)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1938588)\n[PASS] test_registryAddRemove() (gas: 304662)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 204488)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16249278)\n[PASS] test_sellAllEmitsEvents() (gas: 429100)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1287757)\n[PASS] test_setImdRouteValidation() (gas: 201698)\n[PASS] test_setTokenRouteValidation() (gas: 321092)\n[PASS] test_slippageAtExecutionRefundsAndRollsBackFirstLeg() (gas: 926407)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 622818)\n[PASS] test_twoStepOwnership() (gas: 103168)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 614829)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 85.82ms (222.25ms CPU time)\n\nRan 25 tests for test/SwarmExitRevision.t.sol:SwarmExitRevisionTest\n[PASS] testFuzz_v4SaleConservesImdAndLeavesNothingBehind(uint96,uint96) (runs: 256, μ: 1456758, ~: 1456905)\nLogs:\n  Bound result 9999000000000000007794\n  Bound result 99999000000000000003857\n\n[PASS] test_bridgeMismatchIsNotSellable() (gas: 480773)\n[PASS] test_contractFloorIsEnforcedOnChain() (gas: 523549)\n[PASS] test_feeOnTransferTokenPaysNoFeeForAPredictableFailure() (gas: 367951)\n[PASS] test_honeypotIsStillRefundedOnChain() (gas: 610024)\n[PASS] test_oneWeiBalanceSellsNothingBelowTheFloor() (gas: 209052)\n[PASS] test_priceImpactBoundInPreview() (gas: 2076705)\n[PASS] test_pullThatRevertsIsASaleFailureNotABatchRevert() (gas: 375257)\n[PASS] test_quoteImdInBridgeReversesMultiHopV4Route() (gas: 1393259)\n[PASS] test_quoteImdInBridgeReversesV4Route() (gas: 234059)\n[PASS] test_refundRefusedByTokenIsHeldAndClaimable() (gas: 965308)\n[PASS] test_registryRefusesAddressesWithoutCode() (gas: 37983)\n[PASS] test_rescueEthAndStrayTokens() (gas: 750877)\n[PASS] test_revertingBalanceDoesNotBrickPreviewAll() (gas: 3119672)\n[PASS] test_routeThatPaysNothingIsRefundedNotSettled() (gas: 1113981)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 16249234)\n[PASS] test_unlockCallbackOnlyFromTheManagerInFlight() (gas: 64153)\n[PASS] test_unregisterClearsTheRoute() (gas: 232941)\n[PASS] test_v2ToWethThenV4NativeImdRoute() (gas: 1129335)\n[PASS] test_v4DirectRouteQuoteMatchesExecution() (gas: 1049370)\n[PASS] test_v4PartialFillFailsTheSaleAndRefunds() (gas: 722189)\n[PASS] test_v4QuoteFailsOnMissingPool() (gas: 211187)\n[PASS] test_v4RouteValidation() (gas: 209300)\n[PASS] test_v4ToNativeThenV4NativeImdRoute() (gas: 1189350)\n[PASS] test_v4ToNativeThenWrapIntoV3ImdRoute() (gas: 1275896)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 87.79ms (167.92ms CPU time)\n\nRan 2 test suites in 88.66ms (173.61ms CPU time): 61 tests passed, 0 failed, 0 skipped (61 total tests)\n","passed":true},{"durationMs":73,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.claimRefund(address)\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.receive()\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMaxImpactBps(uint256)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unlockCallback(bytes)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":86,\"foundry.toml\":20,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":844,\"src/interfaces/IDexVenues.sol\":89,\"test/SwarmExit.t.sol\":751,\"test/SwarmExitRevision.t.sol\":612,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":336,\"web/app.js\":318,\"web/config.js\":13,\"web/index.html\":96,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2217,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/SwarmExit.sol:815: SwarmExit._send(address,address,uint256) (src/SwarmExit.sol#815-822) sends eth to arbitrary user\n[high/medium] reentrancy-balance at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:371: Reentrancy in SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#371-399):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[high/medium] reentrancy-balance at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[medium/high] incorrect-equality at src/SwarmExit.sol:832: SwarmExit._callToken(address,bytes) (src/SwarmExit.sol#832-843) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmExit.sol:371: SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#371-399) uses a dangerous strict equality:\n[medium/medium] unused-return at src/SwarmExit.sol:441: SwarmExit.unlockCallback(bytes) (src/SwarmExit.sol#441-457) ignores return value by pm.settle() (src/SwarmExit.sol#453)\n[medium/medium] unused-return at src/SwarmExit.sol:549: SwarmExit._swapV4(SwarmExit.Route,uint256) (src/SwarmExit.sol#549-554) ignores return value by IUniswapV4PoolManager(route.router).unlock(abi.encode(false,currencyIn,hops,amountIn)) (src/SwarmExit.sol#552)\n[medium/medium] unused-return at src/SwarmExit.sol:441: SwarmExit.unlockCallback(bytes) (src/SwarmExit.sol#441-457) ignores return value by pm.settle{value: amountIn}() (src/SwarmExit.sol#449)\n[medium/medium] unused-return at src/SwarmExit.sol:676: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#676-696) ignores return value by (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#689-695)\n[medium/medium] unused-return at src/SwarmExit.sol:323: SwarmExit.ethUsdPrice() (src/SwarmExit.sol#323-329) ignores return value by (None,answer,None,updatedAt,None) = ethUsdFeed.latestRoundData() (src/SwarmExit.sol#325)\n[medium/medium] unused-return at src/SwarmExit.sol:525: SwarmExit._swap(SwarmExit.Route,uint256,uint256) (src/SwarmExit.sol#525-547) ignores return value by IUniswapV3SwapRouter(route.router).exactInput(IUniswapV3SwapRouter.ExactInputParams({path:route.data,recipient:address(this),amountIn:amountIn,amountOutMinimum:0})) (src/SwarmExit.sol#537-542)\n[low/medium] calls-loop at src/SwarmExit.sol:700: SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#700-712) has external calls inside a loop: (success,reason) = manager.call(abi.encodeCall(IUniswapV4PoolManager.unlock,(abi.encode(true,currencyIn,hops,amountIn)))) (src/SwarmExit.sol#703-704)\n[low/medium] calls-loop at src/SwarmExit.sol:825: SwarmExit._staticUint(address,bytes) (src/SwarmExit.sol#825-829) has external calls inside a loop: (success,ret) = target.staticcall(data) (src/SwarmExit.sol#826)\n[low/medium] calls-loop at src/SwarmExit.sol:463: SwarmExit._sellOne(address,uint256,uint256) (src/SwarmExit.sol#463-505) has external calls inside a loop: before = IERC20(token).balanceOf(address(this)) (src/SwarmExit.sol#480)\n[low/medium] calls-loop at src/SwarmExit.sol:676: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#676-696) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#689-695)\n[low/medium] calls-loop at src/SwarmExit.sol:558: SwarmExit._swapV4Hops(address,address,SwarmExit.V4Hop[],uint256) (src/SwarmExit.sol#558-593) has external calls inside a loop: delta = IUniswapV4PoolManager(manager).swap(key,IUniswapV4PoolManager.SwapParams({zeroForOne:zeroForOne,amountSpecified:- int256(amount),sqrtPriceLimitX96:V4_MAX_PRICE_LIMIT}),) (src/SwarmExit.sol#575-584)\n[low/medium] calls-loop at src/SwarmExit.sol:676: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#676-696) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#689-695)\n[low/medium] calls-loop at src/SwarmExit.sol:676: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#676-696) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#680-684)\n[low/medium] calls-loop at src/SwarmExit.sol:676: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#676-696) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#680-684)\n[low/medium] calls-loop at src/SwarmExit.sol:700: SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#700-712) has external calls inside a loop: (success,reason) = manager.call(abi.encodeCall(IUniswapV4PoolManager.unlock,(abi.encode(true,currencyIn,hops,amountIn)))) (src/SwarmExit.sol#703-704)\n[low/medium] calls-loop at src/SwarmExit.sol:463: SwarmExit._sellOne(address,uint256,uint256) (src/SwarmExit.sol#463-505) has external calls inside a loop: leftover = IERC20(token).balanceOf(address(this)) - before (src/SwarmExit.sol#500)\n[low/medium] calls-loop at src/SwarmExit.sol:825: SwarmExit._staticUint(address,bytes) (src/SwarmExit.sol#825-829) has external calls inside a loop: (success,ret) = target.staticcall(data) (src/SwarmExit.sol#826)\n[low/medium] calls-loop at src/SwarmExit.sol:676: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#676-696) has external calls inside a loop: amounts = IUniswapV2Router(route.router).getAmountsOut(amountIn,path) (src/SwarmExit.sol#680-684)\n[low/medium] calls-loop at src/SwarmExit.sol:463: SwarmExit._sellOne(address,uint256,uint256) (src/SwarmExit.sol#463-505) has external calls inside a loop: imdOut = this.executeSale(token,received,minOut,deadline) (src/SwarmExit.sol#494-504)\n[low/medium] calls-loop at src/SwarmExit.sol:463: SwarmExit._sellOne(address,uint256,uint256) (src/SwarmExit.sol#463-505) has external calls inside a loop: received = IERC20(token).balanceOf(address(this)) - before (src/SwarmExit.sol#490)\n[low/medium] calls-loop at src/SwarmExit.sol:832: SwarmExit._callToken(address,bytes) (src/SwarmExit.sol#832-843) has external calls inside a loop: (success,ret) = token.call(data) (src/SwarmExit.sol#837)\n[low/medium] calls-loop at src/SwarmExit.sol:676: SwarmExit._quote(SwarmExit.Route,uint256) (src/SwarmExit.sol#676-696) has external calls inside a loop: (out) = IUniswapV3Quoter(route.quoter).quoteExactInput(route.data,amountIn) (src/SwarmExit.sol#689-695)\n[low/medium] calls-loop at src/SwarmExit.sol:700: SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#700-712) has external calls inside a loop: (success,reason) = manager.call(abi.encodeCall(IUniswapV4PoolManager.unlock,(abi.encode(true,currencyIn,hops,amountIn)))) (src/SwarmExit.sol#703-704)\n[low/medium] calls-loop at src/SwarmExit.sol:825: SwarmExit._staticUint(address,bytes) (src/SwarmExit.sol#825-829) has external calls inside a loop: (success,ret) = target.staticcall(data) (src/SwarmExit.sol#826)\n[low/medium] calls-loop at src/SwarmExit.sol:558: SwarmExit._swapV4Hops(address,address,SwarmExit.V4Hop[],uint256) (src/SwarmExit.sol#558-593) has external calls inside a loop: delta = IUniswapV4PoolManager(manager).swap(key,IUniswapV4PoolManager.SwapParams({zeroForOne:zeroForOne,amountSpecified:- int256(amount),sqrtPriceLimitX96:V4_MIN_PRICE_LIMIT}),) (src/SwarmExit.sol#575-584)\n[low/medium] reentrancy-benign at src/SwarmExit.sol:515: Reentrancy in SwarmExit._refund(address,uint256) (src/SwarmExit.sol#515-522):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:615: Reentrancy in SwarmExit._previewOne(address,address) (src/SwarmExit.sol#615-647):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:549: Reentrancy in SwarmExit._swapV4(SwarmExit.Route,uint256) (src/SwarmExit.sol#549-554):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:700: Reentrancy in SwarmExit._quoteV4(address,bytes,uint256) (src/SwarmExit.sol#700-712):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:650: Reentrancy in SwarmExit._quoteToImd(SwarmExit.Route,uint256) (src/SwarmExit.sol#650-663):\n[low/medium] reentrancy-benign at src/SwarmExit.sol:413: Reentrancy in SwarmExit.executeSale(address,uint256,uint256,uint256) (src/SwarmExit.sol#413-436):\n[low/medium] reentrancy-events at src/SwarmExit.sol:294: Reentrancy in SwarmExit.rescue(address,address,uint256) (src/SwarmExit.sol#294-300):\n[low/medium] timestamp at src/SwarmExit.sol:371: SwarmExit.sellAll(address[],uint256[],uint256) (src/SwarmExit.sol#371-399) uses timestamp for comparisons\n[low/medium] timestamp at src/SwarmExit.sol:323: SwarmExit.ethUsdPrice() (src/SwarmExit.sol#323-329) uses timestamp for comparisons","passed":true},{"durationMs":494,"exitCode":0,"name":"aderyn","output":"[high] storage-array-memory-edit at src/SwarmExit.sol:356: Storage Array Edited with Memory\n[low] centralization-risk at src/SwarmExit.sol:37: Centralization Risk (10 places)\n[low] costly-loop at src/SwarmExit.sol:218: Costly operations inside loop (4 places)\n[low] large-numeric-literal at src/SwarmExit.sol:54: Large Numeric Literal\n[low] literal-instead-of-constant at src/SwarmExit.sol:738: Literal Instead of Constant (14 places)\n[low] require-revert-in-loop at src/SwarmExit.sol:383: Loop Contains `require`/`revert` (5 places)\n[low] state-change-without-event at src/SwarmExit.sol:336: State Change Without Event (4 places)\n[low] state-no-address-check at src/SwarmExit.sol:551: Address State Variable Set Without Checks (2 places)\n[low] unchecked-return at src/SwarmExit.sol:421: Unchecked Return (5 places)\n[low] uninitialized-local-variable at src/SwarmExit.sol:386: Uninitialized Local Variable (5 places)\n[low] unsafe-erc20-operation at src/SwarmExit.sol:483: Unsafe ERC20 Operation (2 places)","passed":true},{"durationMs":3231,"exitCode":0,"name":"proof 2fe394082bb6","output":"Compiling 39 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.44s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-1517414c/Proof_2fe394082bb6.t.sol:ExactBalancePullGriefTest\n[PASS] test_oneWeiDonationAfterExactApprovalDoesNotBlockTheSale() (gas: 313308)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.71ms (310.73µs CPU time)\n\nRan 1 test suite in 4.32ms (1.71ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":2437,"exitCode":0,"name":"proof 567f79eb091e","output":"2026-10-14T04:43:40.664901Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-MuxMXP/repo/test/imd-proof-1517414c/Proof_2fe394082bb6.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.69s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-91a4e783/Proof_567f79eb091e.t.sol:RefundPathRevertsTest\n[PASS] test_tokenWhoseApproveRevertsIsRefundedAndOthersStillSell() (gas: 328638)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.94ms (401.17µs CPU time)\n\nRan 1 test suite in 4.39ms (1.94ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":2545,"exitCode":0,"name":"proof 558d3fb63bc9","output":"2026-10-14T04:43:43.178599Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-MuxMXP/repo/test/imd-proof-91a4e783/Proof_567f79eb091e.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.75s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-b7cdfb31/Proof_558d3fb63bc9.t.sol:RefundRevertTest\n[PASS] test_refundThatRevertsMustNotBlockOtherSales() (gas: 513377)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.60ms (782.67µs CPU time)\n\nRan 1 test suite in 6.61ms (3.60ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":2449,"exitCode":0,"name":"proof 2f77db836e54","output":"2026-10-14T04:43:45.749081Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-MuxMXP/repo/test/imd-proof-b7cdfb31/Proof_558d3fb63bc9.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.66s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-cce57924/Proof_2f77db836e54.t.sol:ZeroProceedsProofTest\n[PASS] test_zeroProceedsSaleIsNotSettledAsSold() (gas: 386660)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.44ms (558.45µs CPU time)\n\nRan 1 test suite in 4.62ms (2.44ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d13f19cb1f97cc68bfcdfdd97f13601b6053ba7e98536614ac4ea2f28385502e","verifiedTreeHash":"0cbdfaeb0262b6e4b9bfdea641aafe272c020791","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":3057,"exitCode":0,"name":"build","output":"Compiling 41 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.96s\nCompiler run successful!\n","passed":true},{"durationMs":174,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 34 tests for test/SwarmExit.t.sol:SwarmExitTest\n[PASS] testFuzz_slippageGuardNeverSettlesBelowMin(uint96,uint256) (runs: 256, μ: 463171, ~: 456042)\nLogs:\n  Bound result 10964816929523717477494\n  Bound result 32689294845249262496316\n\n[PASS] testFuzz_taxSplitConservesImd(uint96,uint96) (runs: 256, μ: 809129, ~: 809288)\nLogs:\n  Bound result 99999000000000000007982\n  Bound result 9999000000000000000652\n\n[PASS] test_bridgeRouteMissingRefunds() (gas: 1028445)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 47594)\n[PASS] test_deployScriptFunctionUsesGivenOwner() (gas: 7291684)\n[PASS] test_ethUsdPriceChecks() (gas: 126413)\n[PASS] test_executeSaleOnlySelf() (gas: 32618)\n[PASS] test_feeOnTransferTokenSellsMeasuredAmount() (gas: 482091)\n[PASS] test_honeypotIsRefundedOthersSell() (gas: 1038562)\n[PASS] test_inputValidation() (gas: 703621)\n[PASS] test_missingApprovalRevertsWholeCall() (gas: 63261)\n[PASS] test_noLiquidityTokenIsRefunded() (gas: 581735)\n[PASS] test_noRouteTokenIsSkippedWithoutPull() (gas: 463921)\n[PASS] test_nothingSoldReverts() (gas: 622471)\n[PASS] test_ownerOnlyFunctionsRejectOthers() (gas: 203117)\n[PASS] test_pauseBlocksSelling() (gas: 675528)\n[PASS] test_previewAllCoversRegistry() (gas: 375633)\n[PASS] test_previewBridgeRouteMissing() (gas: 405742)\n[PASS] test_previewStatuses() (gas: 708547)\n[PASS] test_quoteImdInBridgeNotConfigured() (gas: 28649)\n[PASS] test_quoteImdInBridgeReversesMultiHopV3Route() (gas: 523036)\n[PASS] test_quoteImdInBridgeReversesV2Route() (gas: 190601)\n[PASS] test_quoteImdInBridgeReversesV3Route() (gas: 97755)\n[PASS] test_reentrancyFromTokenIsBlocked() (gas: 1909547)\n[PASS] test_registryAddRemove() (gas: 265511)\n[PASS] test_rescueOnlyNonSwarmTokens() (gas: 201024)\n[PASS] test_runtimeSizeAndNoForbiddenOpcodes() (gas: 10457717)\n[PASS] test_sellAllEmitsEvents() (gas: 405094)\n[PASS] test_sellAllHappyPathSplitsTax() (gas: 1205777)\n[PASS] test_setImdRouteValidation() (gas: 201654)\n[PASS] test_setTokenRouteValidation() (gas: 320793)\n[PASS] test_slippageBreachRefundsThatTokenOnly() (gas: 847245)\n[PASS] test_twoStepOwnership() (gas: 103211)\n[PASS] test_zeroBalanceTokenIsSkippedNotPulled() (gas: 586364)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 64.11ms (162.49ms CPU time)\n\nRan 1 test suite in 65.23ms (64.11ms CPU time): 34 tests passed, 0 failed, 0 skipped (34 total tests)\n","passed":true},{"durationMs":56,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"SwarmExit.acceptOwnership()\",\"SwarmExit.executeSale(address,uint256,uint256,uint256)\",\"SwarmExit.pause()\",\"SwarmExit.preview(address,address[])\",\"SwarmExit.previewAll(address)\",\"SwarmExit.quoteImdInBridge(uint256)\",\"SwarmExit.renounceOwnership()\",\"SwarmExit.rescue(address,address,uint256)\",\"SwarmExit.sellAll(address[],uint256[],uint256)\",\"SwarmExit.setEthUsdFeed(address)\",\"SwarmExit.setImdRoute(uint8,address,address,bytes)\",\"SwarmExit.setMinImdOut(uint256)\",\"SwarmExit.setSwarmToken(address,bool)\",\"SwarmExit.setTokenRoute(address,uint8,address,address,bytes)\",\"SwarmExit.transferOwnership(address)\",\"SwarmExit.unpause()\"],\"files\":{\".gitignore\":6,\"README.md\":78,\"foundry.toml\":20,\"launch.json\":10,\"remappings.txt\":2,\"script/Deploy.s.sol\":22,\"src/SwarmExit.sol\":540,\"src/interfaces/IDexVenues.sol\":49,\"test/SwarmExit.t.sol\":711,\"test/mocks/MockTokens.sol\":92,\"test/mocks/MockVenues.sol\":178,\"web/app.js\":222,\"web/config.js\":13,\"web/index.html\":95,\"web/vendor/ethers.umd.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e4ba6e120a71929eb229d03e375d15acc68b7b7579dbc1ae7c40a0b220de84a8","verifiedTreeHash":"875d812923e8d291ee74d5c37e03622ef63bd1f7","verifierVersion":"0.1.0+67c588fa"}]}