{"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":"75b42be0-4125-47c5-b027-db140597cd4c","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"efef1486ee39d058baeab76eb7b12a82102d162b11127999610a841dcda68e22","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":"2fa3bffedd91b9073a4dc82d48284cc71b4b629c9028d5ae36f2f235b76ac254","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":"b34c339cecb928aa95d5376cf693e92e9feaefc818526d921473ca6e304a1a3a","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":"9d9cc7783c0e3582ba518e6553dde8275f5dc3816502b2c4d15ec483e83661d5","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":"1071bc7bbc61cac59c52e7a1d1dbf52f86cf8d9f82172bbc5656b29a2678c4b7","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":"1972d262e2825a564067e0a4859770440302423b87f4ecc986981a136505a394","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"699f565508ed54ad8bf2fd2c7de80a5617a6481e63e00fffc19c0545ba7d1fc5","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"b582f0eb71b6429bc65e450de461a6023b5ab522c65061b96911ca784d2c2cdc","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":"A custom token: NIGHTSHIFT (SHIFT).\nToken name: NIGHTSHIFT\nToken symbol: SHIFT\nToken supply: 1,000,000,000 with 18 decimals, all minted once to the deployer in the constructor.\nWhat it does: a 1% burn imd fees every transaction (test)","parentJobId":null,"planHash":"b230d51015290a39ed1fe7e1eeae5e3362c6c4594865d76346ca493c91fb613e","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"75b42be0-4125-47c5-b027-db140597cd4c","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2126-nightshift"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52600","feedbackHash":"86f22d011ead5ef2e57dcb047d71d821aedf8058c6de14f197e70915b996ca20","nodeKey":"audit_economics","submissionHash":"efef1486ee39d058baeab76eb7b12a82102d162b11127999610a841dcda68e22","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52608","feedbackHash":"9dd102098919f9cfe509bbe9607117661eaf519b5b788e0145d8cdc9a9b08f5f","nodeKey":"audit_flow","submissionHash":"2fa3bffedd91b9073a4dc82d48284cc71b4b629c9028d5ae36f2f235b76ac254","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52266","feedbackHash":"c30cde4f00b25640abec2659405273bba5f4507b6c1c5bab8558985de2093931","nodeKey":"audit_judge","submissionHash":"cde17ce25d69dbfccccbe300cdeb56965b4f8d6c276c0f7177b49b4f931fe5cb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50966","feedbackHash":"a1e852680d0e3f2311738ce0ee013713f03c74dd5848630faa76c0c146b53fe3","nodeKey":"audit_judge","submissionHash":"b34c339cecb928aa95d5376cf693e92e9feaefc818526d921473ca6e304a1a3a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52617","feedbackHash":"6aa9421e86379cbc76069064c7d3deac601296bec4c3bcd0613bcaf797147aea","nodeKey":"audit_math","submissionHash":"9d9cc7783c0e3582ba518e6553dde8275f5dc3816502b2c4d15ec483e83661d5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52606","feedbackHash":"bf4963771b04515cf9e797892aaab426cf9647c2882304662b5ba6f80d592f5d","nodeKey":"audit_permissions","submissionHash":"1071bc7bbc61cac59c52e7a1d1dbf52f86cf8d9f82172bbc5656b29a2678c4b7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"54232","feedbackHash":"e36c6243dd4562575a0dc0b685e866401c523fa0fd705758cb93ff757e62e51a","nodeKey":"build_contract_project","submissionHash":"f0d85ba05d89f2d1091a36a30c856408f58949d6b02e9fafbcb630f267013f04","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51347","feedbackHash":"f2440e39df3fae4fc53d429f40b3c39db507daaca3069afab86057a287291ca4","nodeKey":"build_contract_project","submissionHash":"1972d262e2825a564067e0a4859770440302423b87f4ecc986981a136505a394","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"53907","feedbackHash":"c21e94c1f1fd623ba0a80a2fb9a79ad206e229d0cad0f788a89e25b9548f868d","nodeKey":"manifest","submissionHash":"5cfef36c017773a14b097967a11c70faedd992118038bedecee1080a1afc4452","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51507","feedbackHash":"cfb0f0ce3bfc2cf9f586a7c048fa262664fc014d11865082244bf3e8e6b2a3a0","nodeKey":"manifest","submissionHash":"699f565508ed54ad8bf2fd2c7de80a5617a6481e63e00fffc19c0545ba7d1fc5","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52167","feedbackHash":"8802f5775fcb6fbcbe7c227845bb0a8d5b64e97e56d96c39409a1091c57cc8cf","nodeKey":"write_foundry_tests","submissionHash":"b582f0eb71b6429bc65e450de461a6023b5ab522c65061b96911ca784d2c2cdc","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"54231","feedbackHash":"8ecb2429cd5fb65d73cffa0d0da8efe33f8e974bf6161ea94cf43ade69804184","nodeKey":"write_foundry_tests","submissionHash":"0add812f13d5d228c5f864eb502c794646acfb8b6948c8ec5b41a2ea7284d30d","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"ed57c30df5e438a58e196f9d6c060d966740e9e8e47f908060fda0c408a73a64","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"368c4f6119fac12406e83aad807523b9947e3a6dc34cf000048b5ab2654b0b36","device":"a0f92fec784312b7","findings":[{"description":"The objective is a 1% burn on every transaction. _update skips the burn when either side isLaunchExempt, and POOL_MANAGER is exempt as sender as well as recipient (line 74). Uniswap v4 lets anyone unlock, sync(SHIFT), transfer SHIFT in, settle, and take(SHIFT, recipient, amount): both legs touch the PoolManager, so value moves between two ordinary holders with no burn at all. The same exemption also makes every buy from the launch pool burn-free. Only transfers INTO the PoolManager have to arrive whole (otherwise settle credits short and the seed or a sell reverts with CurrencyNotSettled). A payout FROM the PoolManager (a buy or a take) can be taxed like any other transfer without breaking the launch floor: the protected test buys, reads balanceOf(trader) and sells exactly that. Suggested fix: exempt POOL_MANAGER only as recipient (to == POOL_MANAGER), or tax from == POOL_MANAGER when the recipient is not exempt. Note that the existing accepted test test_poolManagerFlowsAreNotTaxedEitherWay asserts that a buy arrives whole and would need updating along with the fix.","line":92,"path":"src/ShiftToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ShiftToken} from \"src/ShiftToken.sol\";\n\ncontract BypassFactory {\n    mapping(uint64 => address) public distributorOf;\n\n    function deploy(address poolManager, uint64 launchNumber) external returns (ShiftToken) {\n        return new ShiftToken(address(this), poolManager, launchNumber);\n    }\n\n    function move(ShiftToken token, address to, uint256 amount) external returns (bool) {\n        return token.transfer(to, amount);\n    }\n}\n\n/// @notice Two ordinary holders move value through the Uniswap v4 PoolManager the way its flash\n/// accounting allows (holder transfers in and `settle`s, then `take`s to the recipient). Both legs\n/// touch the PoolManager, so neither is taxed: the 1% burn on every transaction is skipped. Only\n/// transfers *into* the PoolManager need to arrive whole for sells and the seed to settle; a\n/// payout *from* it (a buy or a `take`) can be taxed like any other transfer.\ncontract PoolManagerBypassTest is Test {\n    address constant POOL_MANAGER = address(0x9001);\n    address constant ALICE = address(0xA11CE);\n    address constant BOB = address(0xB0B);\n\n    function test_routingThroughThePoolManagerStillBurnsOnePercent() public {\n        BypassFactory factory = new BypassFactory();\n        ShiftToken token = factory.deploy(POOL_MANAGER, 1);\n        factory.move(token, ALICE, 1_000e18);\n        uint256 supplyBefore = token.totalSupply();\n\n        // sync + transfer + settle: Alice's tokens enter the PoolManager.\n        vm.prank(ALICE);\n        token.transfer(POOL_MANAGER, 1_000e18);\n        // take: the PoolManager pays the credit out to Bob.\n        vm.prank(POOL_MANAGER);\n        token.transfer(BOB, 1_000e18);\n\n        assertEq(token.balanceOf(BOB), 990e18, \"Bob received Alice's value with no burn\");\n        assertEq(token.totalSupply(), supplyBefore - 10e18, \"the 1% burn was skipped\");\n    }\n}","reproduction":"Factory funds ALICE with 1000e18. ALICE.transfer(POOL_MANAGER, 1000e18) (the settle leg), then POOL_MANAGER.transfer(BOB, 1000e18) (the take leg). Expected under '1% burn every transaction': BOB gets 990e18 and totalSupply falls by 10e18. Actual: BOB gets 1000e18 and totalSupply is unchanged.","severity":"medium","title":"Transfers out of the PoolManager are exempt, so any holder can skip the 1% burn by routing through v4 flash accounting"},{"description":"launchDistributor() handles a failed or short reply by returning address(0), but abi.decode(data, (address)) reverts if the first word is not a clean address. Because _update calls isLaunchExempt for every taxed transfer, that revert blocks every ordinary transfer, which contradicts the documented intent that the token never bricks on a factory problem. The real ProjectFactory returns a typed address, so this cannot happen with it. Hardening: decode as uint256 and treat values above type(uint160).max as no distributor.","line":69,"path":"src/ShiftToken.sol","reproduction":"Factory whose fallback returns abi.encode(type(uint256).max) for distributorOf(uint64). Fund 0xA with 1e18 from the factory, then 0xA.transfer(0xB, 1e18). Expected: taxed transfer succeeds (0xB gets 0.99e18). Actual: reverts.","severity":"info","title":"A distributorOf reply with dirty upper bits reverts every ordinary transfer"},{"description":"burnFor rounds down, so any transfer of 99 wei or less burns nothing. Splitting a transfer saves at most 1 wei of burn per extra transfer, which costs far more in gas than it saves, so this is not exploitable at 18 decimals. Recorded for completeness. Covered by testFuzz_splittingNeverBurnsMoreThanOneTransfer and test_oneWeiAndNinetyNineWeiNeverBurn.","line":92,"path":"src/ShiftToken.sol","reproduction":"ALICE holds 100 wei; transfer(BOB, 99) then transfer(BOB, 1): BOB receives 100 wei and totalSupply is unchanged. A single transfer of 100 wei would burn 1 wei.","severity":"info","title":"Transfers under 100 wei burn nothing, so dust-sized transfers are fee-free"}],"hash":"0add812f13d5d228c5f864eb502c794646acfb8b6948c8ec5b41a2ea7284d30d","nodeId":"676065b4-1316-416f-a531-936399f3f157","outcome":"completed","summary":"I added two test files and reported one real defect in the token. `forge build` succeeds and `forge test` passes 54 of 54 with the verifier's fuzz seed: the 31 existing tests plus 23 new ones.\n\n**What I added (only under `test/`; the existing `test/ShiftToken.t.sol` is unchanged):**\n- **`test/ShiftToken.invariant.t.sol`**: invariant tests that make random sequences of calls from 7 accounts, including the factory, PoolManager and distributor. The calls are transfers, approve + transferFrom (exact and unlimited allowances), transfers over the balance or allowance that must revert, re-pointing the distributor, and tries at admin calls like mint, pause or blacklist. Every call checks its own result: the sender loses the full amount, the recipient gets the amount minus the burn, and the supply falls by exactly the burn. The invariants are:\n  - balances always add up to the total supply;\n  - total supply plus everything burned equals the initial 1,000,000,000;\n  - the supply never grows;\n  - nothing ever sits at the zero address;\n  - the fixed settings and the 1% rate never change.\n\n  These run 256 sequences of 64 calls each and fail on any revert, so a broken check can't be silently skipped. That setting caught a bug in my own test code, which I fixed; it was not a token bug.\n- **`test/ShiftToken.edge.t.sol`** (22 tests):\n  - the factory's distributor lookup returning short, empty or over-long replies;\n  - a distributor that is re-pointed and loses its exemption;\n  - unlimited allowances not being spent down;\n  - a failed transfer leaving the allowance untouched;\n  - transfers to the zero address and approving the zero address both rejected;\n  - exempt accounts trying to send more than they hold;\n  - the whole supply moving between ordinary holders;\n  - the burn event firing only when something is burned;\n  - the edges of the 1% calculation.\n\n  It also has fuzz tests: the burn is always exactly 1% rounded down, exempt flows arrive in full, splitting a transfer saves at most 1 wei of burn, and a failed transfer changes nothing.\n\n**Reported in `.imd-findings.json` instead of written as passing tests:**\n- **Medium: anyone can skip the 1% burn.** Any transfer touching the PoolManager is burn-free in either direction (`src/ShiftToken.sol:74`, `:92`). Anyone can use Uniswap v4's settle-and-take flow to move tokens between two ordinary holders with no burn. Every buy from the launch pool is also burn-free. Example: Alice sends 1000 to the PoolManager, which pays 1000 to Bob. Bob gets 1000 and the supply is unchanged; under \"1% burn every transaction\" he should get 990. The launch only needs transfers *into* the PoolManager to be exempt, so payouts from it could be taxed without breaking the launch checks. The finding includes a test that fails on the current code. If the token is fixed, the existing test `test_poolManagerFlowsAreNotTaxedEitherWay` will need updating, because it asserts that a buy arrives in full.\n- **Info:** if the factory's `distributorOf` returned a malformed address, every ordinary transfer would revert (confirmed). The real factory can't return one.\n- **Info:** transfers under 100 wei burn nothing because of rounding down. Splitting saves at most 1 wei per extra transfer, which isn't worth the gas.\n\nI couldn't run the two provided launch-check files here, because the Uniswap v4 code they import isn't in this repository.","treeHash":"8505c1afdb3c2e85769be31a90f898c422c34d02","usage":{"cachedInputTokens":805374,"inputTokens":30,"model":"claude-opus-5-5","outputTokens":22714,"runtime":"claude","turns":15,"wallClockMs":419373}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"829d84438fb3fdc8","findings":[{"citation":"resolved","description":"Area: Asymmetry x Trust Gap (access x economics). The burn is skipped whenever the sender OR the recipient is FACTORY, POOL_MANAGER or the distributor (src/ShiftToken.sol:73-77, 92). The exemption is by address, not by flow, and the Uniswap v4 PoolManager is a permissionless custody contract: any caller may `unlock`, transfer SHIFT into it (to == POOL_MANAGER, delivered whole), call `settle()` (credited by balance delta) and then `take(currency, to, amount)` to any address of its choosing (from == POOL_MANAGER, delivered whole), ending with a zero delta. No pool, swap or liquidity is involved. The same applies to ERC-6909 claim balances held inside the PoolManager, which can be transferred between holders indefinitely without ever touching `_update`. Consequence: the token's one feature, \"a 1% burn on every transaction\", is voluntary for every contract account (smart wallets, exchanges, OTC desks) and costs an EOA holder the 1% at most once, when it first moves tokens into a contract it controls; after that every onward transfer is burn-free. The README (Assumptions) does not disclose this; it states only that non-launch contracts receive 1% less. Trust note: the recipient-side exemption for POOL_MANAGER is required for sells to settle exactly (the protected floor's TraderProbe settles exactly the swap delta), so the bypass is inherent to the chosen design, not a missing check. Fix requires a scope decision: either (a) accept and document that the burn applies only to transfers that do not route through the PoolManager, and stop describing the token as burning on every transaction, or (b) change the model (e.g. tax only when neither side is exempt AND the transfer is not a PoolManager flow cannot be distinguished on-chain, so this means taxing sells and having routers settle `value/0.99`, which the floor's exact-settle test does not allow). A proof test is not attached because no code-only fix exists that preserves the floor.","line":92,"path":"src/ShiftToken.sol","reproduction":"State: ShiftToken deployed with POOL_MANAGER = the v4 PoolManager P; Alice's contract wallet W holds 500e18 SHIFT (received whole from a buy, or funded once at a 1% cost). Sequence (verified in test/scratch/Leads.t.sol::test_lead1 with a PoolManager stand-in that mirrors v4's unlock/sync/settle/take): 1) W calls P.unlock(data). 2) In W.unlockCallback: P.sync(SHIFT); SHIFT.transfer(P, 500e18) -> isLaunchExempt(to) is true at line 92, super._update moves 500e18 whole; P.settle() credits W +500e18; P.take(SHIFT, Bob, 500e18) -> P calls SHIFT.transfer(Bob, 500e18), isLaunchExempt(from) true, whole; W's delta is 0 and unlock completes. Expected (per README/NatSpec 'burns 1% of every ordinary transfer'): Bob receives 495e18 and totalSupply falls by 5e18, as a direct Alice->Bob transfer of 500e18 does (checked in the same test: 495e18 / SUPPLY-5e18). Actual: Bob's balance rises by exactly 500e18 and totalSupply is unchanged; P ends with 0 SHIFT. Cost to the actor: gas only.","severity":"medium","snippet":"        if (burned == 0 || isLaunchExempt(from) || isLaunchExempt(to)) {","title":"Recipient-side PoolManager exemption lets anyone move SHIFT with no burn via unlock/settle/take"},{"citation":"resolved","description":"Area: Trust Gap (periphery fallback vs documented guarantee). NatSpec (lines 24-25) and the README promise that if the factory lookup 'fails or the factory has no code, no distributor is exempt and the transfer is simply taxed', so 'the token never bricks on a factory problem'. The guard only covers `!ok` and short return data. If the call succeeds and returns >= 32 bytes whose first word has any of the top 96 bits set (a different return type at that selector, a proxy fallback echoing calldata, a factory upgrade that changes `distributorOf`'s shape), `abi.decode(data, (address))` reverts with a decoding panic, and because `isLaunchExempt` runs inside `_update` for every non-exempt transfer, every ordinary `transfer`/`transferFrom` reverts for as long as the factory answers that way. Flows from/to FACTORY and POOL_MANAGER still work (checked before the lookup), so the launch itself survives, but holders cannot move tokens peer to peer. Reachability needs the factory to misbehave, hence low. Minimal fix preserving behaviour: decode defensively, e.g. `uint256 word = abi.decode(data, (uint256)); if (word > type(uint160).max) return address(0); return address(uint160(word));` (or an assembly mload with the same bound), keeping `data.length < 32 -> address(0)`.","line":68,"path":"src/ShiftToken.sol","reproduction":"State: factory contract F whose distributorOf(uint64) returns bytes32(uint256(1) << 255) (test/scratch/Leads.t.sol::DirtyFactory); token = new ShiftToken(F, 0x9001, 7) deployed by F; F transfers 100e18 to Alice (exempt path, succeeds). Call: vm.prank(Alice); token.transfer(Bob, 10e18). Expected per NatSpec: Bob receives 9.9e18 and 0.1e18 burns (lookup treated as failed). Actual: the call reverts (abi.decode panic inside launchDistributor), and token.launchDistributor() itself reverts instead of returning address(0). Verified by test_lead2_dirtyDistributorAnswerBricksOrdinaryTransfers.","severity":"low","snippet":"        if (!ok || data.length < 32) return address(0);\n        return abi.decode(data, (address));","title":"launchDistributor() reverts, bricking every ordinary transfer, when the factory answers 32 bytes that are not a clean address"},{"citation":"resolved","description":"Area: Access Control / Trust Gap (documented, not a defect). The token has no owner, mint, pause, blocklist or setter (verified: constructor sets only immutables, lines 53-59; the only external state writers are OZ approve/transfer/transferFrom; test_noMintOrAdminEntryPointExists and the runtime opcode scan pass). The one mutable input to the permission model is the factory's live answer to distributorOf(LAUNCH_NUMBER): whoever can change that answer (an upgradeable or admin-controlled factory) can at any time name an arbitrary address that both receives and sends SHIFT with no burn, i.e. a tax-free hub equivalent to the PoolManager bypass in finding 1, and can un-name the real distributor so that contributor claims arrive 1% short. The staticcall also forwards all remaining gas, so a factory that burns gas on that call makes ordinary transfers fail until it stops. These powers sit with IMD's factory, outside this repository, and the token cannot constrain them without dropping the live lookup. Record as a trust assumption in the README's Assumptions section: 'the factory's distributorOf(launchNumber) is correct and does not change after launch'.","line":75,"path":"src/ShiftToken.sol","reproduction":"State: token deployed by MockFactory F (test/ShiftToken.t.sol pattern), Alice holds 100e18. F.setDistributor(LAUNCH_NUMBER, X) for an arbitrary X after launch. Then Alice.transfer(X, 100e18) -> X holds 100e18 (no burn); X.transfer(Bob, 100e18) -> Bob holds 100e18, totalSupply unchanged. Expected if only the launch's real distributor were exempt: 1% burned on each hop. This is the intended lookup behaving as designed; it is listed so the judge sees the trust boundary.","severity":"info","snippet":"        address distributor = launchDistributor();\n        return distributor != address(0) && account == distributor;","title":"Trust assumption: the factory chooses the third tax-exempt address at every transfer"}],"hash":"1071bc7bbc61cac59c52e7a1d1dbf52f86cf8d9f82172bbc5656b29a2678c4b7","nodeId":"6f5054af-b581-4448-a7c2-4462c0bbddd1","outcome":"completed","summary":"The review is complete. The findings file is written and validated against the tree, and nothing outside `.imd-findings.json` and `test/scratch/` was touched.\n\n**Outcome.** Three findings, none critical or high, so no proof file is attached. All 31 existing tests pass, and both of my scratch reproductions behave as described.\n\n- **Medium: the 1% burn is bypassable by anyone through the PoolManager.** The exemption at `src/ShiftToken.sol:92` is by address on either side of a transfer. Any caller can `unlock` the Uniswap v4 PoolManager, transfer SHIFT into it whole, `settle`, then `take` to any recipient whole, ending with zero delta and no burn. ERC-6909 claim balances inside the PoolManager also move without ever reaching the token's burn logic. The burn therefore applies at most once per holder, when tokens first enter a contract they control. The recipient-side exemption is forced by the protected floor's exact-settle sell test, so this is a design consequence that needs a scope decision, not a missing check. I said so in the finding and listed the tradeoff.\n- **Low: the documented lookup fallback is not total.** When the factory's `distributorOf` returns 32 or more bytes whose first word has high bits set, `abi.decode` at line 69 reverts, and every ordinary transfer reverts with it, contrary to the NatSpec promise that a failed lookup just taxes the transfer. Launch flows still work. Fix is a bounded decode.\n- **Info: trust assumption.** The factory's live `distributorOf` answer is the only mutable input to the permission model. Whoever can change it can name a tax-free hub or make contributor claims arrive short. Recorded for the README, not as a defect.\n\n**Coverage.** All three entry points have rows: `approve` holds, `transfer` and `transferFrom` carry finding 1. Four invariant rows cover fixed supply with no privileged surface, balance conservation, the launch flows, and the absent manifest. Access Control was covered fully, since the contract has no roles or setters. Asymmetry was covered for transfer versus transferFrom, burn versus exempt branches, and self-transfer. Trust Gap was covered for the factory lookup and the PoolManager custody path. Nothing in my area was left unreached.","treeHash":null,"usage":{"cachedInputTokens":647000,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":19838,"runtime":"claude","turns":19,"wallClockMs":323312}},{"artifacts":[],"attempt":1,"bundleHash":"ec6aafb5f266e054fb364790e0e5c21bc9b9129d54b7d53211c3a54735d84037","device":"af9a875696459139","findings":[],"hash":"1972d262e2825a564067e0a4859770440302423b87f4ecc986981a136505a394","nodeId":"b0725736-8415-4a45-ab0b-917abfa807cf","outcome":"completed","summary":"Revision complete. All three findings that reproduced are fixed, two advisory ones are answered as documented behaviour, and the reviewer's proof now passes.\n\n**What reproduced and what changed**\n\n- **PoolManager bypass (medium).** The proof failed on the starting tree exactly as reported. `src/ShiftToken.sol` now exempts the PoolManager only as a recipient through a new `isExemptRecipient` view. Seeds and sells still arrive whole, so settle sees an exact delta. Buys and takes are taxed 1% like any other transfer, which closes the sync/settle/take hop. Payouts from the PoolManager to the factory or distributor stay whole since those recipients are exempt in both directions.\n- **Dirty distributor reply (low).** A factory returning a 32-byte word with high bits set bricked ordinary transfers. The lookup now decodes the word as an integer and treats anything above 160 bits as \"no distributor\".\n- **Overflow panic (info).** The fee is now computed with OpenZeppelin's internal `Math.mulDiv`, so huge transfers revert with the documented insufficient-balance error instead of an arithmetic panic. The verifier's fuzz seed still passes, so results for reachable amounts are unchanged.\n- **Rounding down (info).** Reproduces but is intended and documented. Answered as disputed with the reasoning. No code change.\n- **Factory trust boundary (info).** Added to the README's Assumptions as the token's one external trust dependency. No code change.\n\n**Tests.** The suite grew from 30 to 36 tests in `test/ShiftToken.t.sol`. The old \"not taxed either way\" PoolManager test was replaced with one asserting the buy arrives at 99% and the sell of the full bought balance arrives whole. New tests cover the proof's scenario, PoolManager payouts to launch addresses, dirty and short factory replies, and the overflow revert reason.\n\n| Check | Result |\n| --- | --- |\n| forge build | passes, one pre-existing lint note about an event after a staticcall |\n| forge test with verifier seed | 39 passed, 0 failed, including the proof |\n| forge fmt --check | passes |\n\n**Files changed.** `src/ShiftToken.sol`, `test/ShiftToken.t.sol`, `README.md`, and the new `.imd-responses.json` answering all five finding ids. Nothing is committed, since the harness owns the submission commit. The scratch copies under `test/scratch/` are disposable.","treeHash":"0e6086b6b0ecd36623f9299a6146b2d781c461cb","usage":{"cachedInputTokens":816793,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":19502,"runtime":"claude","turns":27,"wallClockMs":292577}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c9b660ee1938912","findings":[{"citation":"resolved","description":"Execution-trace / first-principles: _update exempts a transfer when either side is POOL_MANAGER (line 74: `if (account == FACTORY || account == POOL_MANAGER) return true;`). The code assumes that every token movement touching the PoolManager is a launch flow (seed, buy, sell). In Uniswap v4 that does not hold: inside `unlock`, anyone can call `sync(SHIFT)`, transfer SHIFT to the PoolManager, call `settle()` to get a positive delta, and call `take(SHIFT, recipient, amount)`. None of these steps needs a pool or a swap. Both legs (holder->PoolManager and PoolManager->recipient) are exempt, so the recipient receives the gross amount and nothing is burned. The same applies to any other v4 pool holding SHIFT on that PoolManager, and to SHIFT held as ERC-6909 claims inside it. In practice the burn is optional for anyone who adds one unlock callback. The README's 'burns 1% of every ordinary transfer' and the brief's 'a 1% burn every transaction' are therefore not guarantees. The floor requires PoolManager buys and sells to arrive whole, so dropping the exemption is not an acceptable fix. The minimal fix is to state in the README/NatSpec that transfers routed through the PoolManager (all v4 trades and settle/take round trips) burn nothing, and that the burn applies only to direct transfers. Taxing more would need a design decision from the requester. It cannot be done inside the token without breaking the required exact v4 flows.","line":92,"path":"src/ShiftToken.sol","reproduction":"State: token deployed by the factory with poolManager_ = PM. The factory sends Alice 1,000,000e18 (exempt). Alice, from her own contract's unlockCallback: PM.sync(SHIFT); SHIFT.transfer(PM, 1_000_000e18) [to==PM, exempt]; PM.settle() [credits +1_000_000e18 to Alice]; PM.take(SHIFT, Bob, 1_000_000e18) [from==PM, exempt]. Expected under the stated rule (a direct ordinary transfer): Bob gets 990_000e18 and totalSupply falls by 10_000e18. Actual: Bob gets 1_000_000e18 and totalSupply is unchanged. I checked this locally with a scratch Foundry test against a minimal model of the PoolManager's sync/settle/take accounting at the POOL_MANAGER address. It passed (bob == 1_000_000e18, supply unchanged).","severity":"low","snippet":"        if (burned == 0 || isLaunchExempt(from) || isLaunchExempt(to)) {","title":"The 1% burn can be avoided on any wallet-to-wallet transfer by routing it through the v4 PoolManager's flash accounting"}],"hash":"2fa3bffedd91b9073a4dc82d48284cc71b4b629c9028d5ae36f2f235b76ac254","nodeId":"a78787fb-cf69-4cf6-8e8f-5f6df8d6a6bc","outcome":"completed","summary":"I found one low-severity defect in `ShiftToken`: anyone can skip the 1% burn on a wallet-to-wallet transfer by routing it through the Uniswap v4 PoolManager. Nothing else in my area failed. The findings are in `.imd-findings.json` and no other file was changed. My scratch test was deleted and `git status` is clean.\n\n**Finding 1 (low): the burn can be bypassed through the PoolManager** (`src/ShiftToken.sol:92`, together with the exemption at line 74)\n- **Cause:** a transfer is untaxed if either side is the PoolManager. Inside one v4 `unlock`, Alice can call `sync`, transfer to the PoolManager, call `settle`, then call `take` to send to Bob. Both legs are exempt and no pool or swap is needed.\n- **Reproduction:** Alice moves 1,000,000e18 this way. Bob receives the full 1,000,000e18 and total supply is unchanged. A direct transfer would deliver 990,000e18 and burn 10,000e18.\n- **Check:** a scratch Foundry test passed, but against a simple stand-in for the PoolManager's `sync`/`settle`/`take`. The project doesn't vendor v4-core, so I couldn't run it against the real PoolManager.\n- **Fix:** removing the exemption isn't an option, because the launch rules require buys and sells through the PoolManager to arrive whole. The minimal fix is to document that all v4 trades and settle/take round trips burn nothing. Taxing more would need a design decision from the requester.\n\n**Coverage:** I covered all three entry points through the Execution Trace, Periphery and First Principles guides, plus two invariants:\n- **`approve`:** holds; it is OpenZeppelin's version, unchanged.\n- **`transfer` and `transferFrom`:** finding 1. Otherwise both behave as intended:\n  - the fee rounds down;\n  - a short balance reverts with the full amount before anything is burned;\n  - `transferFrom` spends allowance for the full amount;\n  - a factory with no code, or a `distributorOf` lookup that fails, just means the transfer is taxed rather than blocked.\n- **Supply never grows after construction:** holds. There is no mint path, no owner, and no `delegatecall` or `selfdestruct`.\n- **Launch flows arrive whole:** holds for factory → distributor, distributor → claimant, the pool seed, and buys and sells.\n\n**Limits:**\n- I didn't run the separate launch tests the network applies before admission, because they need its environment.\n- I couldn't confirm the real factory's `distributorOf` signature (the token calls it with a `uint64`; a `uint256` version would not match). The test probe uses `uint64`.\n- The existing 31 tests all pass.","treeHash":null,"usage":{"cachedInputTokens":498057,"inputTokens":20,"model":"claude-opus-5-5","outputTokens":10008,"runtime":"claude","turns":10,"wallClockMs":129552}},{"artifacts":[],"attempt":1,"bundleHash":"e1159ac4f7d7cbf83cbaff3595b40c1ed80b0e2a832ca60312dfac13babcb0a1","device":"1d91ef3b034fced4","findings":[],"hash":"5cfef36c017773a14b097967a11c70faedd992118038bedecee1080a1afc4452","nodeId":"24e24cd0-29b2-45ae-a165-fa9e4617195b","outcome":"completed","summary":"Created [launch.json](/home/seat/.identitymd/work/75b42be0-4125-47c5-b027-db140597cd4c/24e24cd0-29b2-45ae-a165-fa9e4617195b/launch.json) matching `ShiftToken`, its constructor arguments, exact supply, and required pool and economics.\n\nManifest checks and `forge build` passed. All 31 tests passed with the specified fuzz seed. Only `launch.json` is included in the deliverable.","treeHash":"9c66bb8fb8e60e957fe6c79cc443815a4cda1efc","usage":{"cachedInputTokens":196096,"inputTokens":27512,"model":"gpt-6-astra","outputTokens":2666,"runtime":"codex","turns":3,"wallClockMs":56694}},{"artifacts":[],"attempt":1,"bundleHash":"8151c752b8abd299ef2be41b0593872163cf31d72060f60cc292046d15ae70f2","device":"2dc755dfe7bd177c","findings":[],"hash":"699f565508ed54ad8bf2fd2c7de80a5617a6481e63e00fffc19c0545ba7d1fc5","nodeId":"24e24cd0-29b2-45ae-a165-fa9e4617195b","outcome":"completed","summary":"The revision is complete. Only launch.json changed.\n\n**What I checked**\n- The accepted token is `src/ShiftToken.sol`: constructor takes factory, poolManager and launchNumber in that order, and mints 1,000,000,000 SHIFT with 18 decimals once to msg.sender. The manifest's token block, constructorArgs order and totalSupply of 1000000000000000000000000000 match it exactly.\n- No application contracts exist in the tree, so contracts stays empty.\n- The pool block and the economics object are verbatim from the brief, with lowercase addresses and only schema keys present.\n\n**What I changed**\n- The notes previously said transfers to or from the pool manager are exempt from the burn. The source exempts the PoolManager only as a recipient, so buys paid out by it are taxed. I rewrote that sentence to describe the accepted behavior, and added that a failed distributorOf lookup exempts no distributor. Notes are now 940 characters.\n\n**Verification**\n- A JSON check confirmed kind, totalSupply, decimals, economics, pool, contract-name shape and notes length.\n- `forge build` succeeded with lint warnings only.\n- `forge test` with the verifier's fuzz seed passed all 36 tests.","treeHash":"50ad4bd0eee27f8680a48d1b296aa3f56da06a01","usage":{"cachedInputTokens":201305,"inputTokens":162,"model":"claude-fable-5-1","outputTokens":2651,"runtime":"claude","turns":6,"wallClockMs":60609}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"76df9026b98e4277","findings":[{"citation":"resolved","description":"Math precision / zero-rounding (boundary x precision seam). burnFor truncates (value*100)/10000 toward zero, and _update (line 92, `if (burned == 0 || ...`) then delivers the whole transfer untaxed. Any ordinary transfer of 1..99 wei burns nothing, which contradicts the brief's 1% burn on every transaction, and every value that is not a multiple of 100 burns less than 1%. At value = 199 the effective rate is 0.50%. The fee is the protocol's take, so the checklist's 'fees round UP' rule applies: rounding favours the sender. Impact is economically tiny. Moving 1 SHIFT (1e18 wei) fee-free takes about 1.01e16 transfers of 99 wei, so splitting is not a profitable way to dodge the fee, and the severity is low. Fix that keeps the design: round up, burned = (value * BURN_BPS + BPS_DENOMINATOR - 1) / BPS_DENOMINATOR, or Math.mulDiv(value, BURN_BPS, BPS_DENOMINATOR, Math.Rounding.Ceil), keeping the `value == 0` passthrough. Otherwise document that sub-100-wei transfers are unburned.","line":81,"path":"src/ShiftToken.sol","reproduction":"Deploy through a factory mock (FACTORY=f, POOL_MANAGER=0x9001, launch 7), and have f transfer 1e18 to A=0xA11CE (exempt, so it arrives whole). Then: (1) vm.prank(A); token.transfer(B=0xB0B, 99). Expected under a 1% burn on every transfer: 1 wei burned (rounded up). Actual: B receives 99 and totalSupply is unchanged (burned = 9900/10000 = 0). (2) vm.prank(A); token.transfer(B, 199). Expected: 2 wei burned (1%, rounded up). Actual: totalSupply drops by 1 (0.50%). (3) 100 transfers of 99 wei move 9,900 wei and burn 0, while one transfer of 9,900 burns 99. All three ran as a scratch Foundry test against this tree and passed, which confirms the behaviour.","severity":"low","snippet":"        return (value * BURN_BPS) / BPS_DENOMINATOR;","title":"Burn fee rounds down to zero for transfers below 100 wei and under-collects on every non-multiple of 100"},{"citation":"resolved","description":"Overflow intermediate. burnFor computes value * BURN_BPS before it divides, so for value > type(uint256).max / 100 the checked multiplication panics. _update calls burnFor at line 91, before the exemption test and before the up-front balance check at line 99, whose comment says it exists 'so the revert reports `value`'. For these values the revert is an arithmetic panic, not ERC20InsufficientBalance. This also hits exempt senders (factory, PoolManager, distributor), whose transfers should be plain ERC-20 passthroughs, and the public view burnFor(type(uint256).max) panics instead of returning a fee. No balance can come near 1.15e75 (supply is 1e27), so no transfer that would otherwise succeed fails. The defect is the wrong revert reason for integrators and wallets, and a view that is not total over its input. Fix: compute the fee as value / BPS_DENOMINATOR * BURN_BPS + (value % BPS_DENOMINATOR) * BURN_BPS / BPS_DENOMINATOR, or Math.mulDiv, or call burnFor only after the exemption and balance checks.","line":91,"path":"src/ShiftToken.sol","reproduction":"Same setup as finding 1. (a) token.burnFor(type(uint256).max): expected a fee value, actual revert Panic(0x11). (b) vm.prank(A /* holds 1e18 */); token.transfer(B, type(uint256).max): expected ERC20InsufficientBalance(A, 1e18, type(uint256).max), actual Panic(0x11). (c) vm.prank(factory); token.transfer(B, type(uint256).max / 100 + 1): expected ERC20InsufficientBalance from the untaxed OZ path, actual Panic(0x11). All three were confirmed with vm.expectRevert(stdError.arithmeticError) in a scratch Foundry test.","severity":"info","snippet":"        uint256 burned = burnFor(value);","title":"burnFor overflows before the balance check, so oversized transfers revert with Panic(0x11) instead of ERC20InsufficientBalance, including from exempt senders"}],"hash":"9d9cc7783c0e3582ba518e6553dde8275f5dc3816502b2c4d15ec483e83661d5","nodeId":"7a013006-3c15-44ae-a81e-83bcab4d39f6","outcome":"completed","summary":"The math audit of `src/ShiftToken.sol` found two minor issues and nothing critical, high or medium. Both reproduced in a scratch Foundry test, which I then deleted. The existing suite passes (31 tests), and `.imd-findings.json` holds both findings plus a coverage record.\n\n**Findings**\n1. **Low: the 1% burn rounds down, to zero on small transfers** (`src/ShiftToken.sol:81`, `return (value * BURN_BPS) / BPS_DENOMINATOR;`).\n   - An ordinary transfer of 99 wei burns nothing, which breaks \"1% burn on every transaction\".\n   - A transfer of 199 wei burns 1 wei, an effective rate of 0.5%.\n   - 100 transfers of 99 wei burn 0, where one transfer of 9,900 wei burns 99.\n   - Splitting is not a practical way to avoid the fee: moving 1 SHIFT fee-free would take about 10¹⁶ transfers.\n   - Fix: round the fee up (ceil division or `Math.mulDiv` with `Rounding.Ceil`).\n2. **Info: very large amounts fail with the wrong error** (`src/ShiftToken.sol:91`).\n   - `burnFor(value)` multiplies before it divides, so any value above `type(uint256).max / 100` overflows and reverts with a generic arithmetic error.\n   - That happens before the balance check, whose comment says it exists so the revert reports `value`. So `transfer(B, type(uint256).max)` fails with the arithmetic error instead of the usual \"insufficient balance\" error.\n   - It also hits the exempt launch addresses (factory, pool manager, distributor), and the public `burnFor(type(uint256).max)` view reverts too.\n   - No transfer that should succeed fails, since no balance can get near that size (supply is 10²⁷).\n\n**Coverage rows**\n- **`transfer` and `transferFrom`:** both have findings (they share the same internal path).\n  - These hold: the sender is debited exactly `value` and the recipient gets `value - burned`, and the balances always add up to the total supply, including self-transfers and zero-value transfers.\n  - `transferFrom` spends allowance for the full `value` before transferring, as documented.\n- **`approve`:** holds; it is unmodified OpenZeppelin code with no arithmetic.\n- **Invariants:** the supply can never grow after the constructor, and transfers touching the factory, pool manager or distributor move exactly the stated amount.\n\n**Outside my area**\n- **Not run:** I could not test anything against Uniswap v4, because `lib/` contains only forge-std and OpenZeppelin.\n- **Unproven lead, not reported:** because every transfer to or from the pool manager is exempt, a holder could probably send tokens through it to another wallet without the burn. The launch rules require that exemption, so I treated it as a design consequence.\n\nNo repository files were changed.","treeHash":null,"usage":{"cachedInputTokens":410997,"inputTokens":18,"model":"claude-opus-5-5","outputTokens":8239,"runtime":"claude","turns":10,"wallClockMs":101956}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"759c614fdc84ff66","findings":[],"hash":"b34c339cecb928aa95d5376cf693e92e9feaefc818526d921473ca6e304a1a3a","nodeId":"2492ef41-890d-4775-ba18-a83f2a54f391","outcome":"completed","summary":"I'm reporting no new findings this round: three of my four earlier findings are fixed, and I've dropped the rounding note. `.imd-findings.json` holds `{\"findings\":[]}` and a coverage record that marks all three entry points `holds`.\n\nThe full suite passes (69 tests, including the invariant runs). I also ran the attached proof and my own scratch tests against the new code, then deleted the scratch files; no other file changed.\n\n**My earlier findings:**\n- **[medium] Moving SHIFT through the PoolManager avoided the burn — fixed.** The PoolManager is now exempt only when it receives SHIFT (`isExemptRecipient`, `src/ShiftToken.sol:93-95`). What it pays out to an ordinary account is taxed 1%. The proof `Proof_0f2688bd2dc9.t.sol` now passes: Bob gets 990e18 and the supply drops by 10e18. I checked this against the launch floor in `Token.protected.t.sol`:\n  - **Seed:** comes from the factory, so it arrives whole.\n  - **Sell:** goes to the PoolManager, so it arrives whole and settles the exact amount.\n  - **Buy:** arrives 1% short. The floor only requires `bought > 0` and then sells the trader's whole balance, so it still passes.\n  - **Payouts to launch addresses:** the PoolManager paying the factory or the distributor is still whole.\n  - **PoolManager accounting:** it is debited the full amount on payout, so its balance bookkeeping is unchanged.\n  - **What's left:** passing SHIFT claims between holders inside the PoolManager still burns nothing, but taking them out is taxed, and the README now says so.\n- **[low] A malformed distributor reply froze ordinary transfers — fixed.** `launchDistributor()` (lines 75-76) now returns the zero address when the reply's first word exceeds `type(uint160).max`. With a dirty word, a dirty word plus extra data, or a 1-byte reply, the transfer goes through and is taxed. Nothing reverts.\n- **[info] Huge amounts reverted with an overflow panic — fixed.** `burnFor` now uses `Math.mulDiv`, so `burnFor(max)` returns `max / 100`. A transfer of `max` now reverts with `ERC20InsufficientBalance(A, 1000e18, max)`. The opcode scan is still clean (no DELEGATECALL, CALLCODE or SELFDESTRUCT), and the deployed code is under 24,576 bytes.\n- **[info] The burn rounds down — dropped.** The author is right. It reproduces exactly as described, but it's intended, documented in NatSpec and the README, and pinned by a test. Splitting a transfer saves at most 1 wei of burn each time, so there's nothing to exploit.\n- **[info] Trust in the factory's `distributorOf` — settled.** This is a trust assumption, not a code defect. The README's Assumptions section now records it.\n\nThe specialists' findings all have one of these root causes, so nothing new comes from them. Slither reported nothing. Aderyn's large-number note is about the supply constants, which are correct. `launch.json` has the right constructor arguments and supply, and its notes describe the PoolManager change accurately.","treeHash":null,"usage":{"cachedInputTokens":393514,"inputTokens":14,"model":"claude-opus-5-5","outputTokens":6606,"runtime":"claude","turns":7,"wallClockMs":97465}},{"artifacts":[],"attempt":1,"bundleHash":"ab013f3cfe9cd74c9af2410db3979585bfe6418ed6c142a39e85e6a92c79959e","device":"1ca477e8d9b58040","findings":[],"hash":"b582f0eb71b6429bc65e450de461a6023b5ab522c65061b96911ca784d2c2cdc","nodeId":"676065b4-1316-416f-a531-936399f3f157","outcome":"completed","summary":"I've updated my two test files to match the revised implementation, and all 68 tests now pass with `forge build` and `forge test` under the verifier's fuzz seed. I found no defects, so I didn't write `.imd-findings.json`.\n\n**What broke.** Another contributor changed `src/ShiftToken.sol` after my last round in two ways:\n- The PoolManager is now exempt from the burn only when it receives tokens. When it pays tokens out (a buy, or a `take`), the 1% burn applies.\n- `burnFor` now uses `Math.mulDiv`, so it gives an answer for every value instead of reverting on very large ones.\n\nFour of my tests still expected the old behaviour and failed. I changed only what the new rules required.\n\n**`test/ShiftToken.invariant.t.sol`**\n- The handler now works out the expected burn from the rules themselves rather than calling the token's own exemption checks. Transfers to or from the factory or the named distributor are free, transfers to the PoolManager are free, and everything else burns 1% rounded down.\n- New handler action `roundTripThroughPoolManager`: a sell into the PoolManager arrives in full, and buying the same amount back burns 1%.\n- All 5 invariants pass with failing on any revert turned on.\n\n**`test/ShiftToken.edge.t.sol`**\n- **Fixed tests:**\n  - The zero-distributor check now asserts the PoolManager is exempt only as a recipient.\n  - The \"overspend from an exempt sender\" test now uses the distributor instead of the PoolManager.\n  - The overflow test became `test_burnForIsTotalUpToTheMaximum`, and the fuzzed fee check now runs over the full number range.\n  - The fuzzed exempt-counterparty test no longer treats the PoolManager as an exempt sender.\n- **New tests:**\n  - **PoolManager payouts:** a payout to an ordinary holder is taxed (99% arrives, one `FeeBurned` event); a payout to the factory or distributor arrives in full; a PoolManager self-transfer burns nothing.\n  - **Exemption follows the address, not the caller:** a `transferFrom` between two holders is still taxed when the PoolManager is the spender.\n  - **No cheaper route through the pool (fuzzed):** sending tokens into the PoolManager and back out to someone else costs exactly the same as a direct transfer.\n  - **Corrupted distributor reply (fuzzed):** if the factory's answer has extra high bits set, no distributor is exempt.\n- **New failure-path tests:** the PoolManager paying out more than it holds, a holder selling more than they hold, and a transfer of `type(uint256).max` all revert with the full requested amount.\n\nI also checked the new design against the launch test in the protected files: the seed and sells arrive in full, and a buy still delivers more than zero tokens, so that test's requirements still hold.\n\nThe changes are in the working tree and not committed. `forge build` prints only lint notes, no errors.","treeHash":"4acfd9bcc01bf20ea49bc812a1f46279958109f4","usage":{"cachedInputTokens":604658,"inputTokens":24,"model":"claude-opus-5-5","outputTokens":10455,"runtime":"claude","turns":13,"wallClockMs":134771}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"644eb561f6d70a85","findings":[{"citation":"resolved","description":"Merged from write_foundry_tests, audit_economics, audit_permissions and audit_flow (same root cause, four reports). The brief's only economic rule is a 1% burn on every transaction. isLaunchExempt (line 74: `if (account == FACTORY || account == POOL_MANAGER) return true;`) exempts the Uniswap v4 PoolManager by address in both directions, and _update skips the burn whenever either side is exempt. The v4 PoolManager is a permissionless singleton: inside unlock() anyone may sync(SHIFT), transfer SHIFT in, settle() for a credit equal to what arrived, and take(SHIFT, anyRecipient, amount). Both legs touch the PoolManager, so value moves between two ordinary holders with nothing burned, and no pool or swap is involved. Holders can also mint ERC-6909 claims for SHIFT inside the manager and pass them around indefinitely. The same exemption makes every buy from the launch pool burn-free. Only transfers INTO the PoolManager must arrive whole for the launch floor (seed and sell settle an exact delta; Token.protected.t.sol test_theSeedAndASwapEachWaySucceed). A payout FROM the PoolManager may be taxed: the floor's buy check is only `bought > 0` and it sells exactly balanceOf(trader) afterwards, and take() does no post-transfer balance check. Impact: the token's one feature is optional for any contract account (router, exchange, OTC desk, smart wallet) at the cost of one unlock, so the deflation other holders are promised does not apply to routed volume. No funds are stolen or frozen, so medium (broken stated guarantee), not high. The README's Assumptions say the recipient-side exemption is intentional but do not disclose that it makes the burn avoidable. Fix options for the requester: (a) exempt POOL_MANAGER only as recipient, i.e. tax from == POOL_MANAGER when `to` is not exempt, which closes the settle/take route and taxes buys while keeping seed and sells exact; the accepted test test_poolManagerFlowsAreNotTaxedEitherWay (asserts a buy arrives whole) must then be updated; or (b) keep the design and state in README/notes that transfers routed through the PoolManager (all v4 trades and settle/take round trips, and ERC-6909 claims) burn nothing. The attached proof encodes option (a).","line":92,"path":"src/ShiftToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ShiftToken} from \"src/ShiftToken.sol\";\n\ncontract BypassFactory {\n    mapping(uint64 => address) public distributorOf;\n\n    function deploy(address poolManager, uint64 launchNumber) external returns (ShiftToken) {\n        return new ShiftToken(address(this), poolManager, launchNumber);\n    }\n\n    function move(ShiftToken token, address to, uint256 amount) external returns (bool) {\n        return token.transfer(to, amount);\n    }\n}\n\n/// @notice Two ordinary holders move value through the Uniswap v4 PoolManager the way its flash\n/// accounting allows (holder transfers in and `settle`s, then `take`s to the recipient). Both legs\n/// touch the PoolManager, so neither is taxed: the 1% burn on every transaction is skipped. Only\n/// transfers *into* the PoolManager need to arrive whole for sells and the seed to settle; a\n/// payout *from* it (a buy or a `take`) can be taxed like any other transfer.\ncontract PoolManagerBypassTest is Test {\n    address constant POOL_MANAGER = address(0x9001);\n    address constant ALICE = address(0xA11CE);\n    address constant BOB = address(0xB0B);\n\n    function test_routingThroughThePoolManagerStillBurnsOnePercent() public {\n        BypassFactory factory = new BypassFactory();\n        ShiftToken token = factory.deploy(POOL_MANAGER, 1);\n        factory.move(token, ALICE, 1_000e18);\n        uint256 supplyBefore = token.totalSupply();\n\n        // sync + transfer + settle: Alice's tokens enter the PoolManager.\n        vm.prank(ALICE);\n        token.transfer(POOL_MANAGER, 1_000e18);\n        // take: the PoolManager pays the credit out to Bob.\n        vm.prank(POOL_MANAGER);\n        token.transfer(BOB, 1_000e18);\n\n        assertEq(token.balanceOf(BOB), 990e18, \"Bob received Alice's value with no burn\");\n        assertEq(token.totalSupply(), supplyBefore - 10e18, \"the 1% burn was skipped\");\n    }\n}","reproduction":"State: ShiftToken deployed by factory F with POOL_MANAGER = 0x9001, launch 1; F transfers 1000e18 to ALICE (exempt, arrives whole); supplyBefore = totalSupply(). Calls (the token sees exactly these two transfers when the real PoolManager runs settle and take): vm.prank(ALICE); token.transfer(POOL_MANAGER, 1000e18) -> isLaunchExempt(to) true at line 92, 1000e18 arrives whole. vm.prank(POOL_MANAGER); token.transfer(BOB, 1000e18) -> isLaunchExempt(from) true, whole. Expected under '1% burn every transaction' (what a direct ALICE->BOB transfer of 1000e18 does): BOB = 990e18, totalSupply = supplyBefore - 10e18. Actual: BOB = 1000e18, totalSupply unchanged. Ran the specialist's proof test/scratch/Proof_51f327b59d35.t.sol on this tree: FAIL 'Bob received Alice's value with no burn: 1000000000000000000000 != 990000000000000000000'. The three specialists who modelled unlock/sync/settle/take at the PoolManager address report the same result; the token's exemption is purely address-based so vm.prank(POOL_MANAGER) is equivalent to take().","severity":"medium","snippet":"        if (burned == 0 || isLaunchExempt(from) || isLaunchExempt(to)) {","title":"PoolManager is exempt as sender too, so anyone moves SHIFT burn-free through v4 unlock/sync/settle/take (and every buy is untaxed)"},{"citation":"resolved","description":"Merged from write_foundry_tests (info), audit_economics (info, part 2 of its trust note) and audit_permissions (low). NatSpec lines 24-25 and the README promise that if the distributor lookup fails the transfer is simply taxed, so the token never bricks on a factory problem. The guard at line 68 covers `!ok` and `data.length < 32` only. A successful call returning a first word with any of the top 96 bits set (a different return type at that selector, a proxy fallback echoing calldata, an upgraded factory that changes distributorOf's shape) makes abi.decode(data, (address)) revert with a decoding panic. isLaunchExempt runs inside _update for every transfer between two non-factory, non-PoolManager accounts, so every ordinary transfer and transferFrom reverts for as long as the factory answers that way; launchDistributor() itself reverts instead of returning address(0). Flows from or to FACTORY and POOL_MANAGER are short-circuited at line 74 before the lookup, so the launch itself and pool trading survive. Reachability needs the factory (IMD's, which returns a typed address) to misbehave, hence low. Minimal fix preserving behaviour: decode the word as uint256 and return address(0) when it exceeds type(uint160).max, keeping the existing short-data guard.","line":69,"path":"src/ShiftToken.sol","reproduction":"State: factory F whose fallback answers distributorOf(uint64) with abi.encode(bytes32(uint256(1) << 200)); token = F.deploy(0x9001, 7) (minted to F); F.move(token, A, 100e18) succeeds (exempt path). Call: vm.prank(A); token.transfer(B, 10e18). Expected per NatSpec: B receives 9.9e18 and 0.1e18 burns (lookup treated as failed). Actual: revert (abi.decode panic inside launchDistributor); token.launchDistributor() also reverts instead of returning address(0). Control: F then answers bytes32(uint256(uint160(0x5E7))), launchDistributor() == 0x5E7 and the same transfer succeeds with B = 9.9e18. Verified in test/scratch/Judge.t.sol::test_dirtyDistributorReplyBricksOrdinaryTransfers (passes, i.e. the reverts happen as described).","severity":"low","snippet":"        return abi.decode(data, (address));","title":"launchDistributor() reverts on a 32-byte reply that is not a clean address, bricking every ordinary transfer despite the 'never bricks on a factory problem' guarantee"},{"citation":"resolved","description":"Merged from write_foundry_tests (info) and audit_math (low). burnFor truncates value*100/10000, and the `burned == 0` fast path at line 92 then delivers the whole amount. Any transfer of 1..99 wei burns nothing and e.g. 199 wei burns 1 wei (0.50%). This deviates from a literal '1% on every transaction', but it is stated in NatSpec (line 14, 'rounded down') and in the README ('Amounts under 100 minor units burn nothing'), and at 18 decimals it is not exploitable: splitting saves at most 1 wei of burn per extra transfer, which costs tens of thousands of gas. Recalibrated to info: documented, intended, and economically nil. If the requester wants the rule to hold literally, round up (Math.mulDiv(value, BURN_BPS, BPS_DENOMINATOR, Math.Rounding.Ceil)) while keeping the value == 0 passthrough.","line":81,"path":"src/ShiftToken.sol","reproduction":"State: token deployed by factory F (POOL_MANAGER 0x9001, launch 7); F transfers 1e18 to A; s = totalSupply(). (1) vm.prank(A); transfer(B, 99): expected under a literal 1% rule 1 wei burned; actual B = 99, totalSupply == s. (2) vm.prank(A); transfer(B, 199): expected 2 wei (1% rounded up); actual totalSupply == s - 1, B = 297. Verified in test/scratch/Judge.t.sol::test_roundingBelow100Wei; also covered by the accepted test_oneWeiAndNinetyNineWeiNeverBurn.","severity":"info","snippet":"        return (value * BURN_BPS) / BPS_DENOMINATOR;","title":"Burn rounds down: transfers under 100 wei burn nothing and non-multiples of 100 burn less than 1% (documented)"},{"citation":"resolved","description":"From audit_math (info). burnFor computes value * BURN_BPS before dividing, so for value > type(uint256).max / 100 the checked multiplication panics. _update calls burnFor at line 91 before the exemption test and before the up-front balance check at line 99 whose comment says it exists 'so the revert reports value'. For those values the revert is an arithmetic panic rather than ERC20InsufficientBalance(from, balance, value), including for exempt senders whose transfers should be plain OZ passthroughs, and the public view burnFor is not total over its input. No balance can approach 1.15e75 (supply 1e27), so no transfer that would otherwise succeed fails; the defect is only the revert reason. The accepted test test_burnForOverflowsOnlyBeyondAnyReachableAmount already pins this behaviour. Fix if wanted: Math.mulDiv, or value / BPS_DENOMINATOR * BURN_BPS + (value % BPS_DENOMINATOR) * BURN_BPS / BPS_DENOMINATOR, or call burnFor after the balance check.","line":91,"path":"src/ShiftToken.sol","reproduction":"State: token deployed by factory F; F transfers 1e18 to A. (a) token.burnFor(type(uint256).max): expected a fee, actual Panic(0x11). (b) vm.prank(A); transfer(B, type(uint256).max): expected ERC20InsufficientBalance(A, 1e18, max), actual Panic(0x11). (c) vm.prank(F); transfer(B, type(uint256).max / 100 + 1): expected ERC20InsufficientBalance from the untaxed OZ path, actual Panic(0x11). Control: vm.prank(A); transfer(B, type(uint256).max / 100) reverts ERC20InsufficientBalance(A, 1e18, max/100) as documented. Verified in test/scratch/Judge.t.sol::test_overflowPanicInsteadOfInsufficientBalance with vm.expectRevert(stdError.arithmeticError).","severity":"info","snippet":"        uint256 burned = burnFor(value);","title":"burnFor overflows before the balance check, so transfers above type(uint256).max/100 revert with Panic(0x11) instead of the documented ERC20InsufficientBalance"},{"citation":"resolved","description":"Merged from audit_permissions (info) and audit_economics (info, part 1). Not a defect in the requester's code; recorded as the token's one external trust boundary, as the Pashov adapter asks. The token has no owner, mint, pause, blocklist or setter (constructor sets only immutables; the only state-changing entry points are OZ approve/transfer/transferFrom; the runtime opcode scan passes). The sole mutable input to the exemption model is the factory's answer to distributorOf(LAUNCH_NUMBER), read on every taxed transfer. Whoever can change that answer (an upgradeable or admin-controlled factory) can name any address as a burn-free hub in both directions, or un-name the real distributor so contributor claims arrive 1% short. Each taxed transfer also makes up to two staticcalls into the factory (about 5-6k gas each). The factory is IMD's and outside this repository. Suggested README Assumptions line: 'the factory's distributorOf(launchNumber) is correct and does not change after launch'.","line":75,"path":"src/ShiftToken.sol","reproduction":"State: token deployed by a factory F with a settable mapping (test/ShiftToken.t.sol MockFactory pattern); F transfers 100e18 to A; s = totalSupply(); F.setDistributor(7, X) for arbitrary X after launch. vm.prank(A); transfer(X, 100e18) -> X holds 100e18 (no burn). vm.prank(X); transfer(B, 100e18) -> B holds 100e18, totalSupply == s. Expected if only the real distributor were exempt: 1% burned on each hop. Verified in test/scratch/Judge.t.sol::test_factoryRepointsExemptHubLive. This is the lookup behaving as designed; listed so the trust boundary is visible.","severity":"info","snippet":"        address distributor = launchDistributor();","title":"Trust assumption: the factory's live distributorOf(LAUNCH_NUMBER) answer names a third burn-exempt hub at every transfer"}],"hash":"cde17ce25d69dbfccccbe300cdeb56965b4f8d6c276c0f7177b49b4f931fe5cb","nodeId":"2492ef41-890d-4775-ba18-a83f2a54f391","outcome":"completed","summary":"The review is complete. The findings file is at `.imd-findings.json`, the working tree is otherwise clean, and the scratch tests are gone.\n\n**Outcome.** The specialists' eleven reports collapse into five findings, each reproduced on this tree. Nothing new surfaced in my own pass with the auditor checklists.\n\n- **Medium, merged from four areas:** the PoolManager is burn-exempt as sender as well as recipient. Anyone can route a holder-to-holder transfer through v4 unlock, sync, settle and take, and both legs skip the burn, so the token's single feature is optional for any contract account. Every buy from the launch pool is also untaxed. The specialist's proof fails on this code with Bob receiving 1000e18 instead of 990e18, so I kept it on the finding. The fix the proof encodes taxes payouts from the PoolManager while keeping seed and sells exact, which the protected floor allows. The accepted test asserting a buy arrives whole would need updating, or the requester can instead document the bypass.\n- **Low, merged from three areas:** a 32-byte distributorOf reply with dirty upper bits makes the decode revert, which blocks every ordinary transfer and contradicts the NatSpec promise that a factory problem only means the transfer is taxed. Verified with a shaped factory. Launch flows survive because they short-circuit before the lookup.\n- **Info, merged from two areas:** rounding down below 100 wei. Documented in NatSpec and README, not exploitable at 18 decimals, so I lowered audit_math's low to info.\n- **Info:** the fee multiplication panics before the balance check for amounts above roughly 1.15e75, giving the wrong revert reason. Unreachable by any balance.\n- **Info, merged from two areas:** the factory's live distributorOf answer is the token's one trust boundary and can name an arbitrary burn-free hub. Recorded as a trust assumption, not a defect.\n\n**Coverage.** All three entry points are answered: approve holds, transfer and transferFrom point to finding 1. Four extra rows record that supply is minted once and cannot grow, the launch flows arrive whole, the CREATE2 deployment model needs nothing the manifest does not give, and launch.json is schema-valid with the correct supply and argument order.\n\n**Dropped.** The aderyn large-literal note is cosmetic and was left out.","treeHash":null,"usage":{"cachedInputTokens":626618,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":19874,"runtime":"claude","turns":17,"wallClockMs":288758}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa4b91aed45c94fa","findings":[{"citation":"resolved","description":"Economic Security / Flow Gap (execution x periphery x first principles). The token's only economic feature is 'a 1% burn on every transaction'. isLaunchExempt (src/ShiftToken.sol:73-77) exempts the PoolManager as sender AND as recipient, and _update (line 92) skips the burn whenever either side is exempt. The Uniswap v4 PoolManager is a permissionless singleton: anyone may call unlock(), and inside the callback may sync(currency), transfer tokens to the manager, settle() to be credited exactly what arrived, and take(currency, to, amount) to send that credit to ANY address, with no swap and no pool involved. Both hops of that route are exempt here (to == POOL_MANAGER on the way in, from == POOL_MANAGER on the way out), so the net effect is a plain holder-to-holder transfer with zero burn. The same manager also lets a holder mint ERC-6909 claim tokens for SHIFT and pass those around burn-free indefinitely. Consequence: the burn applies only to naive wallet-to-wallet ERC-20 transfers; any holder, router, CEX hot wallet or OTC desk can avoid it for the cost of one unlock. The README (Assumptions) says recipient exemption is 'intentional, so sells into the pool settle exactly' but does not say it makes the burn optional, and the deflation that other holders are promised ('the supply can only shrink' by 1% per transfer) does not materialise for routed volume. Who loses: every holder who priced SHIFT on the stated burn; who gains: whoever routes. No funds are stolen, so medium (broken stated guarantee) rather than high. Fix tradeoff (scope decision for the requester): the recipient-side PoolManager exemption is required by the launch floor (a sell settles the exact delta, so trader->PoolManager must arrive whole), so the bypass cannot be closed without failing the sell test. Options: (a) accept it and state in the README/notes that the burn covers only direct ERC-20 transfers between non-launch addresses and is avoidable via the PoolManager and ERC-6909 claims; or (b) change the design so the burn is applied on buys only (from == POOL_MANAGER taxed, to == POOL_MANAGER exempt), which keeps sells exact but taxes take() payouts and buys, which again requires the requester's agreement because the floor's buy test only requires 'bought > 0'.","line":92,"path":"src/ShiftToken.sol","reproduction":"State: ShiftToken deployed with POOL_MANAGER = the v4 PoolManager; Alice holds 1,000e18 SHIFT (received from the factory, so whole). Direct path: Alice calls transfer(Bob, 500e18) -> Bob +495e18, totalSupply -5e18 (expected per spec). Routed path: Alice (or a helper contract she deploys and funds) calls PoolManager.unlock(data); in unlockCallback: PoolManager.sync(SHIFT); SHIFT.transfer(PoolManager, 495e18) [to == POOL_MANAGER -> isLaunchExempt(to) true -> no burn, 495e18 arrives]; PoolManager.settle() -> delta +495e18; PoolManager.take(SHIFT, Bob, 495e18) [PoolManager calls SHIFT.transfer(Bob, 495e18): from == POOL_MANAGER -> no burn] -> delta 0, unlock returns. Expected per the stated rule: Bob receives 490.05e18 and 4.95e18 is burned. Actual: Bob receives 495e18, totalSupply unchanged, PoolManager balance unchanged. Verified with test/scratch/BurnBypassViaPoolManager.t.sol (a PoolManager model implementing unlock/sync/settle/take with the real manager's rules: anyone may unlock, take pays any recipient, net delta must be zero): test_anyoneMovesShiftBurnFreeThroughThePoolManager passes, asserting balanceOf(Bob) grew by exactly 495e18 and totalSupply did not change, while burnFor(495e18) == 4.95e18. The same sequence against the real v4 PoolManager (v4-core PoolManager.unlock/sync/settle/take, all permissionless when unlocked) behaves identically because the token's exemption is purely address-based.","severity":"medium","snippet":"        if (burned == 0 || isLaunchExempt(from) || isLaunchExempt(to)) {\n            super._update(from, to, value);\n            return;\n        }","title":"The 1% burn is bypassable by anyone: routing a transfer through the exempt Uniswap v4 PoolManager (settle + take, no swap) delivers it whole"},{"citation":"resolved","description":"Documented as a privileged-power note, not a defect in the requester's code (the factory is IMD's). Two consequences of reading the exemption live from the factory, for the record: (1) whatever address the factory reports for LAUNCH_NUMBER at any later time is exempt in both directions, so if the factory can re-point distributorOf after launch, it can grant a burn-free address at will; (2) launchDistributor guards !ok and short returndata but not a 32-byte word whose upper 12 bytes are non-zero: abi.decode(data,(address)) then reverts inside the token, and because isLaunchExempt is called for every taxed transfer between two non-factory, non-PoolManager accounts, every such transfer (and transferFrom) reverts until the factory answers correctly. Transfers from or to the factory or the PoolManager still work (lines 74 and 92 short-circuit before the call), so the launch flows and pool trading are unaffected. Also a cost note: each taxed transfer makes up to two staticcalls into the factory (one per side), roughly 5-6k gas each.","line":66,"path":"src/ShiftToken.sol","reproduction":"State: factory contract whose distributorOf(uint64) returns abi.encode(uint256(1) << 200) (32 bytes, not a clean address). Alice holds 100e18 (funded by the factory, exempt). Call: Alice transfer(Bob, 100e18). Expected: Bob receives 99e18. Actual: burned = 1e18 != 0, isLaunchExempt(Alice) -> launchDistributor() -> staticcall ok, data.length == 32 -> abi.decode(data,(address)) reverts (dirty upper bits) -> the transfer reverts. With a well-behaved factory (returns a clean address or reverts) the transfer succeeds as intended; the precondition is factory misbehaviour, hence info.","severity":"info","snippet":"        (bool ok, bytes memory data) =\n            factory.staticcall(abi.encodeWithSignature(\"distributorOf(uint64)\", LAUNCH_NUMBER));\n        if (!ok || data.length < 32) return address(0);\n        return abi.decode(data, (address));","title":"Trust assumption: the factory's distributorOf(launchNumber) answer is read live on every taxed transfer and decides a third exempt address; a malformed answer reverts every ordinary transfer"}],"hash":"efef1486ee39d058baeab76eb7b12a82102d162b11127999610a841dcda68e22","nodeId":"d8c8e36a-6852-4775-a7c1-256e7a75369f","outcome":"completed","summary":"Both reproductions hold. The review is complete and `.imd-findings.json` is written; the tree is otherwise untouched (scratch tests live under the gitignored `test/scratch/`).\n\n**Outcome.** The NIGHTSHIFT token's burn arithmetic, conservation invariant, supply monotonicity and launch-flow exemptions all check out under the Economic Security, Invariant and Flow Gap guides. One medium finding and one info-level trust note are recorded.\n\n**Finding 1, medium.** The 1% burn can be avoided by anyone. Because the PoolManager is exempt as both sender and recipient, a holder can call the v4 PoolManager's permissionless unlock, sync, transfer in, settle, then `take` to any address, with no swap. Both hops are exempt, so the recipient gets the full amount and nothing burns. ERC-6909 claim tokens inside the manager give the same effect permanently. The repro in `test/scratch/BurnBypassViaPoolManager.t.sol` shows 495e18 SHIFT arriving whole where the rule says 4.95e18 should burn. This cannot be closed without failing the launch floor's sell test, so the fix is a scope decision: document it, or move the burn to the buy side with the requester's agreement.\n\n**Finding 2, info.** The distributor exemption is read live from the factory on every taxed transfer. A factory answer whose upper bytes are dirty makes `abi.decode` revert, which blocks every ordinary transfer while launch flows keep working. Verified in `test/scratch/DirtyDistributorWord.t.sol`. Precondition is factory misbehaviour, so it is a trust assumption, not a defect.\n\n**Coverage.** All three listed entry points have rows: `approve` holds, `transfer` and `transferFrom` reference finding 1. Five extra rows record the invariants and leads I checked, including the aderyn literal lead, which is style only. The existing 31 project tests pass.","treeHash":null,"usage":{"cachedInputTokens":624195,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":20637,"runtime":"claude","turns":23,"wallClockMs":300599}},{"artifacts":[],"attempt":1,"bundleHash":"76767c034d492f75cb8576b5893f1ae521ad39521336f630cb2cc8bcc37dd99c","device":"89eed35842d609d7","findings":[],"hash":"f0d85ba05d89f2d1091a36a30c856408f58949d6b02e9fafbcb630f267013f04","nodeId":"b0725736-8415-4a45-ab0b-917abfa807cf","outcome":"completed","summary":"The NIGHTSHIFT (SHIFT) token is implemented, tested, documented and committed. A clean export of the commit builds, passes all tests with the verifier's fuzz seed, and passes the format check, all offline.\n\n**What was built**\n\n- `src/ShiftToken.sol`: an OpenZeppelin v5 ERC-20 named NIGHTSHIFT, symbol SHIFT, 18 decimals. The constructor mints the full 1,000,000,000 supply once to the deployer and makes no external calls.\n- Every ordinary transfer burns 1% of the amount, rounded down. The sender pays the gross amount, the recipient receives the net, and the supply shrinks. The rate is a constant, so there is no owner and no setter.\n- Transfers touching the factory, the Uniswap v4 PoolManager, or the launch's MerkleDistributor pass through whole. The distributor is read from the factory at transfer time, and a failing lookup just means the transfer is taxed normally.\n- No mint, pause, blocklist, burnFrom, delegatecall or selfdestruct exists, so the supply can only fall and no privileged hand can move a holder's balance.\n\n**Checks**\n\n| Check | Result |\n| --- | --- |\n| forge build (solc 0.8.26, bytecode_hash none) | pass |\n| forge test with the verifier's fuzz seed | 31 passed, 0 failed |\n| forge fmt --check | pass |\n\nThe tests cover the burn on both transfer paths, rounding and zero amounts, each launch flow with the distributor unset and set, factory-as-EOA and reverting-lookup robustness, the revert paths, the absence of admin entry points, and an opcode scan of the runtime.\n\n**One thing to know**\n\nThe sandbox pinned a read-only `.gitmodules` inside the raw OpenZeppelin clone, so that directory is git-ignored and the project builds against a clean copy under `lib/openzeppelin/`. Dependencies are committed as ordinary files, and the commit contains no gitlinks.\n\nThe README documents the burn rule, the three constructor arguments the manifest fills, the fee-on-transfer caveat for integrators, and that nothing needs configuring after launch.","treeHash":"2bab6fe8cc86d1f76a2c8277a32dc229b608f311","usage":{"cachedInputTokens":1391122,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":26539,"runtime":"claude","turns":23,"wallClockMs":411528}}],"verification":[{"checks":[{"durationMs":1586,"exitCode":0,"name":"build","output":"Compiling 28 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.51s\nCompiler run successful!\n","passed":true},{"durationMs":2409,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 31 tests for test/ShiftToken.t.sol:ShiftTokenTest\n[PASS] testFuzz_ordinaryTransferConservesValue(uint256,uint256) (runs: 256, μ: 209298, ~: 214753)\nLogs:\n  Bound result 2352015141698317165726549\n  Bound result 2191015344265908250969603\n\n[PASS] testFuzz_transferFromMatchesTransfer(uint256) (runs: 256, μ: 325014, ~: 328589)\nLogs:\n  Bound result 92306272079895674980887854\n\n[PASS] test_burnCompoundsAcrossHops() (gas: 214776)\n[PASS] test_burnRoundsDownAndTinyTransfersPassWhole() (gas: 277829)\n[PASS] test_claimantsAreTaxedWhenTheySpend() (gas: 240358)\n[PASS] test_constructorMintsToWhoeverDeploys() (gas: 14565)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3961)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 6133)\n[PASS] test_distributorClaimsArriveWholeOnceTheFactoryNamesIt() (gas: 210273)\n[PASS] test_distributorOfAnotherLaunchIsNotExempt() (gas: 202687)\n[PASS] test_factoryCannotPullFromAHolderWithoutAllowance() (gas: 106271)\n[PASS] test_factoryTransfersAreNotTaxed() (gas: 73927)\n[PASS] test_metadata() (gas: 63185)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 17389)\n[PASS] test_noMintOrAdminEntryPointExists() (gas: 388454)\n[PASS] test_ordinaryTransfersWorkWhenDistributorLookupReverts() (gas: 414637)\n[PASS] test_ordinaryTransfersWorkWhenTheFactoryIsAnEOA() (gas: 137251)\n[PASS] test_poolManagerFlowsAreNotTaxedEitherWay() (gas: 191802)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 1221637)\n[PASS] test_selfTransferStillBurns() (gas: 132285)\n[PASS] test_supportsIERC20Surface() (gas: 201056)\n[PASS] test_transferBurnsOnePercent() (gas: 179555)\n[PASS] test_transferFromBurnsOnePercentAndSpendsFullAllowance() (gas: 218010)\n[PASS] test_transferFromWithAllowanceShortOfGrossReverts() (gas: 146923)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 99672)\n[PASS] test_transferRevertsWhenBalanceIsShortByTheFeeAlone() (gas: 107698)\n[PASS] test_transferRevertsWhenBalanceIsShortReportingTheGrossAmount() (gas: 119123)\n[PASS] test_transferToZeroAddressReverts() (gas: 97042)\n[PASS] test_transfersIntoTheFactoryAreNotTaxed() (gas: 123509)\n[PASS] test_unnamedDistributorIsAnOrdinaryAccount() (gas: 172310)\n[PASS] test_zeroValueTransferIsANoOp() (gas: 121493)\nSuite result: ok. 31 passed; 0 failed; 0 skipped; finished in 36.07ms (63.29ms CPU time)\n\nRan 22 tests for test/ShiftToken.edge.t.sol:ShiftTokenEdgeTest\n[PASS] testFuzz_burnForIsFloorOfOnePercent(uint256) (runs: 1000, μ: 9714, ~: 9680)\nLogs:\n  Bound result 9116\n\n[PASS] testFuzz_exemptCounterpartyMovesWhole(uint256,uint8,bool) (runs: 1000, μ: 236111, ~: 258617)\nLogs:\n  Bound result 2168703874092214831232\n\n[PASS] testFuzz_failedTransferChangesNothing(uint256,uint256) (runs: 1000, μ: 134714, ~: 135197)\nLogs:\n  Bound result 199171404788741643513269829\n  Bound result 495\n\n[PASS] testFuzz_splittingNeverBurnsMoreThanOneTransfer(uint256,uint256) (runs: 1000, μ: 339538, ~: 349004)\nLogs:\n  Bound result 349709110633902377407561348\n  Bound result 179553096668498222314921917\n\n[PASS] test_approveZeroSpenderReverts() (gas: 30797)\n[PASS] test_burnForOverflowsOnlyBeyondAnyReachableAmount() (gas: 14405)\n[PASS] test_emptyDistributorReplyMeansNoDistributor() (gas: 202579)\n[PASS] test_exemptSelfTransferBurnsNothing() (gas: 48531)\n[PASS] test_exemptSenderCannotOverspend() (gas: 100058)\n[PASS] test_exemptTransfersEmitNoFeeBurned() (gas: 226735)\n[PASS] test_factoryCannotSendMoreThanItHolds() (gas: 36298)\n[PASS] test_infiniteAllowanceIsNotSpentByATaxedTransferFrom() (gas: 200760)\n[PASS] test_longDistributorReplyReadsTheFirstWord() (gas: 191927)\n[PASS] test_oneWeiAndNinetyNineWeiNeverBurn() (gas: 172363)\n[PASS] test_ordinaryHolderCannotSendIntoTheFactoryMoreThanHeld() (gas: 108950)\n[PASS] test_repointedDistributorLosesItsExemption() (gas: 301431)\n[PASS] test_shortDistributorReplyMeansNoDistributor() (gas: 214015)\n[PASS] test_taxedTransferEmitsExactlyOneFeeBurned() (gas: 146666)\n[PASS] test_transferFromRevertsOnShortBalanceEvenWithAllowance() (gas: 173277)\n[PASS] test_transferFromToZeroAddressReverts() (gas: 150103)\n[PASS] test_wholeSupplyMovedBetweenOrdinaryHolders() (gas: 145617)\n[PASS] test_zeroDistributorNeverExemptsTheZeroAddress() (gas: 67044)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 43.27ms (166.29ms CPU time)\n\nRan 1 test for test/ShiftToken.invariant.t.sol:ShiftTokenInvariantTest\n[PASS]\nShiftTokenInvariantTest invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_immutablesAndMetadataFixed\n[PASS] invariant_nothingParkedAtZero\n[PASS] invariant_supplyNeverGrows\n[PASS] invariant_supplyPlusBurnedIsTheInitialSupply\n ShiftTokenInvariantTest invariants (runs: 256, calls: 16384, reverts: 0)\n\n╭--------------+---------------------------+-------+---------+----------╮\n| Contract     | Selector                  | Calls | Reverts | Discards |\n+=======================================================================+\n| ShiftHandler | approveAndTransferFrom    | 2787  | 0       | 0        |\n|--------------+---------------------------+-------+---------+----------|\n| ShiftHandler | setDistributor            | 2702  | 0       | 0        |\n|--------------+---------------------------+-------+---------+----------|\n| ShiftHandler | transfer                  | 2757  | 0       | 0        |\n|--------------+---------------------------+-------+---------+----------|\n| ShiftHandler | transferFromOverAllowance | 2710  | 0       | 0        |\n|--------------+---------------------------+-------+---------+----------|\n| ShiftHandler | transferTooMuch           | 2718  | 0       | 0        |\n|--------------+---------------------------+-------+---------+----------|\n| ShiftHandler | tryPrivileged             | 2710  | 0       | 0        |\n╰--------------+---------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 11000000000000000000\n  Bound result 9941\n  Bound result 396\n  Bound result 8221\n  Bound result 500000000000000000000\n  Bound result 13366\n  Bound result 573032676714468067448238493\n  Bound result 12540\n  Bound result 905\n  Bound result 185367276341377375426008599\n  Bound result 3\n  Bound result 1243\n  Bound result 32\n  Bound result 440\n  Bound result 0\n  Bound result 0\n  Bound result 85273767203512531427163808\n  Bound result 2131\n  Bound result 7295\n  Bound result 3630\n  Bound result 10334\n  Bound result 18975384082176821\n  Bound result 2471\n  Bound result 100\n  Bound result 207096469469853525917840\n  Bound result 12732\n  Bound result 7865\n  Bound result 118610282464636918077616410\n  Bound result 5\n  Bound result 253806804661\n  Bound result 0\n  Bound result 990000000000000000000\n  Bound result 4202047189\n  Bound result 12868\n  Bound result 280641043525914522770993115\n  Bound result 3182\n  Bound result 495790613316\n  Bound result 6715\n  Bound result 4\n  Bound result 3328\n  Bound result 12327\n  Bound result 999999999999999999999\n  Bound result 212637138912080978759507491\n  Bound result 7\n  Bound result 1000\n  Bound result 18947972775366140\n  Bound result 3998\n  Bound result 2960\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.33s (2.33s CPU time)\n\nRan 3 test suites in 2.33s (2.41s CPU time): 54 tests passed, 0 failed, 0 skipped (54 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShiftToken.approve(address,uint256)\",\"ShiftToken.transfer(address,uint256)\",\"ShiftToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":11,\"README.md\":115,\"foundry.toml\":17,\"remappings.txt\":2,\"src/ShiftToken.sol\":104,\"test/ShiftToken.edge.t.sol\":312,\"test/ShiftToken.invariant.t.sol\":300,\"test/ShiftToken.t.sol\":439},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"0add812f13d5d228c5f864eb502c794646acfb8b6948c8ec5b41a2ea7284d30d","verifiedTreeHash":"8505c1afdb3c2e85769be31a90f898c422c34d02","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":1107,"exitCode":0,"name":"build","output":"Compiling 29 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.02s\nCompiler run successful!\n","passed":true},{"durationMs":106,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 36 tests for test/ShiftToken.t.sol:ShiftTokenTest\n[PASS] testFuzz_ordinaryTransferConservesValue(uint256,uint256) (runs: 256, μ: 210221, ~: 214961)\nLogs:\n  Bound result 2352015141698317165726549\n  Bound result 2191015344265908250969603\n\n[PASS] testFuzz_transferFromMatchesTransfer(uint256) (runs: 256, μ: 325199, ~: 328818)\nLogs:\n  Bound result 2522\n\n[PASS] test_burnCompoundsAcrossHops() (gas: 214894)\n[PASS] test_burnForIsTotalAndHugeTransfersRevertWithInsufficientBalance() (gas: 147392)\n[PASS] test_burnRoundsDownAndTinyTransfersPassWhole() (gas: 278055)\n[PASS] test_claimantsAreTaxedWhenTheySpend() (gas: 240509)\n[PASS] test_constructorMintsToWhoeverDeploys() (gas: 14549)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3983)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 6111)\n[PASS] test_distributorClaimsArriveWholeOnceTheFactoryNamesIt() (gas: 210417)\n[PASS] test_distributorOfAnotherLaunchIsNotExempt() (gas: 202749)\n[PASS] test_factoryCannotPullFromAHolderWithoutAllowance() (gas: 106327)\n[PASS] test_factoryTransfersAreNotTaxed() (gas: 74053)\n[PASS] test_metadata() (gas: 63229)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 17395)\n[PASS] test_noMintOrAdminEntryPointExists() (gas: 388658)\n[PASS] test_ordinaryTransfersWorkWhenDistributorLookupReverts() (gas: 414744)\n[PASS] test_ordinaryTransfersWorkWhenDistributorReplyIsNotAnAddress() (gas: 632831)\n[PASS] test_ordinaryTransfersWorkWhenDistributorReplyIsShort() (gas: 131512)\n[PASS] test_ordinaryTransfersWorkWhenTheFactoryIsAnEOA() (gas: 137425)\n[PASS] test_poolManagerPayoutsToLaunchAddressesArriveWhole() (gas: 236434)\n[PASS] test_routingThroughThePoolManagerStillBurnsOnePercent() (gas: 221237)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 1367638)\n[PASS] test_selfTransferStillBurns() (gas: 132355)\n[PASS] test_supportsIERC20Surface() (gas: 201171)\n[PASS] test_transferBurnsOnePercent() (gas: 179659)\n[PASS] test_transferFromBurnsOnePercentAndSpendsFullAllowance() (gas: 218153)\n[PASS] test_transferFromWithAllowanceShortOfGrossReverts() (gas: 147134)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 99855)\n[PASS] test_transferRevertsWhenBalanceIsShortByTheFeeAlone() (gas: 107828)\n[PASS] test_transferRevertsWhenBalanceIsShortReportingTheGrossAmount() (gas: 119215)\n[PASS] test_transferToZeroAddressReverts() (gas: 97020)\n[PASS] test_transfersIntoTheFactoryAreNotTaxed() (gas: 123700)\n[PASS] test_transfersIntoThePoolManagerAreNotTaxedButPayoutsAre() (gas: 244167)\n[PASS] test_unnamedDistributorIsAnOrdinaryAccount() (gas: 172281)\n[PASS] test_zeroValueTransferIsANoOp() (gas: 121628)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 16.69ms (41.78ms CPU time)\n\nRan 1 test suite in 17.33ms (16.69ms CPU time): 36 tests passed, 0 failed, 0 skipped (36 total tests)\n","passed":true},{"durationMs":36,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShiftToken.approve(address,uint256)\",\"ShiftToken.transfer(address,uint256)\",\"ShiftToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":11,\"README.md\":135,\"foundry.toml\":17,\"remappings.txt\":2,\"src/ShiftToken.sol\":124,\"test/ShiftToken.t.sol\":556},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1077,"exitCode":0,"name":"slither","output":"slither: no results at low impact or above","passed":true},{"durationMs":412,"exitCode":0,"name":"aderyn","output":"[low] large-numeric-literal at src/ShiftToken.sol:35: Large Numeric Literal (2 places)","passed":true},{"durationMs":1261,"exitCode":0,"name":"proof 0f2688bd2dc9","output":"Compiling 29 files with Solc 0.8.26\nSolc 0.8.26 finished in 663.01ms\nCompiler run successful!\n\nRan 1 test for test/imd-proof-286ac6a5/Proof_0f2688bd2dc9.t.sol:PoolManagerBypassTest\n[PASS] test_routingThroughThePoolManagerStillBurnsOnePercent() (gas: 513533)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 517.43µs (386.23µs CPU time)\n\nRan 1 test suite in 4.83ms (517.43µs 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":"1972d262e2825a564067e0a4859770440302423b87f4ecc986981a136505a394","verifiedTreeHash":"0e6086b6b0ecd36623f9299a6146b2d781c461cb","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":1095,"exitCode":0,"name":"build","output":"Compiling 26 files with Solc 0.8.26\nSolc 0.8.26 finished in 999.84ms\nCompiler run successful!\n","passed":true},{"durationMs":102,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 31 tests for test/ShiftToken.t.sol:ShiftTokenTest\n[PASS] testFuzz_ordinaryTransferConservesValue(uint256,uint256) (runs: 256, μ: 209507, ~: 214753)\nLogs:\n  Bound result 366464158841470834193854551\n  Bound result 332968538475637002355\n\n[PASS] testFuzz_transferFromMatchesTransfer(uint256) (runs: 256, μ: 325561, ~: 328589)\nLogs:\n  Bound result 308513473133173095279914380\n\n[PASS] test_burnCompoundsAcrossHops() (gas: 214776)\n[PASS] test_burnRoundsDownAndTinyTransfersPassWhole() (gas: 277829)\n[PASS] test_claimantsAreTaxedWhenTheySpend() (gas: 240358)\n[PASS] test_constructorMintsToWhoeverDeploys() (gas: 14565)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3961)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 6133)\n[PASS] test_distributorClaimsArriveWholeOnceTheFactoryNamesIt() (gas: 210273)\n[PASS] test_distributorOfAnotherLaunchIsNotExempt() (gas: 202687)\n[PASS] test_factoryCannotPullFromAHolderWithoutAllowance() (gas: 106271)\n[PASS] test_factoryTransfersAreNotTaxed() (gas: 73927)\n[PASS] test_metadata() (gas: 63185)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 17389)\n[PASS] test_noMintOrAdminEntryPointExists() (gas: 388454)\n[PASS] test_ordinaryTransfersWorkWhenDistributorLookupReverts() (gas: 414637)\n[PASS] test_ordinaryTransfersWorkWhenTheFactoryIsAnEOA() (gas: 137251)\n[PASS] test_poolManagerFlowsAreNotTaxedEitherWay() (gas: 191802)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 1221637)\n[PASS] test_selfTransferStillBurns() (gas: 132285)\n[PASS] test_supportsIERC20Surface() (gas: 201056)\n[PASS] test_transferBurnsOnePercent() (gas: 179555)\n[PASS] test_transferFromBurnsOnePercentAndSpendsFullAllowance() (gas: 218010)\n[PASS] test_transferFromWithAllowanceShortOfGrossReverts() (gas: 146923)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 99672)\n[PASS] test_transferRevertsWhenBalanceIsShortByTheFeeAlone() (gas: 107698)\n[PASS] test_transferRevertsWhenBalanceIsShortReportingTheGrossAmount() (gas: 119123)\n[PASS] test_transferToZeroAddressReverts() (gas: 97042)\n[PASS] test_transfersIntoTheFactoryAreNotTaxed() (gas: 123509)\n[PASS] test_unnamedDistributorIsAnOrdinaryAccount() (gas: 172310)\n[PASS] test_zeroValueTransferIsANoOp() (gas: 121493)\nSuite result: ok. 31 passed; 0 failed; 0 skipped; finished in 15.07ms (37.54ms CPU time)\n\nRan 1 test suite in 15.95ms (15.07ms CPU time): 31 tests passed, 0 failed, 0 skipped (31 total tests)\n","passed":true},{"durationMs":39,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShiftToken.approve(address,uint256)\",\"ShiftToken.transfer(address,uint256)\",\"ShiftToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":11,\"README.md\":115,\"foundry.toml\":17,\"launch.json\":24,\"remappings.txt\":2,\"src/ShiftToken.sol\":104,\"test/ShiftToken.t.sol\":439},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"5cfef36c017773a14b097967a11c70faedd992118038bedecee1080a1afc4452","verifiedTreeHash":"9c66bb8fb8e60e957fe6c79cc443815a4cda1efc","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":1088,"exitCode":0,"name":"build","output":"Compiling 29 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.01s\nCompiler run successful!\n","passed":true},{"durationMs":91,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 36 tests for test/ShiftToken.t.sol:ShiftTokenTest\n[PASS] testFuzz_ordinaryTransferConservesValue(uint256,uint256) (runs: 256, μ: 210221, ~: 214961)\nLogs:\n  Bound result 2352015141698317165726549\n  Bound result 2191015344265908250969603\n\n[PASS] testFuzz_transferFromMatchesTransfer(uint256) (runs: 256, μ: 325199, ~: 328818)\nLogs:\n  Bound result 2522\n\n[PASS] test_burnCompoundsAcrossHops() (gas: 214894)\n[PASS] test_burnForIsTotalAndHugeTransfersRevertWithInsufficientBalance() (gas: 147392)\n[PASS] test_burnRoundsDownAndTinyTransfersPassWhole() (gas: 278055)\n[PASS] test_claimantsAreTaxedWhenTheySpend() (gas: 240509)\n[PASS] test_constructorMintsToWhoeverDeploys() (gas: 14549)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3983)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 6111)\n[PASS] test_distributorClaimsArriveWholeOnceTheFactoryNamesIt() (gas: 210417)\n[PASS] test_distributorOfAnotherLaunchIsNotExempt() (gas: 202749)\n[PASS] test_factoryCannotPullFromAHolderWithoutAllowance() (gas: 106327)\n[PASS] test_factoryTransfersAreNotTaxed() (gas: 74053)\n[PASS] test_metadata() (gas: 63229)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 17395)\n[PASS] test_noMintOrAdminEntryPointExists() (gas: 388658)\n[PASS] test_ordinaryTransfersWorkWhenDistributorLookupReverts() (gas: 414744)\n[PASS] test_ordinaryTransfersWorkWhenDistributorReplyIsNotAnAddress() (gas: 632831)\n[PASS] test_ordinaryTransfersWorkWhenDistributorReplyIsShort() (gas: 131512)\n[PASS] test_ordinaryTransfersWorkWhenTheFactoryIsAnEOA() (gas: 137425)\n[PASS] test_poolManagerPayoutsToLaunchAddressesArriveWhole() (gas: 236434)\n[PASS] test_routingThroughThePoolManagerStillBurnsOnePercent() (gas: 221237)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 1367638)\n[PASS] test_selfTransferStillBurns() (gas: 132355)\n[PASS] test_supportsIERC20Surface() (gas: 201171)\n[PASS] test_transferBurnsOnePercent() (gas: 179659)\n[PASS] test_transferFromBurnsOnePercentAndSpendsFullAllowance() (gas: 218153)\n[PASS] test_transferFromWithAllowanceShortOfGrossReverts() (gas: 147134)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 99855)\n[PASS] test_transferRevertsWhenBalanceIsShortByTheFeeAlone() (gas: 107828)\n[PASS] test_transferRevertsWhenBalanceIsShortReportingTheGrossAmount() (gas: 119215)\n[PASS] test_transferToZeroAddressReverts() (gas: 97020)\n[PASS] test_transfersIntoTheFactoryAreNotTaxed() (gas: 123700)\n[PASS] test_transfersIntoThePoolManagerAreNotTaxedButPayoutsAre() (gas: 244167)\n[PASS] test_unnamedDistributorIsAnOrdinaryAccount() (gas: 172281)\n[PASS] test_zeroValueTransferIsANoOp() (gas: 121628)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 12.62ms (35.71ms CPU time)\n\nRan 1 test suite in 13.46ms (12.62ms CPU time): 36 tests passed, 0 failed, 0 skipped (36 total tests)\n","passed":true},{"durationMs":34,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShiftToken.approve(address,uint256)\",\"ShiftToken.transfer(address,uint256)\",\"ShiftToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":11,\"README.md\":135,\"foundry.toml\":17,\"launch.json\":24,\"remappings.txt\":2,\"src/ShiftToken.sol\":124,\"test/ShiftToken.t.sol\":556},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"699f565508ed54ad8bf2fd2c7de80a5617a6481e63e00fffc19c0545ba7d1fc5","verifiedTreeHash":"50ad4bd0eee27f8680a48d1b296aa3f56da06a01","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":1960,"exitCode":0,"name":"build","output":"Compiling 31 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.87s\nCompiler run successful!\n","passed":true},{"durationMs":2512,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 31 tests for test/ShiftToken.edge.t.sol:ShiftTokenEdgeTest\n[PASS] testFuzz_burnForIsFloorOfOnePercent(uint256) (runs: 1000, μ: 6109, ~: 6096)\n[PASS] testFuzz_dirtyDistributorWordMeansNoDistributor(uint256) (runs: 1000, μ: 276250, ~: 275994)\nLogs:\n  Bound result 4147\n\n[PASS] testFuzz_exemptCounterpartyMovesWhole(uint256,uint8,bool) (runs: 1000, μ: 235161, ~: 258868)\nLogs:\n  Bound result 49569\n\n[PASS] testFuzz_failedTransferChangesNothing(uint256,uint256) (runs: 1000, μ: 135103, ~: 135615)\nLogs:\n  Bound result 94044394513996679143671471\n  Bound result 989612951739300933841033602\n\n[PASS] testFuzz_settleAndTakeCostsTheSameAsADirectTransfer(uint256) (runs: 1000, μ: 356560, ~: 362310)\nLogs:\n  Bound result 5\n\n[PASS] testFuzz_splittingNeverBurnsMoreThanOneTransfer(uint256,uint256) (runs: 1000, μ: 341020, ~: 350168)\nLogs:\n  Bound result 160\n  Bound result 35\n\n[PASS] test_approveZeroSpenderReverts() (gas: 30803)\n[PASS] test_burnForIsTotalUpToTheMaximum() (gas: 29236)\n[PASS] test_emptyDistributorReplyMeansNoDistributor() (gas: 202841)\n[PASS] test_exemptSelfTransferBurnsNothing() (gas: 48613)\n[PASS] test_exemptSenderCannotOverspend() (gas: 151458)\n[PASS] test_exemptTransfersEmitNoFeeBurned() (gas: 227061)\n[PASS] test_factoryCannotSendMoreThanItHolds() (gas: 36352)\n[PASS] test_holderCannotSellMoreThanHeldIntoThePoolManager() (gas: 106876)\n[PASS] test_infiniteAllowanceIsNotSpentByATaxedTransferFrom() (gas: 201193)\n[PASS] test_longDistributorReplyReadsTheFirstWord() (gas: 191981)\n[PASS] test_maxValueTransferRevertsWithTheGrossAmount() (gas: 109479)\n[PASS] test_oneWeiAndNinetyNineWeiNeverBurn() (gas: 172710)\n[PASS] test_ordinaryHolderCannotSendIntoTheFactoryMoreThanHeld() (gas: 109239)\n[PASS] test_poolManagerAsSpenderStillTaxesOrdinaryHops() (gas: 199394)\n[PASS] test_poolManagerCannotPayOutMoreThanItHolds() (gas: 112755)\n[PASS] test_poolManagerPayoutIsTaxed() (gas: 169934)\n[PASS] test_poolManagerPayoutToFactoryOrDistributorIsWhole() (gas: 230748)\n[PASS] test_poolManagerSelfTransferBurnsNothing() (gas: 118906)\n[PASS] test_repointedDistributorLosesItsExemption() (gas: 302139)\n[PASS] test_shortDistributorReplyMeansNoDistributor() (gas: 214117)\n[PASS] test_taxedTransferEmitsExactlyOneFeeBurned() (gas: 147070)\n[PASS] test_transferFromRevertsOnShortBalanceEvenWithAllowance() (gas: 173700)\n[PASS] test_transferFromToZeroAddressReverts() (gas: 150203)\n[PASS] test_wholeSupplyMovedBetweenOrdinaryHolders() (gas: 146027)\n[PASS] test_zeroDistributorNeverExemptsTheZeroAddress() (gas: 103873)\nSuite result: ok. 31 passed; 0 failed; 0 skipped; finished in 77.42ms (456.39ms CPU time)\n\nRan 36 tests for test/ShiftToken.t.sol:ShiftTokenTest\n[PASS] testFuzz_ordinaryTransferConservesValue(uint256,uint256) (runs: 256, μ: 209771, ~: 214943)\nLogs:\n  Bound result 366464158841470834193854551\n  Bound result 332968538475637002355\n\n[PASS] testFuzz_transferFromMatchesTransfer(uint256) (runs: 256, μ: 325411, ~: 328818)\nLogs:\n  Bound result 308513473133173095279914380\n\n[PASS] test_burnCompoundsAcrossHops() (gas: 214894)\n[PASS] test_burnForIsTotalAndHugeTransfersRevertWithInsufficientBalance() (gas: 147392)\n[PASS] test_burnRoundsDownAndTinyTransfersPassWhole() (gas: 278055)\n[PASS] test_claimantsAreTaxedWhenTheySpend() (gas: 240509)\n[PASS] test_constructorMintsToWhoeverDeploys() (gas: 14549)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3983)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 6111)\n[PASS] test_distributorClaimsArriveWholeOnceTheFactoryNamesIt() (gas: 210417)\n[PASS] test_distributorOfAnotherLaunchIsNotExempt() (gas: 202749)\n[PASS] test_factoryCannotPullFromAHolderWithoutAllowance() (gas: 106327)\n[PASS] test_factoryTransfersAreNotTaxed() (gas: 74053)\n[PASS] test_metadata() (gas: 63229)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 17395)\n[PASS] test_noMintOrAdminEntryPointExists() (gas: 388658)\n[PASS] test_ordinaryTransfersWorkWhenDistributorLookupReverts() (gas: 414744)\n[PASS] test_ordinaryTransfersWorkWhenDistributorReplyIsNotAnAddress() (gas: 632831)\n[PASS] test_ordinaryTransfersWorkWhenDistributorReplyIsShort() (gas: 131512)\n[PASS] test_ordinaryTransfersWorkWhenTheFactoryIsAnEOA() (gas: 137425)\n[PASS] test_poolManagerPayoutsToLaunchAddressesArriveWhole() (gas: 236434)\n[PASS] test_routingThroughThePoolManagerStillBurnsOnePercent() (gas: 221237)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 1367638)\n[PASS] test_selfTransferStillBurns() (gas: 132355)\n[PASS] test_supportsIERC20Surface() (gas: 201171)\n[PASS] test_transferBurnsOnePercent() (gas: 179659)\n[PASS] test_transferFromBurnsOnePercentAndSpendsFullAllowance() (gas: 218153)\n[PASS] test_transferFromWithAllowanceShortOfGrossReverts() (gas: 147134)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 99855)\n[PASS] test_transferRevertsWhenBalanceIsShortByTheFeeAlone() (gas: 107828)\n[PASS] test_transferRevertsWhenBalanceIsShortReportingTheGrossAmount() (gas: 119215)\n[PASS] test_transferToZeroAddressReverts() (gas: 97020)\n[PASS] test_transfersIntoTheFactoryAreNotTaxed() (gas: 123700)\n[PASS] test_transfersIntoThePoolManagerAreNotTaxedButPayoutsAre() (gas: 244167)\n[PASS] test_unnamedDistributorIsAnOrdinaryAccount() (gas: 172281)\n[PASS] test_zeroValueTransferIsANoOp() (gas: 121628)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 77.45ms (92.45ms CPU time)\n\nRan 1 test for test/ShiftToken.invariant.t.sol:ShiftTokenInvariantTest\n[PASS]\nShiftTokenInvariantTest invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_immutablesAndMetadataFixed\n[PASS] invariant_nothingParkedAtZero\n[PASS] invariant_supplyNeverGrows\n[PASS] invariant_supplyPlusBurnedIsTheInitialSupply\n ShiftTokenInvariantTest invariants (runs: 256, calls: 16384, reverts: 0)\n\n╭--------------+-----------------------------+-------+---------+----------╮\n| Contract     | Selector                    | Calls | Reverts | Discards |\n+=========================================================================+\n| ShiftHandler | approveAndTransferFrom      | 2336  | 0       | 0        |\n|--------------+-----------------------------+-------+---------+----------|\n| ShiftHandler | roundTripThroughPoolManager | 2261  | 0       | 0        |\n|--------------+-----------------------------+-------+---------+----------|\n| ShiftHandler | setDistributor              | 2290  | 0       | 0        |\n|--------------+-----------------------------+-------+---------+----------|\n| ShiftHandler | transfer                    | 2377  | 0       | 0        |\n|--------------+-----------------------------+-------+---------+----------|\n| ShiftHandler | transferFromOverAllowance   | 2450  | 0       | 0        |\n|--------------+-----------------------------+-------+---------+----------|\n| ShiftHandler | transferTooMuch             | 2319  | 0       | 0        |\n|--------------+-----------------------------+-------+---------+----------|\n| ShiftHandler | tryPrivileged               | 2351  | 0       | 0        |\n╰--------------+-----------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 99999999999999999999999991\n  Bound result 600000000000000000000\n  Bound result 386187990948554765220583041\n  Bound result 3230\n  Bound result 1414\n  Bound result 980100000000000000000\n  Bound result 198000000000000000000\n  Bound result 37\n  Bound result 990000000000000000000\n  Bound result 55934\n  Bound result 0\n  Bound result 7\n  Bound result 1000000000000000001\n  Bound result 0\n  Bound result 990000000000000000000\n  Bound result 4903321771384594144408365\n  Bound result 55934\n  Bound result 198\n  Bound result 4259\n  Bound result 11913\n  Bound result 7820\n  Bound result 38463463159565989992548700\n  Bound result 7918\n  Bound result 659918\n  Bound result 484831055485704507179226835\n  Bound result 1574\n  Bound result 160\n  Bound result 6\n  Bound result 199\n  Bound result 999999999999999999999\n  Bound result 152\n  Bound result 2827\n  Bound result 8713\n  Bound result 213575167073203251842827375\n  Bound result 61453\n  Bound result 18\n  Bound result 26\n  Bound result 4999390198999999999944065\n  Bound result 88\n  Bound result 110\n  Bound result 34753142575902379834914\n  Bound result 6758\n  Bound result 5\n  Bound result 100\n  Bound result 500000000000000000000\n  Bound result 963\n  Bound result 88\n  Bound result 420\n  Bound result 88\n  Bound result 198\n  Bound result 104226394697333617445\n  Bound result 489773605302666376991\n  Bound result 99000000000000000000\n  Bound result 9900000000000000000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.42s (2.42s CPU time)\n\nRan 3 test suites in 2.42s (2.57s CPU time): 68 tests passed, 0 failed, 0 skipped (68 total tests)\n","passed":true},{"durationMs":43,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShiftToken.approve(address,uint256)\",\"ShiftToken.transfer(address,uint256)\",\"ShiftToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":11,\"README.md\":135,\"foundry.toml\":17,\"remappings.txt\":2,\"src/ShiftToken.sol\":124,\"test/ShiftToken.edge.t.sol\":428,\"test/ShiftToken.invariant.t.sol\":319,\"test/ShiftToken.t.sol\":556},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"b582f0eb71b6429bc65e450de461a6023b5ab522c65061b96911ca784d2c2cdc","verifiedTreeHash":"4acfd9bcc01bf20ea49bc812a1f46279958109f4","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":832,"exitCode":0,"name":"build","output":"Compiling 26 files with Solc 0.8.26\nSolc 0.8.26 finished in 766.66ms\nCompiler run successful!\n","passed":true},{"durationMs":85,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 31 tests for test/ShiftToken.t.sol:ShiftTokenTest\n[PASS] testFuzz_ordinaryTransferConservesValue(uint256,uint256) (runs: 256, μ: 209507, ~: 214753)\nLogs:\n  Bound result 2352015141698317165726549\n  Bound result 2191015344265908250969603\n\n[PASS] testFuzz_transferFromMatchesTransfer(uint256) (runs: 256, μ: 325561, ~: 328589)\nLogs:\n  Bound result 2261\n\n[PASS] test_burnCompoundsAcrossHops() (gas: 214776)\n[PASS] test_burnRoundsDownAndTinyTransfersPassWhole() (gas: 277829)\n[PASS] test_claimantsAreTaxedWhenTheySpend() (gas: 240358)\n[PASS] test_constructorMintsToWhoeverDeploys() (gas: 14565)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3961)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 6133)\n[PASS] test_distributorClaimsArriveWholeOnceTheFactoryNamesIt() (gas: 210273)\n[PASS] test_distributorOfAnotherLaunchIsNotExempt() (gas: 202687)\n[PASS] test_factoryCannotPullFromAHolderWithoutAllowance() (gas: 106271)\n[PASS] test_factoryTransfersAreNotTaxed() (gas: 73927)\n[PASS] test_metadata() (gas: 63185)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 17389)\n[PASS] test_noMintOrAdminEntryPointExists() (gas: 388454)\n[PASS] test_ordinaryTransfersWorkWhenDistributorLookupReverts() (gas: 414637)\n[PASS] test_ordinaryTransfersWorkWhenTheFactoryIsAnEOA() (gas: 137251)\n[PASS] test_poolManagerFlowsAreNotTaxedEitherWay() (gas: 191802)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 1221637)\n[PASS] test_selfTransferStillBurns() (gas: 132285)\n[PASS] test_supportsIERC20Surface() (gas: 201056)\n[PASS] test_transferBurnsOnePercent() (gas: 179555)\n[PASS] test_transferFromBurnsOnePercentAndSpendsFullAllowance() (gas: 218010)\n[PASS] test_transferFromWithAllowanceShortOfGrossReverts() (gas: 146923)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 99672)\n[PASS] test_transferRevertsWhenBalanceIsShortByTheFeeAlone() (gas: 107698)\n[PASS] test_transferRevertsWhenBalanceIsShortReportingTheGrossAmount() (gas: 119123)\n[PASS] test_transferToZeroAddressReverts() (gas: 97042)\n[PASS] test_transfersIntoTheFactoryAreNotTaxed() (gas: 123509)\n[PASS] test_unnamedDistributorIsAnOrdinaryAccount() (gas: 172310)\n[PASS] test_zeroValueTransferIsANoOp() (gas: 121493)\nSuite result: ok. 31 passed; 0 failed; 0 skipped; finished in 13.12ms (33.23ms CPU time)\n\nRan 1 test suite in 13.84ms (13.12ms CPU time): 31 tests passed, 0 failed, 0 skipped (31 total tests)\n","passed":true},{"durationMs":36,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShiftToken.approve(address,uint256)\",\"ShiftToken.transfer(address,uint256)\",\"ShiftToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":11,\"README.md\":115,\"foundry.toml\":17,\"remappings.txt\":2,\"src/ShiftToken.sol\":104,\"test/ShiftToken.t.sol\":439},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":447,"exitCode":0,"name":"slither","output":"slither: no results at low impact or above","passed":true},{"durationMs":196,"exitCode":0,"name":"aderyn","output":"[low] large-numeric-literal at src/ShiftToken.sol:30: Large Numeric Literal (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f0d85ba05d89f2d1091a36a30c856408f58949d6b02e9fafbcb630f267013f04","verifiedTreeHash":"2bab6fe8cc86d1f76a2c8277a32dc229b608f311","verifierVersion":"0.1.0+c3fba9eb"}]}