{"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":"6c2edf25-ed78-446b-97a0-e4e8aa9898ea","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"36afcadaecf495ccbc937f023285ab6aff2afc907e3959d8a53c83c52674758f","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":"f144e272c103473cef49cce1e8d698fdbe5388fbe2ca92803eceb15208346dd1","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":"8703002485875c28e08597b80e7ff5b0a5f16e8b820f4d9cd5147ee47bad2be8","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"70532d92cc7e1de476f2db080d7e84159e4ede059b6ac9e623f083ccd63903cb","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":"216fddb7905da2b67e7bf1029f44b4e55e4ab0aef687224f54cfb854b53254fa","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":"f088be11e941d5371f542739081f76417220452bb5f9fb69776ca68a8a75d363","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b6503de65ad02f845827887c23da4c3b56ccc5df7a459db263bae6e549d92f7f","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"b6503de65ad02f845827887c23da4c3b56ccc5df7a459db263bae6e549d92f7f","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"a0de981d38d5db51eec1690d9128a57159ed08117f48fb05c84df183b419edcd","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"efe2ef7cd616c0c184d0534a4a43788dc91292ebf25f6fa5358910de3d5e92ed","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"A custom token: Identity (ID).\nToken name: Identity\nToken symbol: ID\nToken supply: 1,000,000,000 with 18 decimals, all minted once to the deployer in the constructor.\nWhat it does: Pay 8% fee to the deployer","parentJobId":null,"planHash":"376e91e4de27ef348e0d44928dd88ad595600640c6abda06dd1f34953c8aa2c0","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"6c2edf25-ed78-446b-97a0-e4e8aa9898ea","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-747-identity"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"50989","feedbackHash":"3a88a494fbee37885411f1704b0f52d98c5096dc2621b6a9cd84e8a28d363124","nodeKey":"audit_economics","submissionHash":"36afcadaecf495ccbc937f023285ab6aff2afc907e3959d8a53c83c52674758f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52182","feedbackHash":"01f2edc28cbc7daa790197281635e3bd75373a36f425b001fbd3048f29ad2cb0","nodeKey":"audit_flow","submissionHash":"f144e272c103473cef49cce1e8d698fdbe5388fbe2ca92803eceb15208346dd1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51557","feedbackHash":"93d98e0f4addf84851ee24fc4cef6f2f399dbfbfba08d5ce57e215c6075121c5","nodeKey":"audit_judge","submissionHash":"14bf530f2df05ee12f43ddc11954cfa485723f070f757b00648a20cd7d802c25","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51435","feedbackHash":"7c7f11cff57320f4b258f6428e605b1599809b17947bcd91b7c7c276b498b260","nodeKey":"audit_judge","submissionHash":"8703002485875c28e08597b80e7ff5b0a5f16e8b820f4d9cd5147ee47bad2be8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51028","feedbackHash":"69ea3c74d20faea55ef8e594face2651e177f2c60e5fe09bfe1701bc2e01de04","nodeKey":"audit_math","submissionHash":"70532d92cc7e1de476f2db080d7e84159e4ede059b6ac9e623f083ccd63903cb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50960","feedbackHash":"8f5dea457c1bf0d13112ee9d7266daedae5fa1ffe0f97084bb8878c89c54d00c","nodeKey":"audit_permissions","submissionHash":"216fddb7905da2b67e7bf1029f44b4e55e4ab0aef687224f54cfb854b53254fa","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51742","feedbackHash":"74586e5de755373b7c95467a4a9fd3eb87acd80f87589eae1791190a68faacbc","nodeKey":"build_contract_project","submissionHash":"f088be11e941d5371f542739081f76417220452bb5f9fb69776ca68a8a75d363","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51445","feedbackHash":"4daa2fb35679c1b6a41d8cee94d5f26b4ce38caa3b299aee17ea288fcfdde817","nodeKey":"build_contract_project","submissionHash":"cda5835b7e36af78d6bf4020df61960573905062d70a7c0c3c3a44f6f231268a","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51018","feedbackHash":"9c7728466d52b2b655d1482165547f8be4818180483bffe4394de374160b1d17","nodeKey":"manifest","submissionHash":"a0de981d38d5db51eec1690d9128a57159ed08117f48fb05c84df183b419edcd","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50997","feedbackHash":"55be20cd35030e4437775f227d96ac7b10ec2455ea8172126d603ba329d7f8c2","nodeKey":"manifest","submissionHash":"cea81866b4ddaddeaa8aea17440fdd08c4cff379beaadaab3bd2be677c3a3733","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50975","feedbackHash":"fe6a55b49d67724c84c8cf08a0f82e11f2ed04615110d3bccb92a818021ae5cf","nodeKey":"write_foundry_tests","submissionHash":"61a324d935aafaaa5093aa390607ec273efb8ff0492cc18580b8a867cbadaac3","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51024","feedbackHash":"eefefd33420a6fe1ae7778a43856cb9dd962d7d88910297952a0ffadf2ed5b09","nodeKey":"write_foundry_tests","submissionHash":"efe2ef7cd616c0c184d0534a4a43788dc91292ebf25f6fa5358910de3d5e92ed","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"7df369526b250de63ef8001aa71da56631069bdffb363a6d368639f902be108b","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"559cfaaab2c0d013","findings":[{"citation":"resolved","description":"The constructor maps feeRecipient_ == address(0) to msg.sender. Under launchCustom msg.sender is the ProjectFactory. The accepted manifest in this tree (launch.json line 12) sets the fourth constructor argument to \"0x0000000000000000000000000000000000000000\" and its notes state that this selects the factory as fee recipient. The README (lines 55-59) says the manifest must pass the requester's wallet (economics.remainderTo = 0x70bcbde387539d95ffe6d43edbf7c6aa2da87a09) and that a zero recipient 'would send every fee to the factory'; the manifest does exactly that. Consequence: every ordinary transfer after launch credits 8% to the factory contract's balance (line 192). setFeeRecipient (line 113) only accepts calls from the current recipient, and the factory has no code path that calls it or moves ID, so the stream can never be redirected to the requester and every fee ever collected is permanently stuck. The brief's 'Pay 8% fee to the deployer' is realised as 'pay 8% to a contract that cannot spend it'. Nothing in the launch floor notices: the factory is already exempt, so the swarm share, seed and swaps pass. Two defects, one root cause: (a) launch.json must pass economics.remainderTo as constructorArgs[3]; (b) the contract should refuse the silent default when deployed under a launch. Verified fix for (b) on a copy: `if (factory_ != address(0) && feeRecipient_ == address(0)) revert InvalidFeeRecipient();` before line 97; it keeps the deployer default for stand-alone deployments, makes the attached proof pass, and leaves the fee rule and exemption set unchanged. The existing tests that deploy with a non-zero factory and a zero recipient (IdentityTokenInvariant.t.sol setUp, test_deployScriptZeroFeeRecipientMeansTheDeployer) must then pass an explicit recipient. Merged from audit_flow a52b1e39 and audit_economics 57369130, raised from low because the accepted manifest actually ships the zero argument.","line":97,"path":"src/IdentityToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IdentityToken} from \"src/IdentityToken.sol\";\n\n/// @dev Stands in for ProjectFactory: deploys the token (so the constructor's msg.sender is a\n///      contract, as under launchCustom), answers `distributorOf`, and can move tokens. Like the real\n///      factory it has no function that calls `setFeeRecipient` on the token.\ncontract FactoryStub {\n    mapping(uint64 => address) public distributorOf;\n\n    function deployToken(address poolManager, uint64 launchNumber, address feeRecipient)\n        external\n        returns (IdentityToken)\n    {\n        return new IdentityToken(address(this), poolManager, launchNumber, feeRecipient);\n    }\n\n    function move(IdentityToken token, address to, uint256 amount) external returns (bool) {\n        return token.transfer(to, amount);\n    }\n}\n\n/// @notice launch.json in this tree passes the zero address as the fourth constructor argument:\n///         [\"$factory\", \"$poolManager\", \"$launchNumber\", \"0x0000000000000000000000000000000000000000\"].\n///         The constructor maps zero to msg.sender, which under the launch is the ProjectFactory.\n///         Fails on the current code: the 8% stream is bound to the factory, which can neither\n///         spend it nor hand the role to the requester. Passes once the constructor refuses to bind\n///         the fee to a contract deployer (or requires an explicit non-zero recipient).\ncontract ZeroFeeRecipientUnderFactoryTest is Test {\n    uint256 constant SUPPLY = 1_000_000_000e18;\n    uint64 constant LAUNCH = 7;\n    address constant POOL_MANAGER = address(0x9001);\n    address constant REQUESTER = 0x70bcBDE387539d95ffE6d43EDBf7C6AA2dA87A09; // economics.remainderTo\n    address constant ALICE = address(0xA1);\n    address constant BOB = address(0xB0B);\n\n    FactoryStub factory;\n    IdentityToken token;\n\n    function setUp() public {\n        factory = new FactoryStub();\n    }\n\n    function test_factoryDeploymentWithZeroRecipientMustNotStrandTheFee() public {\n        // The exact deployment the manifest produces: factory is msg.sender, recipient argument is zero.\n        try factory.deployToken(POOL_MANAGER, LAUNCH, address(0)) returns (IdentityToken deployed) {\n            token = deployed;\n        } catch {\n            // Fixed: the constructor refuses a zero recipient when deployed by a contract.\n            return;\n        }\n\n        // The launch succeeds and the supply sits with the factory, so nothing in the floor notices.\n        assertEq(token.balanceOf(address(factory)), SUPPLY);\n\n        // First ordinary transfer after launch: 8% is credited to the ProjectFactory.\n        factory.move(token, ALICE, 1_000e18);\n        uint256 factoryBefore = token.balanceOf(address(factory));\n        vm.prank(ALICE);\n        token.transfer(BOB, 1_000e18);\n        uint256 feeToFactory = token.balanceOf(address(factory)) - factoryBefore;\n\n        // The requester (economics.remainderTo) cannot take the role: only the factory could, and it has no such call.\n        vm.prank(REQUESTER);\n        (bool rotated,) = address(token).call(abi.encodeCall(IdentityToken.setFeeRecipient, (REQUESTER)));\n\n        assertTrue(\n            rotated || feeToFactory == 0,\n            \"8% fee stream bound to the ProjectFactory: fee credited to the factory and the requester cannot take the role\"\n        );\n    }\n}","reproduction":"State: a factory contract (FactoryStub, no function that can call setFeeRecipient, as ProjectFactory) runs `new IdentityToken(address(this), 0x9001, 7, address(0))`, the exact shape launch.json produces. Observed: token.feeRecipient() == address(factory). factory.move(token, ALICE, 1_000e18); vm.prank(ALICE); token.transfer(BOB, 1_000e18): BOB = 920e18 and balanceOf(factory) grew by 80e18. vm.prank(0x70bcBDE387539d95ffE6d43EDBf7C6AA2dA87A09); token.setFeeRecipient(requester) reverts NotFeeRecipient(requester). Expected: the requester receives 80e18, or the constructor rejects the argument. Run: forge test --match-path test/scratch/ZeroFeeRecipientUnderFactory.t.sol -> FAIL '8% fee stream bound to the ProjectFactory: fee credited to the factory and the requester cannot take the role'. With the guard above applied on a copy the same test passes (the deployment reverts with InvalidFeeRecipient).","severity":"high","snippet":"        address recipient = feeRecipient_ == address(0) ? msg.sender : feeRecipient_;","title":"launch.json passes a zero fee recipient, so the constructor binds the 8% fee stream to the ProjectFactory, which can neither spend it nor hand the role to the requester"},{"citation":"resolved","description":"isFeeExempt waives the fee whenever either side of a transfer is the PoolManager, so sells (user -> PoolManager) and buys (PoolManager -> user) move whole, which the launch floor requires. The PoolManager is permissionless: inside unlock() any contract may sync(currency), transfer ID to the manager, settle() to be credited, and take(currency, to, amount) to send the same amount to any address, with no pool, hook or swap involved; it may also mint ERC-6909 claims of ID and move them between accounts with no ERC-20 transfer at all. Both legs are exempt, so a wallet-to-wallet transfer of X arrives as X and the fee recipient receives 0 instead of 0.08X. A single helper contract makes this available to every wallet, exchange and bridge at gas cost, contradicting README lines 37-38 ('transfers through intermediaries that take custody ... pay 8%') and the brief's fee rule: the fee becomes opt-in for informed senders and the fee recipient loses that revenue. No holder principal is at risk, hence medium. No token-side change preserves both the floor (seed and swaps must move whole) and the fee, because the token cannot distinguish settle+take from sell+buy; the requester must decide between documenting that the fee applies only to transfers not touching the PoolManager (and correcting the README), or moving fee collection to the pool level via a hook. No proof is attached because the only resolution is a scope decision, not a code fix this test could bind. The reproduction uses an in-file stand-in that copies v4-core's unlock/sync/settle/take accounting because v4-core is not vendored here; the call sequence against the real PoolManager is identical. Reproduced from audit_economics 0e72559c.","line":155,"path":"src/IdentityToken.sol","reproduction":"State: factory deploys the token with poolManager = M and feeRecipient = REQUESTER; factory moves 1_000e18 ID to ALICE. ALICE approves helper H for 1_000e18 and calls H.send(BOB, 1_000e18). H calls M.unlock(data); in unlockCallback: M.sync(ID); token.transferFrom(ALICE, M, 1_000e18) [to == poolManager -> fee 0]; M.settle() [credits +1_000e18 to H]; M.take(ID, BOB, 1_000e18) [M calls token.transfer(BOB, 1_000e18); from == poolManager -> fee 0]; unlock ends with all deltas zero. Expected under the token's rule for two ordinary parties: balanceOf(BOB) = 920e18, balanceOf(REQUESTER) += 80e18. Actual: balanceOf(BOB) = 1_000e18, balanceOf(REQUESTER) += 0, balanceOf(ALICE) = 0, balanceOf(M) unchanged. Run: forge test --match-path test/scratch/Proof_0e72559c6223.t.sol (copy of .imd/reads/proofs/Proof_0e72559c6223.t.sol) -> FAIL 'fee recipient should have received 8% of a wallet-to-wallet transfer: 0 != 80000000000000000000'; the direct-transfer control test in the same file passes (920e18 / 80e18).","severity":"medium","snippet":"        if (from == poolManager || to == poolManager) return true;","title":"Anyone can move ID wallet-to-wallet fee-free through the Uniswap v4 PoolManager's permissionless sync/settle/take surface"},{"citation":"resolved","description":"The guard on line 148 (`!ok || data.length != 32`) only covers call failure and length. A 32-byte word whose upper 12 bytes are non-zero passes it and reaches abi.decode(data, (address)), which in Solidity 0.8.26 reverts when the word does not fit 160 bits. distributor() runs on every transfer that is not short-circuited by the factory, PoolManager or fee-recipient checks (line 158), so the revert propagates and every ordinary transfer and transferFrom reverts (even amount 0) while the factory answers that way; only exempt parties can move tokens. The NatSpec (line 27) and README (lines 40-42) promise that malformed data reads as 'no distributor' and that 'a lookup failure can never brick transfers'. Reachable only if the exempted factory address is a non-conforming contract (distributorOf returning uint256/bytes32, upgraded, or a different contract than assumed); the real factory's mapping getter returns a clean word, so this is a robustness gap in a stated guarantee, not an attack: low. Verified fix on a copy: `uint256 word = abi.decode(data, (uint256)); if (word > type(uint160).max) return address(0); return address(uint160(word));` makes the attached proof and the other three specialist proofs pass with no other test affected. Merged from audit_flow 9195c254, audit_economics 0f217fad, audit_permissions b1b53006, audit_math 0a72c019 and write_foundry_tests fc761528 (same root cause, same fix).","line":149,"path":"src/IdentityToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IdentityToken} from \"src/IdentityToken.sol\";\n\n/// @dev A factory whose `distributorOf` answers with a 32-byte word whose upper 12 bytes are not zero.\n///      The token documents that \"a factory that reverts or returns malformed data is treated as\n///      'no distributor'\" and that \"a lookup failure can never brick transfers\", so ordinary\n///      transfers must keep working (taxed) against this factory.\ncontract DirtyWordFactory {\n    fallback() external {\n        assembly {\n            // 0x000000000000000000000001000000000000000000000000000000000000d157:\n            // a plausible distributor address with one stray bit above bit 160.\n            mstore(0, or(shl(160, 1), 0xd157))\n            return(0, 32)\n        }\n    }\n}\n\ncontract DistributorDirtyWordTest is Test {\n    uint256 constant SUPPLY = 1_000_000_000e18;\n    address constant ALICE = address(0xA1);\n    address constant BOB = address(0xB0B);\n\n    function test_dirtyWordFactoryDoesNotBrickOrdinaryTransfers() public {\n        DirtyWordFactory bad = new DirtyWordFactory();\n        // This test contract is the deployer and the fee recipient.\n        IdentityToken token = new IdentityToken(address(bad), address(0), 1, address(this));\n\n        // Fund Alice fee-free (deployer is the fee recipient, so exempt; no lookup on this path).\n        token.transfer(ALICE, 100e18);\n        assertEq(token.balanceOf(ALICE), 100e18);\n\n        // An ordinary transfer must still go through, taxed at 8%. On the current code it reverts\n        // inside `distributor()` because `abi.decode(data, (address))` rejects the dirty word.\n        vm.prank(ALICE);\n        (bool ok, bytes memory ret) = address(token).call(abi.encodeCall(IdentityToken.transfer, (BOB, 100e18)));\n        assertTrue(ok, \"ordinary transfer reverted: distributor lookup bricked transfers\");\n        assertTrue(abi.decode(ret, (bool)));\n        assertEq(token.balanceOf(BOB), 92e18, \"recipient gets 92%\");\n        assertEq(token.balanceOf(address(this)), SUPPLY - 100e18 + 8e18, \"fee recipient gets 8%\");\n\n        // Documented behaviour: malformed data reads as \"no distributor\".\n        assertEq(token.distributor(), address(0), \"malformed word should read as no distributor\");\n    }\n}","reproduction":"State: a factory whose fallback returns the 32-byte word (1 << 160) | 0xd157 (assembly: mstore(0, or(shl(160, 1), 0xd157)); return(0, 32)). Deploy `new IdentityToken(address(thatFactory), address(0), 1, deployer)`; deployer (fee recipient, exempt) transfers 100e18 to ALICE, which succeeds. vm.prank(ALICE); token.transfer(BOB, 100e18). Expected: BOB = 92e18, fee recipient += 8e18, token.distributor() == address(0). Actual: empty revert inside IdentityToken.distributor at abi.decode; balances unchanged; token.distributor() reverts. transferFrom by an approved spender reverts the same way. Run: forge test --match-path test/scratch/Proof_9195c2549776.t.sol -> FAIL 'ordinary transfer reverted: distributor lookup bricked transfers' (also reproduced by Proof_0f217fadcfcc.t.sol and Proof_b1b53006b7c1.t.sol, 2 tests).","severity":"low","snippet":"        return abi.decode(data, (address));","title":"distributor() reverts on a 32-byte factory answer with non-zero upper bits, so every ordinary transfer reverts against such a factory, contradicting the documented 'malformed data means no distributor"},{"citation":"resolved","description":"The README (line 86) says the setter exists 'so a lost or rotated key does not strand the fee stream forever', but the hand-off writes the new recipient immediately with only a zero check (line 114), and the previous holder loses the role in the same call. A recipient that passes a mistyped address or a contract that cannot call back strands the 8% stream exactly as a lost key would. Self-inflicted by the trusted role, so informational. A two-step pattern (pendingRecipient + acceptFeeRecipient) preserves the design and removes the failure mode. From audit_flow 04d5be55.","line":116,"path":"src/IdentityToken.sol","reproduction":"vm.prank(REQUESTER); token.setFeeRecipient(0x000000000000000000000000000000000000dEaD) succeeds and emits FeeRecipientChanged(REQUESTER, 0xdEaD). Then vm.prank(REQUESTER); token.setFeeRecipient(REQUESTER) reverts NotFeeRecipient(REQUESTER). Every later ordinary transfer credits 8% to 0xdEaD: ALICE transfers 100e18 to BOB -> balanceOf(0xdEaD) += 8e18. No path returns the role.","severity":"info","snippet":"        feeRecipient = newRecipient;","title":"setFeeRecipient is a one-step hand-off: a mistyped or uncontrolled address permanently strands the fee stream the function exists to protect"}],"hash":"14bf530f2df05ee12f43ddc11954cfa485723f070f757b00648a20cd7d802c25","nodeId":"94c50c9c-7385-4545-ba0f-1f31ab5cc746","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds four findings and a nine-row coverage record. No tracked file was changed. Scratch tests live only under the ignored `test/scratch/` directory.\n\n**Findings kept, after reproducing each one**\n\n1. **High. The manifest strands the fee stream in the ProjectFactory.** The accepted `launch.json` passes the zero address as the fourth constructor argument, and the constructor maps zero to the deployer. Under the launch the deployer is the factory contract. Every 8% fee then lands in the factory, which has no way to spend it or hand the role to the requester. The README explicitly requires the requester's wallet there. Two specialists saw the sentinel hazard at low severity under the assumption the manifest would be right. The manifest in the tree is not, so I merged them and raised it. My proof fails on current code and passes with a factory-keyed guard in the constructor, verified on a temporary copy. The fix also needs the manifest changed and two existing tests updated.\n\n2. **Medium. Fee bypass through the PoolManager.** Anyone can route a wallet-to-wallet transfer through the v4 PoolManager's unlock, sync, settle and take calls. Both legs are exempt, so the fee recipient gets nothing. The specialist's proof fails for exactly that reason. No token-side fix keeps both the launch floor and the fee, so I attached no proof. The requester must choose between documenting the gap or collecting the fee at the pool level.\n\n3. **Low. Dirty 32-byte distributor word bricks ordinary transfers.** Five specialists reported this one root cause. The length guard passes the word and the address decode reverts, contradicting the documented fallback. All three attached proofs fail as stated. The decode-as-uint256 fix makes all of them pass on a copy with no other test affected. Proof attached.\n\n4. **Info. One-step fee recipient hand-off.** A wrong address strands the stream the setter exists to protect. Reproduction stated, no proof.\n\n**Coverage.** All four entry points are answered. The project's own 63 tests pass on the current code. Static analysis leads about missing zero checks and the immutable supply variable did not reproduce as defects and were left out.","treeHash":null,"usage":{"cachedInputTokens":964123,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":20857,"runtime":"claude","turns":24,"wallClockMs":350556}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e4a4ecf9fefd4a46","findings":[{"citation":"resolved","description":"distributor() checks the success flag and response length, but a 32-byte word is not necessarily a valid ABI-encoded address. Solidity 0.8.26 rejects a word with nonzero upper 96 bits at abi.decode(data, (address)); that revert escapes the intended malformed-response fallback. If the configured factory returns such a word, ordinary transfer and transferFrom calls fail, while transfers touching the factory, PoolManager or fee recipient bypass the lookup and remain usable. This is an asymmetry in dependency-failure handling, not an allowance or role bypass. Severity is low because it requires an incompatible or faulty factory supplied at deployment; the canonical factory mapping getter returns valid address data, and no unprivileged way to corrupt that getter is demonstrated. Validate the raw uint256 word fits uint160 before converting it, returning address(0) for invalid address encodings, consistently with the existing fallback.","line":147,"path":"src/IdentityToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IdentityToken} from \"src/IdentityToken.sol\";\n\ncontract NonCanonicalDistributorFactory {\n    fallback() external {\n        assembly {\n            // Exactly 32 bytes, but an address cannot have bit 160 set.\n            mstore(0, or(shl(160, 1), 1))\n            return(0, 32)\n        }\n    }\n}\n\ncontract MalformedDistributorResponseTest is Test {\n    address constant ALICE = address(0xA11CE);\n    address constant BOB = address(0xB0B);\n    address constant SPENDER = address(0x5EED);\n    IdentityToken token;\n\n    function setUp() public {\n        NonCanonicalDistributorFactory factory = new NonCanonicalDistributorFactory();\n        token = new IdentityToken(address(factory), address(0), 7, address(0));\n        token.transfer(ALICE, 100 ether);\n    }\n\n    function test_transferSurvivesMalformedDistributorAddress() public {\n        uint256 feesBefore = token.balanceOf(address(this));\n        vm.prank(ALICE);\n        assertTrue(token.transfer(BOB, 100 ether));\n        assertEq(token.balanceOf(ALICE), 0);\n        assertEq(token.balanceOf(BOB), 92 ether);\n        assertEq(token.balanceOf(address(this)), feesBefore + 8 ether);\n    }\n\n    function test_transferFromSurvivesMalformedDistributorAddress() public {\n        vm.prank(ALICE);\n        token.approve(SPENDER, 100 ether);\n        uint256 feesBefore = token.balanceOf(address(this));\n        vm.prank(SPENDER);\n        assertTrue(token.transferFrom(ALICE, BOB, 100 ether));\n        assertEq(token.allowance(ALICE, SPENDER), 0);\n        assertEq(token.balanceOf(ALICE), 0);\n        assertEq(token.balanceOf(BOB), 92 ether);\n        assertEq(token.balanceOf(address(this)), feesBefore + 8 ether);\n    }\n}","reproduction":"Deploy NonCanonicalDistributorFactory whose fallback executes mstore(0, or(shl(160, 1), 1)); return(0, 32). It successfully returns the exact word 0x0000000000000000000000010000000000000000000000000000000000000001. From deployer D create IdentityToken(address(factory), address(0), uint64(7), address(0)), so D is the fee recipient. D calls transfer(address(0xA11CE), 100e18); this succeeds because D is exempt. Alice (0xA11CE) then calls transfer(address(0xB0B), 100e18). Expected: treat the malformed factory response as no distributor, debit Alice 100e18, credit Bob 92e18 and D 8e18. Actual: the address decoder reverts, leaving balances unchanged. On a fresh identical deployment, Alice approves address(0x5EED) for 100e18 and that spender calls transferFrom(Alice, Bob, 100e18); it also reverts instead of transferring, and allowance spending rolls back. Executed with forge test --offline --out /tmp/identity-permissions-out --cache-path /tmp/identity-permissions-cache --match-path test/scratch/MalformedDistributorResponse.t.sol -vv: both regression tests fail at IdentityToken after the successful factory staticcall.","severity":"low","snippet":"        (bool ok, bytes memory data) = factory_.staticcall(abi.encodeCall(ILaunchFactory.distributorOf, (launchNumber)));\n        if (!ok || data.length != 32) return address(0);\n        return abi.decode(data, (address));","title":"A noncanonical 32-byte distributor response bypasses the fallback and reverts ordinary transfers"}],"hash":"216fddb7905da2b67e7bf1029f44b4e55e4ab0aef687224f54cfb854b53254fa","nodeId":"22f22800-2cd0-4d62-888d-85bf1b1c4c33","outcome":"completed","summary":"Saved [.imd-findings.json](/home/seat/.identitymd/work/6c2edf25-ed78-446b-97a0-e4e8aa9898ea/22f22800-2cd0-4d62-888d-85bf1b1c4c33/.imd-findings.json).\n\n- One low-severity finding: malformed factory responses can block ordinary transfers.\n- All four entry points covered.\n- 45 tests passed; two regression tests reproduced the defect.\n- Protected integration tests couldn’t run because required dependencies were absent.","treeHash":null,"usage":{"cachedInputTokens":775552,"inputTokens":84933,"model":"gpt-6-astra","outputTokens":8938,"runtime":"codex","turns":5,"wallClockMs":248233}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"e36579e0223ff908","findings":[{"citation":"resolved","description":"isFeeExempt waives the fee whenever either side of a transfer is the PoolManager, so that a sell (trader -> PoolManager) and a buy (PoolManager -> trader) move the whole amount. The PoolManager is a permissionless contract: inside unlock() any caller may sync(currency), transfer tokens to the manager, settle() to be credited, and take(currency, to, amount) to send them to any address, with no pool, no hook and no swap fee involved. Both legs (from == user, to == poolManager; then from == poolManager, to == anyone) are exempt, so a wallet-to-wallet transfer of X ID arrives as X and the fee recipient receives 0 instead of 0.08*X. The same surface also lets holders mint ERC-6909 claims of ID inside the manager and move them between accounts indefinitely without any ID transfer at all. A single helper contract deployed once makes this available to every wallet, CEX and bridge at gas cost only. This contradicts the README's guarantee that wallet-to-wallet transfers and custodial intermediaries pay 8% (README.md lines 37-38) and the brief's 'Pay 8% fee to the deployer': the fee is opt-in for any informed sender, and the fee recipient (the requester) loses the revenue. Economic Security: token exemption turned into a free pass-through. Flow Gap seam periphery x first-principles: each exemption is correct for the swap it serves, the combination defeats the fee. No token-side fix preserves both the launch floor (seed and swaps must move whole) and the fee, because the token cannot tell a settle+take pass-through from a sell+buy. The requester must decide the scope: accept and document that the fee applies only to transfers that do not touch the PoolManager (and remove the README claim), or restrict the exemption so only msg.sender == poolManager outbound transfers and transfers with to == poolManager are exempt while charging the fee at the pool level through a hook instead. The attached proof uses an in-file stand-in that copies v4-core's unlock/sync/settle/take accounting because v4-core is not vendored in this repository; against the real PoolManager the call sequence is identical.","line":155,"path":"src/IdentityToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IdentityToken} from \"src/IdentityToken.sol\";\n\n/// @dev Stands in for the launch factory: deploys the token so it holds the supply, as the real\n///      ProjectFactory does, and answers `distributorOf`.\ncontract FactoryStub {\n    mapping(uint64 => address) public distributorOf;\n\n    function deployToken(address poolManager, uint64 launchNumber, address feeRecipient)\n        external\n        returns (IdentityToken)\n    {\n        return new IdentityToken(address(this), poolManager, launchNumber, feeRecipient);\n    }\n\n    function move(IdentityToken token, address to, uint256 amount) external returns (bool) {\n        return token.transfer(to, amount);\n    }\n}\n\n/// @dev The permissionless accounting surface of the Uniswap v4 PoolManager that every user can\n///      drive without touching any pool: `unlock` → callback → `sync`, `settle`, `take`.\n///      Semantics copied from v4-core: `sync` snapshots the manager's balance of a currency, `settle`\n///      credits the caller with whatever arrived since the snapshot, `take` sends currency out to any\n///      address and debits the caller, and `unlock` requires every currency delta to be zero at the end.\ncontract PoolManagerStub {\n    IdentityToken public immutable token;\n    bool private unlocked;\n    uint256 private reserves;\n    int256 private delta;\n\n    constructor(IdentityToken token_) {\n        token = token_;\n    }\n\n    function unlock(bytes calldata data) external returns (bytes memory result) {\n        require(!unlocked, \"AlreadyUnlocked\");\n        unlocked = true;\n        result = IUnlockCallback(msg.sender).unlockCallback(data);\n        require(delta == 0, \"CurrencyNotSettled\");\n        unlocked = false;\n    }\n\n    function sync() external {\n        reserves = token.balanceOf(address(this));\n    }\n\n    function settle() external returns (uint256 paid) {\n        require(unlocked, \"ManagerLocked\");\n        paid = token.balanceOf(address(this)) - reserves;\n        delta += int256(paid);\n    }\n\n    function take(address to, uint256 amount) external {\n        require(unlocked, \"ManagerLocked\");\n        delta -= int256(amount);\n        require(token.transfer(to, amount), \"take failed\");\n    }\n}\n\ninterface IUnlockCallback {\n    function unlockCallback(bytes calldata data) external returns (bytes memory);\n}\n\n/// @notice Anyone's fee-free transfer helper: routes `amount` from the caller to `to` through the\n///         PoolManager's settle/take surface. No pool, no hook, no swap fee, no 8%.\ncontract FeeFreeRouter is IUnlockCallback {\n    PoolManagerStub public immutable manager;\n    IdentityToken public immutable token;\n\n    constructor(PoolManagerStub manager_, IdentityToken token_) {\n        manager = manager_;\n        token = token_;\n    }\n\n    function send(address to, uint256 amount) external {\n        manager.unlock(abi.encode(msg.sender, to, amount));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"not manager\");\n        (address from, address to, uint256 amount) = abi.decode(data, (address, address, uint256));\n        manager.sync();\n        // from -> poolManager: fee-exempt because `to == poolManager` (the sell path a swap needs).\n        token.transferFrom(from, address(manager), amount);\n        manager.settle();\n        // poolManager -> to: fee-exempt because `from == poolManager` (the buy path a swap needs).\n        manager.take(to, amount);\n        return \"\";\n    }\n}\n\ncontract PoolManagerFeeBypassTest is Test {\n    uint64 constant LAUNCH = 7;\n    address constant REQUESTER = address(0xA11CE);\n    address constant ALICE = address(0xA1);\n    address constant BOB = address(0xB0B);\n\n    FactoryStub factory;\n    IdentityToken token;\n    PoolManagerStub manager;\n    FeeFreeRouter router;\n\n    function setUp() public {\n        factory = new FactoryStub();\n        // Deploy the manager at a deterministic address first so the token can be told about it.\n        address managerAddr = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1);\n        token = factory.deployToken(managerAddr, LAUNCH, REQUESTER);\n        manager = new PoolManagerStub(token);\n        assertEq(address(manager), managerAddr, \"stand-in manager address\");\n        router = new FeeFreeRouter(manager, token);\n        factory.move(token, ALICE, 1_000e18);\n    }\n\n    /// @dev A direct wallet-to-wallet transfer pays 8%, as designed.\n    function test_directTransferPaysFee() public {\n        vm.prank(ALICE);\n        token.transfer(BOB, 1_000e18);\n        assertEq(token.balanceOf(BOB), 920e18);\n        assertEq(token.balanceOf(REQUESTER), 80e18);\n    }\n\n    /// @dev The same wallet-to-wallet transfer routed through the PoolManager's public settle/take\n    ///      surface pays nothing. Fails on the current code: Bob receives 1,000 ID and the fee\n    ///      recipient receives 0, where the token's rule says 920 and 80.\n    function test_walletToWalletThroughPoolManagerMustStillPayFee() public {\n        vm.prank(ALICE);\n        token.approve(address(router), 1_000e18);\n        vm.prank(ALICE);\n        router.send(BOB, 1_000e18);\n\n        assertEq(token.balanceOf(ALICE), 0, \"Alice paid the full amount\");\n        assertEq(token.balanceOf(address(manager)), 0, \"nothing stays in the manager\");\n        assertEq(token.balanceOf(REQUESTER), 80e18, \"fee recipient should have received 8% of a wallet-to-wallet transfer\");\n        assertEq(token.balanceOf(BOB), 920e18, \"Bob should have received 92%\");\n    }\n}","reproduction":"State: token deployed by the factory with poolManager = the v4 PoolManager, feeRecipient = REQUESTER; factory moves 1,000e18 ID to ALICE (fee-free). ALICE approves a helper contract for 1,000e18 and calls helper.send(BOB, 1_000e18). The helper calls poolManager.unlock(data); in unlockCallback it calls poolManager.sync(ID); token.transferFrom(ALICE, poolManager, 1_000e18) [isFeeExempt: to == poolManager -> fee 0]; poolManager.settle() [credits +1,000e18 to the helper]; poolManager.take(ID, BOB, 1_000e18) [PoolManager calls token.transfer(BOB, 1_000e18); isFeeExempt: from == poolManager -> fee 0]; unlock completes with all deltas zero. Expected per the token's rule for a transfer between two ordinary parties: balanceOf(BOB) = 920e18, balanceOf(REQUESTER) += 80e18. Actual: balanceOf(BOB) = 1_000e18, balanceOf(REQUESTER) += 0, balanceOf(ALICE) = 0, balanceOf(poolManager) unchanged. Run: forge test --match-path test/scratch/PoolManagerFeeBypass.t.sol -> test_walletToWalletThroughPoolManagerMustStillPayFee fails with 'fee recipient should have received 8% of a wallet-to-wallet transfer: 0 != 80000000000000000000'.","severity":"medium","snippet":"        if (from == poolManager || to == poolManager) return true;","title":"8% fee is bypassed by anyone routing a transfer through the Uniswap v4 PoolManager's public settle/take surface"},{"citation":"resolved","description":"distributor() guards the staticcall with `!ok || data.length != 32` and the contract header (lines 26-27) and README (lines 40-42) promise that a factory that reverts or returns malformed data is treated as 'no distributor' and that a lookup failure can never brick transfers. The guard only covers the length. Solidity 0.8 abi.decode(data, (address)) validates that the word fits in 160 bits and reverts when the upper 12 bytes are non-zero. Because isFeeExempt calls distributor() on every transfer that is not already exempt by factory/poolManager/feeRecipient, such a return value makes every ordinary transfer and transferFrom revert while the factory keeps answering that way; only exempt parties can move tokens. Reachable if the launch factory's distributorOf(uint64) returns uint256/bytes32, is upgraded to a different return type, or is a different contract than assumed at the exempted address; the factory is trusted, so this is a robustness defect in the stated guarantee rather than an attack, hence low. Flow Gap seam periphery x first-principles (periphery returns a structure the code assumes is well-formed). Fix: decode as uint256 and treat values above type(uint160).max as 'no distributor', e.g. `uint256 word = abi.decode(data, (uint256)); if (word > type(uint160).max) return address(0); return address(uint160(word));`.","line":149,"path":"src/IdentityToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IdentityToken} from \"src/IdentityToken.sol\";\n\n/// @dev A factory whose `distributorOf` answers with 32 bytes that are not a clean address: the top\n///      twelve bytes are set. Returning a `uint256` or `bytes32` from a factory, or an upgraded\n///      factory with a different return type, produces exactly this.\ncontract DirtyWordFactory {\n    function distributorOf(uint64) external pure returns (uint256) {\n        return type(uint256).max;\n    }\n}\n\ncontract MalformedDistributorBricksTest is Test {\n    address constant ALICE = address(0xA1);\n    address constant BOB = address(0xB0B);\n\n    /// @dev The contract documents that \"a factory that reverts or returns malformed data is treated\n    ///      as 'no distributor'\" and the transfer proceeds taxed. With a 32-byte dirty word the\n    ///      `abi.decode(data, (address))` on line 149 reverts instead, so every ordinary transfer\n    ///      reverts. Fails on the current code; passes once the lookup validates the word before\n    ///      decoding (or decodes as uint256 and range-checks).\n    function test_dirtyAddressWordFromFactoryMustNotBrickTransfers() public {\n        DirtyWordFactory bad = new DirtyWordFactory();\n        IdentityToken token = new IdentityToken(address(bad), address(0), 1, address(this));\n        // Deployer is the fee recipient and so exempt: this funding transfer never consults the factory.\n        token.transfer(ALICE, 100e18);\n        assertEq(token.balanceOf(ALICE), 100e18);\n\n        // An ordinary transfer must settle taxed: 92 to Bob, 8 to the fee recipient.\n        vm.prank(ALICE);\n        (bool ok,) = address(token).call(abi.encodeCall(IdentityToken.transfer, (BOB, 100e18)));\n        assertTrue(ok, \"ordinary transfer reverted on a malformed distributor word\");\n        assertEq(token.balanceOf(BOB), 92e18);\n    }\n}","reproduction":"State: a factory contract whose distributorOf(uint64) returns type(uint256).max (32 bytes, upper bytes set). Deploy IdentityToken(address(thatFactory), address(0), 1, address(this)); deployer (fee recipient, exempt) transfers 100e18 to ALICE -> succeeds. ALICE calls transfer(BOB, 100e18). Expected (per documented rule): taxed transfer succeeds, BOB = 92e18, fee recipient += 8e18. Actual: the call reverts inside abi.decode at line 149 and ALICE cannot transfer to anyone but the exempt parties. Run: forge test --match-path test/scratch/MalformedDistributorBricks.t.sol -> test_dirtyAddressWordFromFactoryMustNotBrickTransfers fails with 'ordinary transfer reverted on a malformed distributor word'.","severity":"low","snippet":"        return abi.decode(data, (address));","title":"A 32-byte non-address word from factory.distributorOf reverts every ordinary transfer, contradicting the documented 'malformed data means no distributor' guarantee"},{"citation":"resolved","description":"The constructor treats feeRecipient_ == address(0) as 'the deployer' (msg.sender). Under launchCustom the deployer is the ProjectFactory, so a manifest whose fourth constructor argument is the zero address, or a factory that passes zero, silently makes the factory the fee recipient. Every 8% fee is then credited to the factory's balance. setFeeRecipient is callable only by the current recipient (line 113), and the factory has no code path to call it or to move the credited ID, so the requester's fee stream is permanently lost: the brief's 'Pay 8% fee to the deployer' is unmet for the requester with no recovery. The README documents the hazard (lines 57-59) but the contract does not enforce it. Temporal threat profile: parameter misconfiguration at deployment with an unrecoverable outcome; the deployer's manifest validation does not check this argument against economics.remainderTo. Fix preserving the intended design: when factory_ != address(0) (a launch), require an explicit non-zero feeRecipient_ (`if (factory_ != address(0) && feeRecipient_ == address(0)) revert InvalidFeeRecipient();`), keeping the deployer default only for stand-alone deployments. Independently, the manifest author must set constructorArgs[3] to economics.remainderTo.","line":97,"path":"src/IdentityToken.sol","reproduction":"State: factory contract deploys IdentityToken(address(this), 0x9001, 7, address(0)). feeRecipient() == address(factory). Factory moves 1,000e18 ID to ALICE; ALICE calls transfer(BOB, 1_000e18). Expected (brief): the requester receives 80e18. Actual: balanceOf(factory) rises by 80e18, requester receives 0; REQUESTER calling setFeeRecipient(REQUESTER) reverts with NotFeeRecipient(REQUESTER). Observed with test/scratch/ZeroFeeRecipientUnderLaunch.t.sol (an observation test that passes on the current code; it is not attached as proof).","severity":"low","snippet":"        address recipient = feeRecipient_ == address(0) ? msg.sender : feeRecipient_;","title":"Under a launch, a zero feeRecipient constructor argument makes the ProjectFactory the fee recipient and no one can ever redirect the fee stream"}],"hash":"36afcadaecf495ccbc937f023285ab6aff2afc907e3959d8a53c83c52674758f","nodeId":"659567bc-ca8c-40c8-a5bb-64e5961e3bd1","outcome":"completed","summary":"The findings file is written, parses, and its three snippets match the source lines exactly. Scratch tests stay under `test/scratch/`, which is gitignored and mine; no tracked file was changed.\n\n**Outcome:** the Identity token's fee economics are sound for direct transfers but the fee is defeasible, and I reported three substantiated defects in `.imd-findings.json` with a 12-row coverage record.\n\n**Findings**\n\n- **Medium, fee bypass through the PoolManager.** The exemption for either side being the PoolManager is required so swaps move whole amounts, but the v4 PoolManager is permissionless. Any sender can call unlock, sync, transfer to the manager, settle, then take to any address, and both legs are exempt. A wallet-to-wallet transfer of 1,000 ID arrives as 1,000 with 0 fee instead of 920 and 80. ERC-6909 claims inside the manager give the same result with no token transfer at all. The proof test fails on the current code. The token cannot distinguish this pass-through from a sell plus buy, so the fix is a scope decision for the requester, which I spelled out in the finding.\n- **Low, malformed factory word bricks ordinary transfers.** The documented guarantee says malformed distributor data is treated as \"no distributor\". A 32-byte return with non-zero upper bytes passes the length check and reverts inside the address decode, so every non-exempt transfer reverts. Proof test fails on the current code. Fix is to decode as uint256 and range-check.\n- **Low, zero fee recipient under a launch strands the fee stream.** A zero fourth constructor argument makes the factory the fee recipient. Only the current recipient can hand the role on and the factory has no path to do so, so all fees accrue to the factory forever. Reproduced with an observation test; suggested fix is to require an explicit recipient when a factory address is given.\n\n**Coverage:** all four ABI entry points have rows. Approve and setFeeRecipient hold. Transfer and transferFrom carry finding 1. Supply conservation, exact sender debit, fee rounding, view/write equivalence, whole launch flows, fixed supply and the slither zero-check leads were traced and hold. The slither and aderyn lines are not defects.\n\n**Not reached:** the real ProjectFactory and MerkleDistributor code are not in the tree, so the exact `distributorOf` ABI and whether the factory can forward tokens were taken from the protected harness rather than verified.","treeHash":null,"usage":{"cachedInputTokens":570336,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":26169,"runtime":"claude","turns":26,"wallClockMs":436575}},{"artifacts":[],"attempt":1,"bundleHash":"0db011ec443b7a111f62741ee39c0f64a3686a0713ccce469ed026229d1a8d84","device":"377843575071cdb1","findings":[{"description":"distributor() accepts any successful 32-byte response and then ABI-decodes it as an address. A word with nonzero upper 96 bits passes the length check but reverts during decoding. This violates the documented fallback that malformed factory data is treated as no distributor: both transfer and transferFrom between non-exempt holders become unavailable. This requires a malformed response from the configured factory; it does not establish an attack against the normal launch factory, and exempt transfers remain available.","line":149,"path":"src/IdentityToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IdentityToken} from \"src/IdentityToken.sol\";\n\ncontract NonCanonicalDistributorFactory {\n    fallback() external {\n        assembly {\n            // Exactly 32 bytes, but the upper 96 bits of an ABI address must be zero.\n            mstore(0, shl(160, 1))\n            return(0, 32)\n        }\n    }\n}\n\ncontract MalformedDistributorProofTest is Test {\n    function test_malformedAddressMustNotBlockOrdinaryTransfer() public {\n        NonCanonicalDistributorFactory factory = new NonCanonicalDistributorFactory();\n        IdentityToken token = new IdentityToken(address(factory), address(0), 7, address(0));\n        address alice = address(0xA11CE);\n        address bob = address(0xB0B);\n        token.transfer(alice, 100e18);\n        uint256 deployerBefore = token.balanceOf(address(this));\n\n        // The documented fallback is \"no distributor\", so this must deliver 92 ID\n        // and pay the deployer 8 ID even though the lookup returned malformed data.\n        vm.prank(alice);\n        assertTrue(token.transfer(bob, 100e18));\n        assertEq(token.balanceOf(alice), 0);\n        assertEq(token.balanceOf(bob), 92e18);\n        assertEq(token.balanceOf(address(this)), deployerBefore + 8e18);\n        assertEq(token.distributor(), address(0));\n    }\n}","reproduction":"Deploy NonCanonicalDistributorFactory whose fallback returns the 32-byte word uint256(1) << 160. Deploy IdentityToken(address(factory), address(0), 7, address(0)), transfer 100e18 from the deployer to Alice, then have Alice transfer 100e18 to Bob. Expected: success, Alice holds 0, Bob receives 92e18, deployer receives 8e18 in fees, and distributor() returns address(0). Actual: the ordinary transfer reverts with empty revert data in abi.decode. Reproduced with forge test --match-path test/scratch/MalformedDistributorProof.t.sol -vvv (plus --out test/scratch/out --cache-path test/scratch/cache to keep generated files in scope): 0 passed, 1 failed, EvmError: Revert.","severity":"low","title":"Malformed 32-byte distributor response blocks ordinary transfers"}],"hash":"61a324d935aafaaa5093aa390607ec273efb8ff0492cc18580b8a867cbadaac3","nodeId":"aefedcdc-111d-496a-b5ad-6fceb36d0816","outcome":"completed","summary":"Added failure-path, boundary, fuzz, and invariant tests; fixed an existing self-transfer double-counting assertion.\n\nOffline `forge build` and `forge test` pass: **62 tests**, including **16,384 invariant calls**.\n\nReported one low-severity defect—malformed factory responses can block ordinary transfers—with a reproduced failing proof in [.imd-findings.json](/home/worker/.identitymd/work/6c2edf25-ed78-446b-97a0-e4e8aa9898ea/aefedcdc-111d-496a-b5ad-6fceb36d0816/.imd-findings.json).","treeHash":"caee36fcac1bb4a58966a3df6f3c264b84defce4","usage":{"cachedInputTokens":978176,"inputTokens":84918,"model":"gpt-6-astra","outputTokens":16474,"runtime":"codex","turns":6,"wallClockMs":401274}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1f805d33d69c340e","findings":[],"hash":"6350aca166e80a6eb53d4fc3e354b8ab60bdb36f38f052fbaf92c47b982292b5","nodeId":"659567bc-ca8c-40c8-a5bb-64e5961e3bd1","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":4,"wallClockMs":226577}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3a271480f26269e3","findings":[{"citation":"resolved","description":"The distributor lookup checks call success and return-data length, then decodes an address. Solidity rejects a 32-byte address word with nonzero upper 96 bits, so this decode can revert despite both checks passing. This violates the implemented fallback contract that malformed factory responses mean no distributor: transfer and transferFrom between non-exempt accounts revert, even for amount zero. Preconditions are a configured factory returning noncanonical ABI data; no such behavior has been established for the actual launch factory, whose implementation is absent. Exempt-party transfers still bypass the lookup. Validate the returned uint256 is at most type(uint160).max before converting it to an address, otherwise return address(0).","line":148,"path":"src/IdentityToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IdentityToken} from \"src/IdentityToken.sol\";\n\ncontract InvalidAddressWordFactory {\n    fallback() external {\n        assembly {\n            mstore(0, shl(160, 1))\n            return(0, 32)\n        }\n    }\n}\n\ncontract MalformedDistributorTest is Test {\n    IdentityToken private token;\n    address private constant ALICE = address(0xA1);\n    address private constant BOB = address(0xB0B);\n    address private constant SPENDER = address(0xCA201);\n\n    function setUp() public {\n        InvalidAddressWordFactory factory = new InvalidAddressWordFactory();\n        token = new IdentityToken(address(factory), address(0), 7, address(this));\n        token.transfer(ALICE, 100e18);\n    }\n\n    function test_malformedAddressMustNotBlockTransfer() public {\n        uint256 recipientBefore = token.balanceOf(address(this));\n        vm.prank(ALICE);\n        assertTrue(token.transfer(BOB, 100e18));\n        assertEq(token.balanceOf(ALICE), 0);\n        assertEq(token.balanceOf(BOB), 92e18);\n        assertEq(token.balanceOf(address(this)), recipientBefore + 8e18);\n    }\n\n    function test_malformedAddressMustNotBlockTransferFrom() public {\n        vm.prank(ALICE);\n        token.approve(SPENDER, 100e18);\n        vm.prank(SPENDER);\n        assertTrue(token.transferFrom(ALICE, BOB, 100e18));\n        assertEq(token.balanceOf(ALICE), 0);\n        assertEq(token.balanceOf(BOB), 92e18);\n        assertEq(token.allowance(ALICE, SPENDER), 0);\n    }\n}","reproduction":"Deploy a factory stub whose fallback executes assembly { mstore(0, shl(160, 1)) return(0, 32) }; its response is the 32-byte encoding of 2^160, which is not an ABI address. Deployer D creates IdentityToken(address(stub), address(0), 7, D). D transfers 100e18 to Alice=address(0xA1), which succeeds because D is exempt. Alice calls transfer(address(0xB0B), 100e18). Expected: malformed lookup is ignored, Alice ends with 0, Bob gets 92e18, and D receives 8e18. Actual: abi.decode reverts and no balances change. Alternatively Alice approves spender address(0xCA201) for 100e18, then the spender calls transferFrom(Alice, Bob, 100e18): this also reverts and the allowance deduction rolls back. Both expected-success tests failed with EvmError: Revert under forge test --offline --out test/scratch/out --cache-path test/scratch/cache --match-path test/scratch/MalformedDistributor.t.sol -vvvv.","severity":"low","snippet":"        if (!ok || data.length != 32) return address(0);\n        return abi.decode(data, (address));","title":"A 32-byte malformed distributor response reverts ordinary transfers"}],"hash":"70532d92cc7e1de476f2db080d7e84159e4ede059b6ac9e623f083ccd63903cb","nodeId":"8d6df913-0dd9-4dce-96ef-d77f0ffa88ff","outcome":"completed","summary":"Saved [`.imd-findings.json`](/root/.identitymd/work/6c2edf25-ed78-446b-97a0-e4e8aa9898ea/8d6df913-0dd9-4dce-96ef-d77f0ffa88ff/.imd-findings.json) with one low-severity finding: malformed factory responses can revert ordinary transfers.\n\nCovered all four entry points. 45 tests passed; two reproduction tests failed as expected. Protected integration tests lacked dependencies and deployment inputs. Production files unchanged.","treeHash":null,"usage":{"cachedInputTokens":674432,"inputTokens":83498,"model":"gpt-6-astra","outputTokens":9814,"runtime":"codex","turns":6,"wallClockMs":271290}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5fdca4f96f37a80d","findings":[],"hash":"8703002485875c28e08597b80e7ff5b0a5f16e8b820f4d9cd5147ee47bad2be8","nodeId":"94c50c9c-7385-4545-ba0f-1f31ab5cc746","outcome":"completed","summary":"All four earlier findings are settled and the revised code holds. The findings record at .imd-findings.json carries an empty findings list and eleven coverage rows. The working tree is unchanged.\n\n**Settlement of the earlier findings**\n\n- **High, zero fee recipient under a launch: fixed.** The constructor now reverts with InvalidFeeRecipient when a factory is set and the recipient is zero. The attached proof passes on the revised code. The manifest in this tree now passes the requester wallet, which is economics.remainderTo, as the fourth constructor argument, so both halves of the finding are closed.\n- **Medium, fee-free relay through the PoolManager: settled as a documented trust assumption, not reopened.** The proof still fails, so the behaviour is unchanged. My finding said no token-side rule can tax the relay without taxing the swaps the launch floor requires, and that only a scope decision could resolve it. The author took that decision: the README and contract NatSpec now state the precise rule, the incorrect sentence about intermediaries is gone, and the pool hook alternative is recorded as the requester's deployment choice.\n- **Low, dirty 32-byte distributor word bricks transfers: fixed.** The lookup now decodes a word and returns no distributor above the address range. All three specialist proofs pass. The permissions proof failed only in setUp because it deployed with a zero recipient under a factory, which the high fix now refuses. With an explicit recipient both of its tests pass.\n- **Info, one-step fee recipient hand-off: fixed.** The hand-off is now propose and accept. I traced the new code and ran a scratch test for the edges it introduces. A pending address collects nothing and is not exempt before accepting, a replaced proposal cannot be accepted by the earlier proposee, and acceptance cannot be replayed.\n\n**Checks run**\n\n- Full project suite: 75 tests pass, including the 256-run invariant suite.\n- Static-analysis leads: the three missing-zero-check lines are intentional defaults that cannot match a transfer party, and the immutable suggestion is style. Neither is a defect.\n\n**What remains for the requester**\n\nThe fee-free PoolManager relay is a known limit of any fee-on-transfer token that must swap whole. Whether to collect a fee on pool flows with a Uniswap v4 hook is a deployment choice outside this token.","treeHash":null,"usage":{"cachedInputTokens":699265,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":10131,"runtime":"claude","turns":16,"wallClockMs":285464}},{"artifacts":[],"attempt":1,"bundleHash":"cbb8cc0870aa4227b10c4e286edd79472a2ae46f56a50c6d5a7cacdb082f55b0","device":"30a6c1a419ef4f9c","findings":[],"hash":"a0de981d38d5db51eec1690d9128a57159ed08117f48fb05c84df183b419edcd","nodeId":"1cd6449b-22fa-4485-b5a8-e5961e56387d","outcome":"completed","summary":"The manifest is correct and unchanged. The reported failure is a seed-dependent bug in a test file outside my permitted paths, not a defect in the token or in launch.json.\n\n**What failed and why.** The fuzz test `testFuzz_feeConservesSupply` in test/IdentityToken.t.sol ends by asserting that the sender's, recipient's and fee recipient's balances sum to at most the supply. When the fuzzer draws the same address for sender and recipient, that sum counts one balance twice, so it reports roughly double the supply. The counterexample in the check output is exactly that case: both addresses are 0xfac. The test's own self-transfer branch handles the per-balance checks correctly, but the final sum does not. The token moves the right amounts and the supply stays constant.\n\n**Evidence.**\n- Replaying the recorded seed reproduces the failure every time.\n- From a clean fuzz cache, the same test passes under eight other seeds, and the full suite passes 46 of 46.\n- The test file belongs to another contributor's accepted work (commits c7365b9 and 7c3c426). My allowed path is launch.json only, so I cannot fix it. The one-line fix is for that file's owner: skip the final sum assertion, or count the sender once, when `from == to`.\n\n**Manifest verification.** I did not change launch.json; the working tree is clean. Checks performed:\n- Validates against the canonical custom_token JSON Schema from the task. Notes are 1258 characters.\n- totalSupply is 10^27, matching the contract's constant and the 1,000,000,000 × 10^18 the constructor mints once to msg.sender.\n- Constructor arguments match the compiled ABI in declaration order: `$factory`, `$poolManager`, `$launchNumber`, then the requester's wallet. The requester address is required because the constructor reverts on a zero fee recipient whenever a factory is set, which the README and the existing launch tests both pin down.\n- Economics is byte-for-byte the job's verbatim object. No application contracts, so nothing reserved is named.\n- Pool pairs with native ETH at the 0.3% tier with tick spacing 60. The recorded sqrtPriceX96 equals sqrt(totalSupply / initialMarketCapWei) × 2^96 for ETH as currency0, computed independently.\n\n**Recap.** launch.json is the finished deliverable and needs no edits. Build and the full test suite pass locally. The only outstanding item is the flaky self-transfer assertion in the token test file, which only its owner can change.","treeHash":"89c092d5b0e581cf9cffb1d5e7004cca32e75782","usage":{"cachedInputTokens":709532,"inputTokens":452,"model":"claude-fable-5-1","outputTokens":14513,"runtime":"claude","turns":20,"wallClockMs":241057}},{"artifacts":[],"attempt":1,"bundleHash":"da6323b1491956ceb1bf7b77be12d59a5c9a017877a04cd6656e4cb6fe0c5fa1","device":"fa8fc4653a9e883d","findings":[],"hash":"cda5835b7e36af78d6bf4020df61960573905062d70a7c0c3c3a44f6f231268a","nodeId":"2acdff5d-9a8c-4d98-ae98-96884b180969","outcome":"completed","summary":"Everything passes. Final state:\n\n**Delivered** a self-contained ERC-20, `src/IdentityToken.sol`, for Identity (ID): 1,000,000,000 tokens with 18 decimals, minted once to the constructor's caller. Ordinary transfers debit the sender the full amount, deliver 92% to the recipient, and pay 8% to the fee recipient, with the supply unchanged.\n\n**Launch compatibility.** The protected floor requires the factory, distributor, and PoolManager flows to move whole, so transfers from or to the factory, the PoolManager, the distributor read from `distributorOf(launchNumber)` at transfer time, and the fee recipient are fee-free. A factory with no code, a reverting lookup, or malformed return data degrades to \"no distributor\" rather than bricking transfers. There is no mint, burn, pause, blacklist, or upgrade path, and the runtime has no DELEGATECALL, CALLCODE or SELFDESTRUCT.\n\n**Design decision to flag.** Under the launch the constructor's caller is the ProjectFactory, so \"pay the deployer\" literally would send fees to the factory. The constructor therefore takes a fourth static argument, the fee recipient, and treats zero as \"the deployer\". The manifest must pass the requester's wallet there. The README states this explicitly. The only privileged function lets the current fee recipient hand the role to a new non-zero address; it cannot touch balances.\n\n**Checks run:**\n\n| Check | Result |\n|-------|--------|\n| `forge build` (solc 0.8.26, bytecode_hash none) | clean, lint warnings only |\n| `forge test` in an empty environment | 37 passed |\n| `forge fmt --check` | clean |\n| scratch mirror of the floor's admin sweep, holder sweep, opcode scan, swarm flow | 4 passed |\n\nThe full Uniswap v4 seed-and-swap check could not be reproduced locally since the harness's v4-core and LaunchLiquidity sources are not in the repo. The exemption logic covers both `take` (manager to trader) and `settle` (trader to manager) paths, and a unit test exercises both directions plus an operator pulling straight into the manager.\n\n**Files:** `foundry.toml`, `remappings.txt`, `.gitignore`, `src/IdentityToken.sol`, `script/DeployIdentityToken.s.sol`, `test/IdentityToken.t.sol`, `README.md`, and forge-std v1.9.7 vendored as plain files under `lib/forge-std` with no submodule or nested git metadata. Nothing was committed, since the git directory is off limits. The README documents assumptions, constructor parameters for launch and direct deployment, a manifest sketch, trust assumptions, and operational responsibilities.","treeHash":"03f1d6722ebc5f0f71bc4a1e08e751f99e190822","usage":{"cachedInputTokens":666019,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":31316,"runtime":"claude","turns":25,"wallClockMs":360099}},{"artifacts":[],"attempt":1,"bundleHash":"24cff2f729df1d79d4bc80cd3713606ce44a8096b21abfae4a5299f87db2ebec","device":"593a9c894818e368","findings":[],"hash":"cea81866b4ddaddeaa8aea17440fdd08c4cff379beaadaab3bd2be677c3a3733","nodeId":"1cd6449b-22fa-4485-b5a8-e5961e56387d","outcome":"completed","summary":"Created `launch.json` with matching constructor arguments, exact supply and economics, and fees directed to the deployer.\n\nSchema and ABI checks passed. `forge build` succeeded; all 37 tests passed. Only `launch.json` is included in the submission.","treeHash":"fe2ca9f678a72700d5b56fe130b2842bd127053b","usage":{"cachedInputTokens":162048,"inputTokens":35780,"model":"gpt-6-astra","outputTokens":3746,"runtime":"codex","turns":4,"wallClockMs":98994}},{"artifacts":[],"attempt":1,"bundleHash":"e0d00b1916477c70576a3c0e0ed8f9c31d9788149684a89fd3566b9ee77be67f","device":"e6de8d4c6cf97551","findings":[],"hash":"efe2ef7cd616c0c184d0534a4a43788dc91292ebf25f6fa5358910de3d5e92ed","nodeId":"aefedcdc-111d-496a-b5ad-6fceb36d0816","outcome":"completed","summary":"Done. The suite passes again against the revised implementation, and the only changes are in the three test files that encoded the old one-step hand-off.\n\n**What broke and why.** Between rounds the token gained two changes: the constructor now rejects a zero fee recipient when a factory is set, and the fee-recipient hand-off became two-step (propose, then accept). Two of my accepted tests encoded the old behaviour and failed. The invariant suite's setUp reverted on construction, and the adversarial hand-off test expected the role to move on proposal.\n\n**What I changed.**\n- `test/helpers/IdentityTokenHandler.sol`: the factory now deploys with an explicit requester actor as recipient. The one-step action is replaced by `proposeFeeRecipient` and `acceptFeeRecipient`, with a pending ghost. The ghost recipient only moves on acceptance, so the model checks that fees keep flowing to the current recipient until the proposee accepts. New rejection actions cover acceptance by non-proposees, acceptance with nothing pending, and proposals by a proposee who has not yet accepted.\n- `test/IdentityTokenInvariant.t.sol`: registers the new actions, asserts the pending slot against the ghost after every call, and extends the deterministic exercise test to walk the full propose, reject, accept, reject-stale sequence.\n- `test/IdentityTokenAdversarial.t.sol`: the hand-off test now checks that a proposal alone changes nothing, that the old recipient keeps its exemption and authority until acceptance, and that both are revoked afterwards. Four new tests cover acceptance with nothing pending, a later proposal superseding an earlier one, self-proposal, and replaying a consumed acceptance.\n\n**Verification.**\n\n| Check | Result |\n|---|---|\n| forge build | clean |\n| forge test | 75 passed, 0 failed |\n| invariant run | 256 runs, 16384 calls, 0 reverts |\n| mutant token (role moves on proposal) | caught on the first proposal |\n| action coverage | all 11 handler actions called 165 to 203 times each at 32 runs |\n\n**No findings file.** I re-read the current implementation adversarially and found nothing to report. The two concerns from the earlier round, the raw-word distributor decode and the factory ending up as fee recipient under a launch, are both fixed in this source and are covered by the existing tests. The PoolManager relay limit remains documented rather than asserted as a defect, as before, since it is the required launch flow.","treeHash":"ec3da841d08785080bb924d27380094722a2c084","usage":{"cachedInputTokens":771515,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":17852,"runtime":"claude","turns":23,"wallClockMs":318835}},{"artifacts":[],"attempt":1,"bundleHash":"b165a0bfb39d4715618a77030e002ee78770fc73a0f1c8dd5a82ae6ad0bdd1e2","device":"ce319efac2b76da0","findings":[],"hash":"f088be11e941d5371f542739081f76417220452bb5f9fb69776ca68a8a75d363","nodeId":"2acdff5d-9a8c-4d98-ae98-96884b180969","outcome":"completed","summary":"All four findings reproduced on the starting tree, and all four are answered in `.imd-responses.json` with verdict \"fixed\".\n\n**What changed in `src/IdentityToken.sol`**\n\n- **High, zero fee recipient under a launch.** The constructor now reverts with `InvalidFeeRecipient` when the factory argument is non-zero and the recipient is zero. The deployer default survives only for stand-alone deployments. The reviewer's proof now passes. The manifest half of the finding is not in this tree, since launch.json is written by the separate manifest node, so the README states the fourth constructor argument must be the requester's wallet and that zero is refused.\n- **Medium, fee-free relay through the PoolManager.** Reproduced with a stand-in copying the manager's unlock, sync, settle and take accounting. No token-side rule can tax the relay without taxing swaps, which the launch floor requires to move whole. Resolved as the scope decision the finding asked for: the README now states the precise rule, 8% on transfers that do not pass through the PoolManager or another exempt party, corrects the earlier sentence about intermediaries, and records a pool hook as an unresolved deployment choice for the requester. A test pins the behaviour against the stand-in.\n- **Low, non-address word from the factory.** The distributor lookup now decodes a word and range-checks it against 160 bits, returning \"no distributor\" instead of reverting. Tests cover a dirty word and an overlong answer, for both transfer and transferFrom.\n- **Info, one-step hand-off.** The hand-off is now two-step. The current recipient proposes via `setFeeRecipient`, and the proposed address takes the role via a new accept function. A later proposal replaces an earlier one. My own test caught that an empty pending slot would have matched a zero-address caller, so the accept step also refuses when nothing is pending.\n\n**Verification**\n\n- forge build, forge test and forge fmt --check all pass with the pinned 0.8.26 compiler.\n- Test count went from 39 to 46 in the main suite, plus the reviewer's proof.\n- A scratch copy of the protected floor's opcode scan passes on the deployed runtime for both the starting tree and the revision.\n\n**Open for the requester**\n\nThe fee-free PoolManager relay stays by design. Taxing pool flows would need a Uniswap v4 hook, which is outside this token and this assignment. The manifest node must pass the requester's wallet as the fourth constructor argument, or the launch will now fail at deployment rather than stranding fees.","treeHash":"87610a6bf23c6010228beedd032d334ee4e2f34d","usage":{"cachedInputTokens":1198515,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":28409,"runtime":"claude","turns":46,"wallClockMs":368625}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d11ea2b5e05fa7a8","findings":[{"citation":"resolved","description":"Execution trace / periphery. `distributor()` is on the hot path of every transfer that is not short-circuited by the factory, PoolManager or fee-recipient checks (`_transfer` -> `isFeeExempt` -> `distributor()`, lines 158 and 179). The NatSpec (line 27) and README promise that a factory that reverts or returns malformed data is treated as 'no distributor' and that 'a lookup failure can never brick transfers'. The guard on line 148 only covers call failure and a length other than 32. A 32-byte return whose upper 12 bytes are not zero passes the guard and reaches `abi.decode(data, (address))`, which in Solidity 0.8 validates the value fits in 160 bits and reverts otherwise. The revert propagates through the view into `_transfer`, so every ordinary `transfer`/`transferFrom` reverts for as long as the factory answers that way. Flows touching the factory, PoolManager or fee recipient still work, so the launch floor (seed, swap, distributor claim when the factory answers correctly) is unaffected; the impact is confined to a direct deployment whose `factory_` argument is a non-conforming contract, or to a launch factory whose `distributorOf` is later changed to return a non-ABI-clean word. Reachability therefore depends on a deployment-time choice, which keeps this at low. Fix: decode the word as uint256 and treat `word >> 160 != 0` as 'no distributor' (verified: the attached test passes against a copy of the contract with that two-line change), or read the word in assembly and mask to 160 bits.","line":149,"path":"src/IdentityToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IdentityToken} from \"src/IdentityToken.sol\";\n\n/// @dev A factory whose `distributorOf` answers with a 32-byte word whose upper 12 bytes are not zero.\n///      The token documents that \"a factory that reverts or returns malformed data is treated as\n///      'no distributor'\" and that \"a lookup failure can never brick transfers\", so ordinary\n///      transfers must keep working (taxed) against this factory.\ncontract DirtyWordFactory {\n    fallback() external {\n        assembly {\n            // 0x000000000000000000000001000000000000000000000000000000000000d157:\n            // a plausible distributor address with one stray bit above bit 160.\n            mstore(0, or(shl(160, 1), 0xd157))\n            return(0, 32)\n        }\n    }\n}\n\ncontract DistributorDirtyWordTest is Test {\n    uint256 constant SUPPLY = 1_000_000_000e18;\n    address constant ALICE = address(0xA1);\n    address constant BOB = address(0xB0B);\n\n    function test_dirtyWordFactoryDoesNotBrickOrdinaryTransfers() public {\n        DirtyWordFactory bad = new DirtyWordFactory();\n        // This test contract is the deployer and the fee recipient.\n        IdentityToken token = new IdentityToken(address(bad), address(0), 1, address(this));\n\n        // Fund Alice fee-free (deployer is the fee recipient, so exempt; no lookup on this path).\n        token.transfer(ALICE, 100e18);\n        assertEq(token.balanceOf(ALICE), 100e18);\n\n        // An ordinary transfer must still go through, taxed at 8%. On the current code it reverts\n        // inside `distributor()` because `abi.decode(data, (address))` rejects the dirty word.\n        vm.prank(ALICE);\n        (bool ok, bytes memory ret) = address(token).call(abi.encodeCall(IdentityToken.transfer, (BOB, 100e18)));\n        assertTrue(ok, \"ordinary transfer reverted: distributor lookup bricked transfers\");\n        assertTrue(abi.decode(ret, (bool)));\n        assertEq(token.balanceOf(BOB), 92e18, \"recipient gets 92%\");\n        assertEq(token.balanceOf(address(this)), SUPPLY - 100e18 + 8e18, \"fee recipient gets 8%\");\n\n        // Documented behaviour: malformed data reads as \"no distributor\".\n        assertEq(token.distributor(), address(0), \"malformed word should read as no distributor\");\n    }\n}","reproduction":"State: deploy a factory whose fallback returns the 32-byte word `(1 << 160) | 0xd157` (a plausible address with one stray bit above bit 160). Deploy `new IdentityToken(address(thatFactory), address(0), 1, deployer)`. Call `token.transfer(ALICE, 100e18)` from the deployer (exempt path, succeeds). Then `vm.prank(ALICE); token.transfer(BOB, 100e18)`. Expected per NatSpec line 27 / README: the lookup yields address(0), BOB receives 92e18 and the fee recipient 8e18. Actual: the call reverts with an empty revert inside `IdentityToken.distributor` (trace: DirtyWordFactory::fallback returns 0x…d157 with the dirty bit, then `abi.decode` reverts). `token.distributor()` reverts likewise instead of returning address(0). Run: forge test --match-path test/scratch/DistributorDirtyWord.t.sol -> FAIL 'ordinary transfer reverted: distributor lookup bricked transfers'.","severity":"low","snippet":"        if (!ok || data.length != 32) return address(0);\n        return abi.decode(data, (address));","title":"distributor() reverts on a 32-byte word with dirty upper bits, so every taxed transfer reverts against such a factory (contradicts the documented 'malformed data = no distributor' guarantee)"},{"citation":"resolved","description":"Execution trace, sentinel path. `feeRecipient_ == address(0)` is special-cased to `msg.sender`. Under the launch `msg.sender` of the constructor is ProjectFactory (the manifest's token constructorArgs are static words plus $factory/$poolManager/$launchNumber; there is no $requester placeholder for the token, so the requester's address must be typed in as a literal). If that literal is the zero address, or omitted in favour of the documented default, the fee recipient becomes the factory. The factory is already exempt as a counterparty (line 154), so nothing in the launch floor observes the mistake: the swarm share, the seed and swaps all pass, and the token deploys. From then on every ordinary transfer credits 8% to the factory (line 192). `setFeeRecipient` (line 113) only accepts the current recipient, and the factory has no call surface to invoke it, so the stream can never be moved to the requester and every fee ever collected sits in the factory. The brief's intent, 'pay 8% fee to the deployer', is realised as 'pay 8% to a contract that cannot spend it'. The README warns about this in prose, but the contract accepts the input silently. Fix that preserves the direct-deployment convenience: revert when `feeRecipient_ == address(0)` and `msg.sender` has code (`msg.sender.code.length != 0` is true for a factory mid-call but zero for an EOA deployer and, under `vm.startBroadcast`, the broadcasting EOA), or simply require a non-zero `feeRecipient_` and have the deploy script pass `msg.sender` explicitly. Either keeps the fee rule and the exemption set unchanged.","line":97,"path":"src/IdentityToken.sol","reproduction":"State: a factory contract with a mapping `distributorOf` and a `move` helper but no arbitrary-call function (as ProjectFactory). It runs `new IdentityToken(address(this), 0x9001, 7, address(0))`, the shape the manifest would produce with constructorArgs [\"$factory\",\"$poolManager\",\"$launchNumber\",\"0x0000000000000000000000000000000000000000\"]. Observed: `token.feeRecipient() == address(factory)`. factory.move(token, ALICE, 100e18); vm.prank(ALICE); token.transfer(BOB, 100e18) -> BOB has 92e18 and `balanceOf(factory)` grew by 8e18. vm.prank(REQUESTER); token.setFeeRecipient(REQUESTER) reverts with NotFeeRecipient(REQUESTER). Expected: either the constructor rejects a zero recipient when the deployer is a contract, or the fee lands with the requester. Reproduced in test/scratch/ZeroRecipientUnderFactory.t.sol (passes, i.e. demonstrates the stuck state).","severity":"low","snippet":"        address recipient = feeRecipient_ == address(0) ? msg.sender : feeRecipient_;","title":"Zero fee-recipient sentinel silently binds the whole 8% fee stream to the launch factory, which can never rotate it"},{"citation":"resolved","description":"First principles. The README says the setter exists 'so a lost or rotated key does not strand the fee stream forever', but the hand-off itself is one-step with only a zero check (line 114). The new recipient is written immediately and the old one loses the role in the same call. A recipient that passes a mistyped address, a contract that cannot call back, or an address it does not control strands the 8% stream exactly as a lost key would. Self-inflicted by the trusted role, so informational only; a two-step pattern (pending recipient + accept) preserves the design and removes the failure mode.","line":116,"path":"src/IdentityToken.sol","reproduction":"vm.prank(REQUESTER); token.setFeeRecipient(0x000000000000000000000000000000000000dEaD) succeeds and emits FeeRecipientChanged(REQUESTER, 0xdEaD). Afterwards vm.prank(REQUESTER); token.setFeeRecipient(REQUESTER) reverts NotFeeRecipient(REQUESTER), and every later ordinary transfer credits its 8% to 0xdEaD (e.g. ALICE transfers 100e18 to BOB: balanceOf(0xdEaD) += 8e18). No path returns the role.","severity":"info","snippet":"        feeRecipient = newRecipient;","title":"setFeeRecipient is a one-step hand-off: a wrong address permanently strands the fee stream the function exists to protect"}],"hash":"f144e272c103473cef49cce1e8d698fdbe5388fbe2ca92803eceb15208346dd1","nodeId":"8a0a6116-7471-4afc-9f77-318bf3d191bc","outcome":"completed","summary":"Review complete. No tracked file was changed. The findings and coverage record are in `.imd-findings.json`, and the two scratch tests remain under `test/scratch/` for the author and verifier.\n\n**Outcome:** the token's core fee logic holds. The sender is debited exactly `amount`, the recipient and fee recipient receive 92% and 8% that sum to it, supply is constant, there is no mint or admin path that touches balances, and the runtime has no forbidden opcodes. The launch flows (factory, PoolManager, distributor) short-circuit before any external call or are exempt when the factory answers correctly. I found three defects, none above low.\n\nFindings reported:\n\n1. **Low. The distributor lookup can brick ordinary transfers.** The guard before `abi.decode` only checks call success and a 32-byte length. A factory returning a 32-byte word with stray bits above bit 160 passes the guard, and the decoder reverts, so every taxed `transfer` and `transferFrom` reverts. This contradicts the documented promise that malformed data means \"no distributor\". Reachable only with a non-conforming factory chosen at deployment, so low. The attached proof test fails on the current code and passes with a two-line mask fix, which I verified on a patched copy.\n\n2. **Low. Zero fee-recipient sentinel under a launch binds the fee stream to the factory forever.** The constructor maps a zero recipient to `msg.sender`, which under `launchCustom` is the ProjectFactory. The factory cannot call `setFeeRecipient`, so every 8% fee would sit in a contract that cannot spend it, and nothing in the protected floor notices. Reproduced with a factory probe.\n\n3. **Info. One-step fee-recipient hand-off.** A mistyped address strands the stream the setter was built to protect. Self-inflicted by the trusted role, so informational.\n\nCoverage: all four ABI entry points plus the constructor and five invariant or trust-assumption rows are answered. One row is marked unreached: the real ProjectFactory's `distributorOf` signature and return encoding are not in the tree, so I relied on the protected harness's `distributorOf(uint64)` as the contract of record.","treeHash":null,"usage":{"cachedInputTokens":807938,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":24583,"runtime":"claude","turns":24,"wallClockMs":358042}}],"verification":[{"checks":[{"durationMs":2385,"exitCode":0,"name":"build","output":"Compiling 26 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.22s\nCompiler run successful!\nwarning[missing-events-access-control]: `feeRecipient` is changed without an event but is used for access control\n    ╭▸ src/IdentityToken.sol:116:9\n    │\n116 │         feeRecipient = newRecipient;\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:35\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                                   ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:79\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                                                                               ━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:17\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                 ━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\n","passed":true},{"durationMs":17946,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 37 tests for test/IdentityToken.t.sol:IdentityTokenTest\n[PASS] testFuzz_feeConservesSupply(address,address,uint256,uint256) (runs: 256, μ: 273064, ~: 279323)\nLogs:\n  Bound result 400742294904623285330\n  Bound result 89426627969350090617\n\n[PASS] test_RevertWhen_approveZeroSpender() (gas: 30803)\n[PASS] test_RevertWhen_factorySetsFeeRecipient() (gas: 34968)\n[PASS] test_RevertWhen_feeRecipientSetToZero() (gas: 32234)\n[PASS] test_RevertWhen_strangerSetsFeeRecipient() (gas: 32627)\n[PASS] test_RevertWhen_transferExceedsBalance() (gas: 91236)\n[PASS] test_RevertWhen_transferFromExceedsAllowance() (gas: 138602)\n[PASS] test_RevertWhen_transferFromWithoutAllowance() (gas: 91503)\n[PASS] test_RevertWhen_transferToZeroAddress() (gas: 88827)\n[PASS] test_approveEmitsAndOverwrites() (gas: 92872)\n[PASS] test_constructorEmitsMintAndFeeRecipient() (gas: 8099)\n[PASS] test_deployScriptUsesTheGivenConfig() (gas: 2021783)\n[PASS] test_deployScriptZeroFeeRecipientMeansTheDeployer() (gas: 1982015)\n[PASS] test_directDeploymentWithoutLaunchTaxesOrdinaryTransfers() (gas: 146681)\n[PASS] test_distributorIsReadFromFactoryAtTransferTime() (gas: 202555)\n[PASS] test_factoryAsEoaIsExemptButNotQueried() (gas: 126372)\n[PASS] test_factoryToDistributorAndClaimAreWhole() (gas: 160336)\n[PASS] test_factoryTransfersToAnyoneAreWhole() (gas: 79130)\n[PASS] test_feeRecipientCanHandOn() (gas: 208156)\n[PASS] test_feeRecipientCannotMoveOrFreezeHolders() (gas: 190381)\n[PASS] test_feeRecipientDefaultsToDeployer() (gas: 17944)\n[PASS] test_feeRoundsDownForDustAmounts() (gas: 237553)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 203804)\n[PASS] test_launchPartiesRecorded() (gas: 51758)\n[PASS] test_malformedFactoryLookupDoesNotBrickTransfers() (gas: 233389)\n[PASS] test_metadata() (gas: 34758)\n[PASS] test_noMintSurface() (gas: 105981)\n[PASS] test_ordinaryTransferPaysEightPercentToFeeRecipient() (gas: 200533)\n[PASS] test_ordinaryTransfersStillTaxedWhenFactoryHasNoDistributor() (gas: 192404)\n[PASS] test_otherLaunchNumbersDistributorIsNotExempt() (gas: 198283)\n[PASS] test_poolManagerFlowsAreWholeBothWays() (gas: 347253)\n[PASS] test_revertingFactoryLookupDoesNotBrickTransfers() (gas: 271108)\n[PASS] test_selfTransferPaysFee() (gas: 146836)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 31245)\n[PASS] test_transferFromPaysFeeAndSpendsAllowance() (gas: 247246)\n[PASS] test_transfersTouchingTheFeeRecipientAreWhole() (gas: 192114)\n[PASS] test_zeroAmountTransferSucceedsWithoutFee() (gas: 51924)\nSuite result: ok. 37 passed; 0 failed; 0 skipped; finished in 82.33ms (74.00ms CPU time)\n\nRan 23 tests for test/IdentityTokenAdversarial.t.sol:IdentityTokenAdversarialTest\n[PASS] testFuzz_failedTransferFromPreservesEverything(uint256,uint256) (runs: 1000, μ: 272880, ~: 273471)\nLogs:\n  Bound result 80000000000000000000\n  Bound result 115792089237316195423570985008687907853269984665640564039377584007913129660102\n\n[PASS] testFuzz_feeRoundingAndPeriodOnTransferableDomain(uint256) (runs: 1000, μ: 22124, ~: 21868)\nLogs:\n  Bound result 10878\n\n[PASS] testFuzz_roundTripLossEqualsDeployerFees(uint256) (runs: 1000, μ: 233520, ~: 236486)\nLogs:\n  Bound result 1766\n\n[PASS] testFuzz_transferFromConservesBalancesAndSpendsGrossAllowance(uint256,uint256) (runs: 1000, μ: 246758, ~: 249148)\nLogs:\n  Bound result 201286989197176585716910495\n  Bound result 198800969157149695481148434\n\n[PASS] test_allowanceCoversGrossAmountIncludingFee() (gas: 258133)\n[PASS] test_delegatedSelfTransferSpendsGrossAllowanceAndOnlyFeeFromBalance() (gas: 181201)\n[PASS] test_feeRecipientCannotSetZeroOrStealUsingTransferFrom() (gas: 296830)\n[PASS] test_feeRecipientHandoffPreservesHoldingsAndRevokesOldAuthority() (gas: 408340)\n[PASS] test_fullSupplySelfTransferCountsHolderOnce() (gas: 151169)\n[PASS] test_maximumTransferFailsWithBalanceErrorAndNoChanges() (gas: 153862)\n[PASS] test_maximumTransferFromPreservesInfiniteApprovalOnFailure() (gas: 261129)\n[PASS] test_repeatedInfiniteAllowanceTransfersDoNotConsumeApproval() (gas: 258216)\n[PASS] test_revocationOverwritesInfiniteAllowanceImmediately() (gas: 283571)\n[PASS] test_roundingEdgesIncludingWholeSupply() (gas: 1487284)\n[PASS] test_spentAllowanceCannotBeReused() (gas: 335909)\n[PASS] test_transferFromInsufficientBalanceRestoresFiniteAllowance() (gas: 265713)\n[PASS] test_transferFromInvalidReceiverRestoresAllowance() (gas: 263004)\n[PASS] test_zeroAndOneWeiTransfersEmitTransferWithoutFee() (gas: 175460)\n[PASS] test_zeroApprovalStillRejectsZeroSpender() (gas: 162291)\n[PASS] test_zeroTransferFromNeedsNoAllowanceAndChangesNoBalances() (gas: 164799)\n[PASS] test_zeroTransferFromRejectsZeroReceiver() (gas: 156423)\n[PASS] test_zeroTransferFromRejectsZeroSender() (gas: 167058)\n[PASS] test_zeroTransferStillRejectsZeroReceiver() (gas: 151473)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 83.80ms (327.35ms CPU time)\n\nRan 2 tests for test/IdentityTokenInvariant.t.sol:IdentityTokenInvariantTest\n[PASS] invariant_supplyBalancesAllowancesAndRolesMatchIndependentModel() (runs: 256, calls: 16384, reverts: 0)\n\n╭----------------------+-----------------------------+-------+---------+----------╮\n| Contract             | Selector                    | Calls | Reverts | Discards |\n+=================================================================================+\n| IdentityTokenHandler | approve                     | 1803  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | changeDistributor           | 1830  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | changeFeeRecipient          | 1815  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectDelegatedTransfer     | 1776  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectInvalidAdministration | 1834  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectOverdraw              | 1817  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectUnauthorizedPull      | 1828  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | transfer                    | 1803  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | transferFrom                | 1878  | 0       | 0        |\n╰----------------------+-----------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1012835141235535496798820156405351442503684328796569702533\n  Bound result 0\n  Bound result 115792089237316195423570985008687907853269984665640464039457584007913129647281\n  Bound result 374\n  Bound result 1\n  Bound result 36865\n  Bound result 0\n  Bound result 99\n  Bound result 610945644573096151702765217\n  Bound result 115792089237316195423570985008687907853269984665640464039457584007913129640145\n  Bound result 115792089237316195423570985008687907853269984665640564039457584007913129639935\n  Bound result 0\n  Bound result 22\n  Bound result 8680\n  Bound result 7152\n  Bound result 0\n  Bound result 9052\n  Bound result 115792089237316195423570985008687907853269984665640164039457584007913129640045\n  Bound result 7442\n\n[PASS] test_handlerExercisesTaxedExemptSelfDelegatedAndRejectedCalls() (gas: 5807363)\nLogs:\n  Bound result 100\n  Bound result 100\n  Bound result 100\n  Bound result 100\n  Bound result 100\n  Bound result 115792089237316195423570985008687907853269984665640564039457584007913129639935\n  Bound result 1\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 17.82s (17.83s CPU time)\n\nRan 3 test suites in 17.83s (17.99s CPU time): 62 tests passed, 0 failed, 0 skipped (62 total tests)\n","passed":true},{"durationMs":70,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IdentityToken.approve(address,uint256)\",\"IdentityToken.setFeeRecipient(address)\",\"IdentityToken.transfer(address,uint256)\",\"IdentityToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":153,\"foundry.toml\":20,\"remappings.txt\":1,\"script/DeployIdentityToken.s.sol\":39,\"src/IdentityToken.sol\":215,\"test/IdentityToken.t.sol\":488,\"test/IdentityTokenAdversarial.t.sol\":319,\"test/IdentityTokenInvariant.t.sol\":91,\"test/helpers/IdentityTokenHandler.sol\":190},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"61a324d935aafaaa5093aa390607ec273efb8ff0492cc18580b8a867cbadaac3","verifiedTreeHash":"caee36fcac1bb4a58966a3df6f3c264b84defce4","verifierVersion":"0.1.0+f8d984f2"},{"checks":[{"durationMs":1332,"exitCode":0,"name":"build","output":"Compiling 23 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.23s\nCompiler run successful!\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:17\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:79\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                                                                               ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:35\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                                   ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IdentityToken.sol:179:24\n    │\n179 │         return address(uint160(word));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint160' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":93,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 46 tests for test/IdentityToken.t.sol:IdentityTokenTest\n[PASS] testFuzz_feeConservesSupply(address,address,uint256,uint256) (runs: 256, μ: 263962, ~: 269913)\nLogs:\n  Bound result 572502949468643647826658777\n  Bound result 10014496422324663894\n\n[PASS] test_RevertWhen_approveZeroSpender() (gas: 30809)\n[PASS] test_RevertWhen_deployScriptHasFactoryButNoFeeRecipient() (gas: 1329426)\n[PASS] test_RevertWhen_factorySetsFeeRecipient() (gas: 34941)\n[PASS] test_RevertWhen_feeRecipientSetToZero() (gas: 32307)\n[PASS] test_RevertWhen_launchDeploymentPassesZeroFeeRecipient() (gas: 35745)\n[PASS] test_RevertWhen_strangerAcceptsFeeRecipient() (gas: 134970)\n[PASS] test_RevertWhen_strangerSetsFeeRecipient() (gas: 32689)\n[PASS] test_RevertWhen_transferExceedsBalance() (gas: 91182)\n[PASS] test_RevertWhen_transferFromExceedsAllowance() (gas: 138560)\n[PASS] test_RevertWhen_transferFromWithoutAllowance() (gas: 91477)\n[PASS] test_RevertWhen_transferToZeroAddress() (gas: 88751)\n[PASS] test_approveEmitsAndOverwrites() (gas: 92979)\n[PASS] test_constructorEmitsMintAndFeeRecipient() (gas: 8089)\n[PASS] test_deployScriptUsesTheGivenConfig() (gas: 2119256)\n[PASS] test_deployScriptZeroFeeRecipientMeansTheDeployerOutsideALaunch() (gas: 2079417)\n[PASS] test_directDeploymentWithoutLaunchTaxesOrdinaryTransfers() (gas: 146810)\n[PASS] test_dirtyWordFactoryLookupDoesNotBrickTransfers() (gas: 376259)\n[PASS] test_distributorIsReadFromFactoryAtTransferTime() (gas: 202651)\n[PASS] test_factoryAsEoaIsExemptButNotQueried() (gas: 126483)\n[PASS] test_factoryToDistributorAndClaimAreWhole() (gas: 160452)\n[PASS] test_factoryTransfersToAnyoneAreWhole() (gas: 79193)\n[PASS] test_feeRecipientCannotMoveOrFreezeHolders() (gas: 190425)\n[PASS] test_feeRecipientDefaultsToDeployerOutsideALaunch() (gas: 18012)\n[PASS] test_feeRecipientHandOffIsTwoStep() (gas: 380979)\n[PASS] test_feeRoundsDownForDustAmounts() (gas: 237618)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 203828)\n[PASS] test_knownLimit_relayThroughPoolManagerIsFeeFree() (gas: 1221910)\n[PASS] test_launchDeploymentBindsTheFeeToTheRequesterNotTheFactory() (gas: 174364)\n[PASS] test_launchPartiesRecorded() (gas: 51961)\n[PASS] test_malformedFactoryLookupDoesNotBrickTransfers() (gas: 233512)\n[PASS] test_metadata() (gas: 34940)\n[PASS] test_mistypedProposalDoesNotStrandTheFeeStream() (gas: 133073)\n[PASS] test_noMintSurface() (gas: 106048)\n[PASS] test_ordinaryTransferPaysEightPercentToFeeRecipient() (gas: 200605)\n[PASS] test_ordinaryTransfersStillTaxedWhenFactoryHasNoDistributor() (gas: 192360)\n[PASS] test_otherLaunchNumbersDistributorIsNotExempt() (gas: 198251)\n[PASS] test_overlongFactoryAnswerReadsAsNoDistributor() (gas: 103062)\n[PASS] test_poolManagerFlowsAreWholeBothWays() (gas: 347504)\n[PASS] test_previousRecipientLosesTheRoleAfterAcceptance() (gas: 110223)\n[PASS] test_revertingFactoryLookupDoesNotBrickTransfers() (gas: 271273)\n[PASS] test_selfTransferPaysFee() (gas: 146918)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 31340)\n[PASS] test_transferFromPaysFeeAndSpendsAllowance() (gas: 247363)\n[PASS] test_transfersTouchingTheFeeRecipientAreWhole() (gas: 192239)\n[PASS] test_zeroAmountTransferSucceedsWithoutFee() (gas: 51906)\nSuite result: ok. 46 passed; 0 failed; 0 skipped; finished in 12.64ms (28.35ms CPU time)\n\nRan 1 test suite in 13.25ms (12.64ms CPU time): 46 tests passed, 0 failed, 0 skipped (46 total tests)\n","passed":true},{"durationMs":32,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IdentityToken.acceptFeeRecipient()\",\"IdentityToken.approve(address,uint256)\",\"IdentityToken.setFeeRecipient(address)\",\"IdentityToken.transfer(address,uint256)\",\"IdentityToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":192,\"foundry.toml\":20,\"launch.json\":25,\"remappings.txt\":1,\"script/DeployIdentityToken.s.sol\":40,\"src/IdentityToken.sol\":245,\"test/IdentityToken.t.sol\":707},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a0de981d38d5db51eec1690d9128a57159ed08117f48fb05c84df183b419edcd","verifiedTreeHash":"89c092d5b0e581cf9cffb1d5e7004cca32e75782","verifierVersion":"0.1.0+f8d984f2"},{"checks":[{"durationMs":1122,"exitCode":0,"name":"build","output":"Compiling 23 files with Solc 0.8.26\nSolc 0.8.26 finished in 996.85ms\nCompiler run successful!\nwarning[missing-events-access-control]: `feeRecipient` is changed without an event but is used for access control\n    ╭▸ src/IdentityToken.sol:116:9\n    │\n116 │         feeRecipient = newRecipient;\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:17\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                 ━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:35\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                                   ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:79\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                                                                               ━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\n","passed":true},{"durationMs":99,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 37 tests for test/IdentityToken.t.sol:IdentityTokenTest\n[PASS] testFuzz_feeConservesSupply(address,address,uint256,uint256) (runs: 256, μ: 262765, ~: 269643)\nLogs:\n  Bound result 17\n  Bound result 0\n\n[PASS] test_RevertWhen_approveZeroSpender() (gas: 30803)\n[PASS] test_RevertWhen_factorySetsFeeRecipient() (gas: 34968)\n[PASS] test_RevertWhen_feeRecipientSetToZero() (gas: 32234)\n[PASS] test_RevertWhen_strangerSetsFeeRecipient() (gas: 32627)\n[PASS] test_RevertWhen_transferExceedsBalance() (gas: 91236)\n[PASS] test_RevertWhen_transferFromExceedsAllowance() (gas: 138602)\n[PASS] test_RevertWhen_transferFromWithoutAllowance() (gas: 91503)\n[PASS] test_RevertWhen_transferToZeroAddress() (gas: 88827)\n[PASS] test_approveEmitsAndOverwrites() (gas: 92872)\n[PASS] test_constructorEmitsMintAndFeeRecipient() (gas: 8099)\n[PASS] test_deployScriptUsesTheGivenConfig() (gas: 2021783)\n[PASS] test_deployScriptZeroFeeRecipientMeansTheDeployer() (gas: 1982015)\n[PASS] test_directDeploymentWithoutLaunchTaxesOrdinaryTransfers() (gas: 146681)\n[PASS] test_distributorIsReadFromFactoryAtTransferTime() (gas: 202555)\n[PASS] test_factoryAsEoaIsExemptButNotQueried() (gas: 126372)\n[PASS] test_factoryToDistributorAndClaimAreWhole() (gas: 160336)\n[PASS] test_factoryTransfersToAnyoneAreWhole() (gas: 79130)\n[PASS] test_feeRecipientCanHandOn() (gas: 208156)\n[PASS] test_feeRecipientCannotMoveOrFreezeHolders() (gas: 190381)\n[PASS] test_feeRecipientDefaultsToDeployer() (gas: 17944)\n[PASS] test_feeRoundsDownForDustAmounts() (gas: 237553)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 203804)\n[PASS] test_launchPartiesRecorded() (gas: 51758)\n[PASS] test_malformedFactoryLookupDoesNotBrickTransfers() (gas: 233389)\n[PASS] test_metadata() (gas: 34758)\n[PASS] test_noMintSurface() (gas: 105981)\n[PASS] test_ordinaryTransferPaysEightPercentToFeeRecipient() (gas: 200533)\n[PASS] test_ordinaryTransfersStillTaxedWhenFactoryHasNoDistributor() (gas: 192404)\n[PASS] test_otherLaunchNumbersDistributorIsNotExempt() (gas: 198283)\n[PASS] test_poolManagerFlowsAreWholeBothWays() (gas: 347253)\n[PASS] test_revertingFactoryLookupDoesNotBrickTransfers() (gas: 271108)\n[PASS] test_selfTransferPaysFee() (gas: 146836)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 31245)\n[PASS] test_transferFromPaysFeeAndSpendsAllowance() (gas: 247246)\n[PASS] test_transfersTouchingTheFeeRecipientAreWhole() (gas: 192114)\n[PASS] test_zeroAmountTransferSucceedsWithoutFee() (gas: 51924)\nSuite result: ok. 37 passed; 0 failed; 0 skipped; finished in 8.78ms (16.62ms CPU time)\n\nRan 1 test suite in 10.28ms (8.78ms CPU time): 37 tests passed, 0 failed, 0 skipped (37 total tests)\n","passed":true},{"durationMs":47,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IdentityToken.approve(address,uint256)\",\"IdentityToken.setFeeRecipient(address)\",\"IdentityToken.transfer(address,uint256)\",\"IdentityToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":153,\"foundry.toml\":20,\"remappings.txt\":1,\"script/DeployIdentityToken.s.sol\":39,\"src/IdentityToken.sol\":215,\"test/IdentityToken.t.sol\":485},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1295,"exitCode":0,"name":"slither","output":"[low/medium] missing-zero-check at src/IdentityToken.sol:97: IdentityToken.constructor(address,address,uint64,address).recipient (src/IdentityToken.sol#97) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IdentityToken.sol:92: IdentityToken.constructor(address,address,uint64,address).factory_ (src/IdentityToken.sol#92) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IdentityToken.sol:92: IdentityToken.constructor(address,address,uint64,address).poolManager_ (src/IdentityToken.sol#92) lacks a zero-check on :","passed":true},{"durationMs":627,"exitCode":0,"name":"aderyn","output":"[low] large-numeric-literal at src/IdentityToken.sol:38: Large Numeric Literal (2 places)\n[low] state-variable-could-be-immutable at src/IdentityToken.sol:62: State Variable Could Be Immutable","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"cda5835b7e36af78d6bf4020df61960573905062d70a7c0c3c3a44f6f231268a","verifiedTreeHash":"03f1d6722ebc5f0f71bc4a1e08e751f99e190822","verifierVersion":"0.1.0+f8d984f2"},{"checks":[{"durationMs":937,"exitCode":0,"name":"build","output":"Compiling 23 files with Solc 0.8.26\nSolc 0.8.26 finished in 839.85ms\nCompiler run successful!\nwarning[missing-events-access-control]: `feeRecipient` is changed without an event but is used for access control\n    ╭▸ src/IdentityToken.sol:116:9\n    │\n116 │         feeRecipient = newRecipient;\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:79\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                                                                               ━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:17\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                 ━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IdentityToken.sol:92:35\n   │\n92 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n   │                                   ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\n","passed":true},{"durationMs":86,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 37 tests for test/IdentityToken.t.sol:IdentityTokenTest\n[PASS] testFuzz_feeConservesSupply(address,address,uint256,uint256) (runs: 256, μ: 263469, ~: 269613)\nLogs:\n  Bound result 405572850389743264304264041\n  Bound result 312482356473974672558392764\n\n[PASS] test_RevertWhen_approveZeroSpender() (gas: 30803)\n[PASS] test_RevertWhen_factorySetsFeeRecipient() (gas: 34968)\n[PASS] test_RevertWhen_feeRecipientSetToZero() (gas: 32234)\n[PASS] test_RevertWhen_strangerSetsFeeRecipient() (gas: 32627)\n[PASS] test_RevertWhen_transferExceedsBalance() (gas: 91236)\n[PASS] test_RevertWhen_transferFromExceedsAllowance() (gas: 138602)\n[PASS] test_RevertWhen_transferFromWithoutAllowance() (gas: 91503)\n[PASS] test_RevertWhen_transferToZeroAddress() (gas: 88827)\n[PASS] test_approveEmitsAndOverwrites() (gas: 92872)\n[PASS] test_constructorEmitsMintAndFeeRecipient() (gas: 8099)\n[PASS] test_deployScriptUsesTheGivenConfig() (gas: 2021783)\n[PASS] test_deployScriptZeroFeeRecipientMeansTheDeployer() (gas: 1982015)\n[PASS] test_directDeploymentWithoutLaunchTaxesOrdinaryTransfers() (gas: 146681)\n[PASS] test_distributorIsReadFromFactoryAtTransferTime() (gas: 202555)\n[PASS] test_factoryAsEoaIsExemptButNotQueried() (gas: 126372)\n[PASS] test_factoryToDistributorAndClaimAreWhole() (gas: 160336)\n[PASS] test_factoryTransfersToAnyoneAreWhole() (gas: 79130)\n[PASS] test_feeRecipientCanHandOn() (gas: 208156)\n[PASS] test_feeRecipientCannotMoveOrFreezeHolders() (gas: 190381)\n[PASS] test_feeRecipientDefaultsToDeployer() (gas: 17944)\n[PASS] test_feeRoundsDownForDustAmounts() (gas: 237553)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 203804)\n[PASS] test_launchPartiesRecorded() (gas: 51758)\n[PASS] test_malformedFactoryLookupDoesNotBrickTransfers() (gas: 233389)\n[PASS] test_metadata() (gas: 34758)\n[PASS] test_noMintSurface() (gas: 105981)\n[PASS] test_ordinaryTransferPaysEightPercentToFeeRecipient() (gas: 200533)\n[PASS] test_ordinaryTransfersStillTaxedWhenFactoryHasNoDistributor() (gas: 192404)\n[PASS] test_otherLaunchNumbersDistributorIsNotExempt() (gas: 198283)\n[PASS] test_poolManagerFlowsAreWholeBothWays() (gas: 347253)\n[PASS] test_revertingFactoryLookupDoesNotBrickTransfers() (gas: 271108)\n[PASS] test_selfTransferPaysFee() (gas: 146836)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 31245)\n[PASS] test_transferFromPaysFeeAndSpendsAllowance() (gas: 247246)\n[PASS] test_transfersTouchingTheFeeRecipientAreWhole() (gas: 192114)\n[PASS] test_zeroAmountTransferSucceedsWithoutFee() (gas: 51924)\nSuite result: ok. 37 passed; 0 failed; 0 skipped; finished in 8.42ms (17.36ms CPU time)\n\nRan 1 test suite in 9.05ms (8.42ms CPU time): 37 tests passed, 0 failed, 0 skipped (37 total tests)\n","passed":true},{"durationMs":40,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IdentityToken.approve(address,uint256)\",\"IdentityToken.setFeeRecipient(address)\",\"IdentityToken.transfer(address,uint256)\",\"IdentityToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":153,\"foundry.toml\":20,\"launch.json\":25,\"remappings.txt\":1,\"script/DeployIdentityToken.s.sol\":39,\"src/IdentityToken.sol\":215,\"test/IdentityToken.t.sol\":485},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"cea81866b4ddaddeaa8aea17440fdd08c4cff379beaadaab3bd2be677c3a3733","verifiedTreeHash":"fe2ca9f678a72700d5b56fe130b2842bd127053b","verifierVersion":"0.1.0+f8d984f2"},{"checks":[{"durationMs":1899,"exitCode":0,"name":"build","output":"Compiling 26 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.81s\nCompiler run successful!\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:35\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                                   ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:17\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:79\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                                                                               ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IdentityToken.sol:179:24\n    │\n179 │         return address(uint160(word));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint160' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":14378,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 46 tests for test/IdentityToken.t.sol:IdentityTokenTest\n[PASS] testFuzz_feeConservesSupply(address,address,uint256,uint256) (runs: 256, μ: 273550, ~: 280013)\nLogs:\n  Bound result 157198260\n  Bound result 151547624\n\n[PASS] test_RevertWhen_approveZeroSpender() (gas: 30809)\n[PASS] test_RevertWhen_deployScriptHasFactoryButNoFeeRecipient() (gas: 1329426)\n[PASS] test_RevertWhen_factorySetsFeeRecipient() (gas: 34941)\n[PASS] test_RevertWhen_feeRecipientSetToZero() (gas: 32307)\n[PASS] test_RevertWhen_launchDeploymentPassesZeroFeeRecipient() (gas: 35745)\n[PASS] test_RevertWhen_strangerAcceptsFeeRecipient() (gas: 134970)\n[PASS] test_RevertWhen_strangerSetsFeeRecipient() (gas: 32689)\n[PASS] test_RevertWhen_transferExceedsBalance() (gas: 91182)\n[PASS] test_RevertWhen_transferFromExceedsAllowance() (gas: 138560)\n[PASS] test_RevertWhen_transferFromWithoutAllowance() (gas: 91477)\n[PASS] test_RevertWhen_transferToZeroAddress() (gas: 88751)\n[PASS] test_approveEmitsAndOverwrites() (gas: 92979)\n[PASS] test_constructorEmitsMintAndFeeRecipient() (gas: 8089)\n[PASS] test_deployScriptUsesTheGivenConfig() (gas: 2119256)\n[PASS] test_deployScriptZeroFeeRecipientMeansTheDeployerOutsideALaunch() (gas: 2079417)\n[PASS] test_directDeploymentWithoutLaunchTaxesOrdinaryTransfers() (gas: 146810)\n[PASS] test_dirtyWordFactoryLookupDoesNotBrickTransfers() (gas: 376259)\n[PASS] test_distributorIsReadFromFactoryAtTransferTime() (gas: 202651)\n[PASS] test_factoryAsEoaIsExemptButNotQueried() (gas: 126483)\n[PASS] test_factoryToDistributorAndClaimAreWhole() (gas: 160452)\n[PASS] test_factoryTransfersToAnyoneAreWhole() (gas: 79193)\n[PASS] test_feeRecipientCannotMoveOrFreezeHolders() (gas: 190425)\n[PASS] test_feeRecipientDefaultsToDeployerOutsideALaunch() (gas: 18012)\n[PASS] test_feeRecipientHandOffIsTwoStep() (gas: 380979)\n[PASS] test_feeRoundsDownForDustAmounts() (gas: 237618)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 203828)\n[PASS] test_knownLimit_relayThroughPoolManagerIsFeeFree() (gas: 1221910)\n[PASS] test_launchDeploymentBindsTheFeeToTheRequesterNotTheFactory() (gas: 174364)\n[PASS] test_launchPartiesRecorded() (gas: 51961)\n[PASS] test_malformedFactoryLookupDoesNotBrickTransfers() (gas: 233512)\n[PASS] test_metadata() (gas: 34940)\n[PASS] test_mistypedProposalDoesNotStrandTheFeeStream() (gas: 133073)\n[PASS] test_noMintSurface() (gas: 106048)\n[PASS] test_ordinaryTransferPaysEightPercentToFeeRecipient() (gas: 200605)\n[PASS] test_ordinaryTransfersStillTaxedWhenFactoryHasNoDistributor() (gas: 192360)\n[PASS] test_otherLaunchNumbersDistributorIsNotExempt() (gas: 198251)\n[PASS] test_overlongFactoryAnswerReadsAsNoDistributor() (gas: 103062)\n[PASS] test_poolManagerFlowsAreWholeBothWays() (gas: 347504)\n[PASS] test_previousRecipientLosesTheRoleAfterAcceptance() (gas: 110223)\n[PASS] test_revertingFactoryLookupDoesNotBrickTransfers() (gas: 271273)\n[PASS] test_selfTransferPaysFee() (gas: 146918)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 31340)\n[PASS] test_transferFromPaysFeeAndSpendsAllowance() (gas: 247363)\n[PASS] test_transfersTouchingTheFeeRecipientAreWhole() (gas: 192239)\n[PASS] test_zeroAmountTransferSucceedsWithoutFee() (gas: 51906)\nSuite result: ok. 46 passed; 0 failed; 0 skipped; finished in 46.93ms (47.09ms CPU time)\n\nRan 27 tests for test/IdentityTokenAdversarial.t.sol:IdentityTokenAdversarialTest\n[PASS] testFuzz_failedTransferFromPreservesEverything(uint256,uint256) (runs: 1000, μ: 273144, ~: 273813)\nLogs:\n  Bound result 2112\n  Bound result 115792089237316195423570985008687907853269984665640564039457584007913129637923\n\n[PASS] testFuzz_feeRoundingAndPeriodOnTransferableDomain(uint256) (runs: 1000, μ: 22107, ~: 21846)\nLogs:\n  Bound result 713786811792496964124558274\n\n[PASS] testFuzz_roundTripLossEqualsDeployerFees(uint256) (runs: 1000, μ: 235034, ~: 236733)\nLogs:\n  Bound result 383012312874258027047291435\n\n[PASS] testFuzz_transferFromConservesBalancesAndSpendsGrossAllowance(uint256,uint256) (runs: 1000, μ: 246926, ~: 249793)\nLogs:\n  Bound result 999\n  Bound result 19038253646551487\n\n[PASS] test_acceptWithNothingPendingIsRefusedForEveryone() (gas: 239410)\n[PASS] test_acceptanceCannotBeReplayedAfterTheRoleMovesOn() (gas: 218617)\n[PASS] test_allowanceCoversGrossAmountIncludingFee() (gas: 258451)\n[PASS] test_delegatedSelfTransferSpendsGrossAllowanceAndOnlyFeeFromBalance() (gas: 181288)\n[PASS] test_feeRecipientCannotSetZeroOrStealUsingTransferFrom() (gas: 297317)\n[PASS] test_feeRecipientHandoffPreservesHoldingsAndRevokesOldAuthority() (gas: 732045)\n[PASS] test_fullSupplySelfTransferCountsHolderOnce() (gas: 151271)\n[PASS] test_laterProposalReplacesEarlierOneAndTheEarlierProposeeIsRefused() (gas: 194578)\n[PASS] test_maximumTransferFailsWithBalanceErrorAndNoChanges() (gas: 154184)\n[PASS] test_maximumTransferFromPreservesInfiniteApprovalOnFailure() (gas: 261447)\n[PASS] test_proposalToSelfIsHarmlessAndClearsOnAcceptance() (gas: 135369)\n[PASS] test_repeatedInfiniteAllowanceTransfersDoNotConsumeApproval() (gas: 258343)\n[PASS] test_revocationOverwritesInfiniteAllowanceImmediately() (gas: 283873)\n[PASS] test_roundingEdgesIncludingWholeSupply() (gas: 1488524)\n[PASS] test_spentAllowanceCannotBeReused() (gas: 336224)\n[PASS] test_transferFromInsufficientBalanceRestoresFiniteAllowance() (gas: 266031)\n[PASS] test_transferFromInvalidReceiverRestoresAllowance() (gas: 263322)\n[PASS] test_zeroAndOneWeiTransfersEmitTransferWithoutFee() (gas: 175623)\n[PASS] test_zeroApprovalStillRejectsZeroSpender() (gas: 162598)\n[PASS] test_zeroTransferFromNeedsNoAllowanceAndChangesNoBalances() (gas: 165127)\n[PASS] test_zeroTransferFromRejectsZeroReceiver() (gas: 156729)\n[PASS] test_zeroTransferFromRejectsZeroSender() (gas: 167349)\n[PASS] test_zeroTransferStillRejectsZeroReceiver() (gas: 151773)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 52.55ms (191.26ms CPU time)\n\nRan 2 tests for test/IdentityTokenInvariant.t.sol:IdentityTokenInvariantTest\n[PASS] invariant_supplyBalancesAllowancesAndRolesMatchIndependentModel() (runs: 256, calls: 16384, reverts: 0)\n\n╭----------------------+-----------------------------+-------+---------+----------╮\n| Contract             | Selector                    | Calls | Reverts | Discards |\n+=================================================================================+\n| IdentityTokenHandler | acceptFeeRecipient          | 1479  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | approve                     | 1479  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | changeDistributor           | 1463  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | proposeFeeRecipient         | 1552  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectDelegatedTransfer     | 1509  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectInvalidAcceptance     | 1443  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectInvalidAdministration | 1431  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectOverdraw              | 1530  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | rejectUnauthorizedPull      | 1495  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | transfer                    | 1494  | 0       | 0        |\n|----------------------+-----------------------------+-------+---------+----------|\n| IdentityTokenHandler | transferFrom                | 1509  | 0       | 0        |\n╰----------------------+-----------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 2478893113\n  Bound result 161\n  Bound result 19214619364871829660268969039931787667372045428790046439049882358527636971346\n  Bound result 115792089237316195423570985008687907853269984665640264039557584007913129639935\n  Bound result 0\n  Bound result 57005\n  Bound result 1\n  Bound result 2\n  Bound result 115792089237316195423570985008687907853269984665640264039457584007913129676801\n  Bound result 115792089237316195423570985008687907853269984665640464039457584007913129639955\n  Bound result 3199\n  Bound result 11\n  Bound result 113961276559525676\n  Bound result 115792089237316195423570985008687907853269984665640564039457584007913129639935\n  Bound result 0\n  Bound result 75776994848481170030766896\n  Bound result 115792089237316195423570985008687907853269984665640464039457584007913129649996\n\n[PASS] test_handlerExercisesTaxedExemptSelfDelegatedAndRejectedCalls() (gas: 6259142)\nLogs:\n  Bound result 100\n  Bound result 100\n  Bound result 100\n  Bound result 100\n  Bound result 100\n  Bound result 100\n  Bound result 115792089237316195423570985008687907853269984665640564039457584007913129639935\n  Bound result 1\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 14.30s (14.30s CPU time)\n\nRan 3 test suites in 14.30s (14.40s CPU time): 75 tests passed, 0 failed, 0 skipped (75 total tests)\n","passed":true},{"durationMs":40,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IdentityToken.acceptFeeRecipient()\",\"IdentityToken.approve(address,uint256)\",\"IdentityToken.setFeeRecipient(address)\",\"IdentityToken.transfer(address,uint256)\",\"IdentityToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":192,\"foundry.toml\":20,\"remappings.txt\":1,\"script/DeployIdentityToken.s.sol\":40,\"src/IdentityToken.sol\":245,\"test/IdentityToken.t.sol\":710,\"test/IdentityTokenAdversarial.t.sol\":400,\"test/IdentityTokenInvariant.t.sol\":106,\"test/helpers/IdentityTokenHandler.sol\":223},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"efe2ef7cd616c0c184d0534a4a43788dc91292ebf25f6fa5358910de3d5e92ed","verifiedTreeHash":"ec3da841d08785080bb924d27380094722a2c084","verifierVersion":"0.1.0+f8d984f2"},{"checks":[{"durationMs":1332,"exitCode":0,"name":"build","output":"Compiling 23 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.21s\nCompiler run successful!\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:35\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                                   ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:79\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                                                                               ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IdentityToken.sol:104:17\n    │\n104 │     constructor(address factory_, address poolManager_, uint64 launchNumber_, address feeRecipient_) {\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IdentityToken.sol:179:24\n    │\n179 │         return address(uint160(word));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint160' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":82,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 46 tests for test/IdentityToken.t.sol:IdentityTokenTest\n[PASS] testFuzz_feeConservesSupply(address,address,uint256,uint256) (runs: 256, μ: 262473, ~: 269865)\nLogs:\n  Bound result 194981160873276\n  Bound result 85703067925939\n\n[PASS] test_RevertWhen_approveZeroSpender() (gas: 30809)\n[PASS] test_RevertWhen_deployScriptHasFactoryButNoFeeRecipient() (gas: 1329426)\n[PASS] test_RevertWhen_factorySetsFeeRecipient() (gas: 34941)\n[PASS] test_RevertWhen_feeRecipientSetToZero() (gas: 32307)\n[PASS] test_RevertWhen_launchDeploymentPassesZeroFeeRecipient() (gas: 35745)\n[PASS] test_RevertWhen_strangerAcceptsFeeRecipient() (gas: 134970)\n[PASS] test_RevertWhen_strangerSetsFeeRecipient() (gas: 32689)\n[PASS] test_RevertWhen_transferExceedsBalance() (gas: 91182)\n[PASS] test_RevertWhen_transferFromExceedsAllowance() (gas: 138560)\n[PASS] test_RevertWhen_transferFromWithoutAllowance() (gas: 91477)\n[PASS] test_RevertWhen_transferToZeroAddress() (gas: 88751)\n[PASS] test_approveEmitsAndOverwrites() (gas: 92979)\n[PASS] test_constructorEmitsMintAndFeeRecipient() (gas: 8089)\n[PASS] test_deployScriptUsesTheGivenConfig() (gas: 2119256)\n[PASS] test_deployScriptZeroFeeRecipientMeansTheDeployerOutsideALaunch() (gas: 2079417)\n[PASS] test_directDeploymentWithoutLaunchTaxesOrdinaryTransfers() (gas: 146810)\n[PASS] test_dirtyWordFactoryLookupDoesNotBrickTransfers() (gas: 376259)\n[PASS] test_distributorIsReadFromFactoryAtTransferTime() (gas: 202651)\n[PASS] test_factoryAsEoaIsExemptButNotQueried() (gas: 126483)\n[PASS] test_factoryToDistributorAndClaimAreWhole() (gas: 160452)\n[PASS] test_factoryTransfersToAnyoneAreWhole() (gas: 79193)\n[PASS] test_feeRecipientCannotMoveOrFreezeHolders() (gas: 190425)\n[PASS] test_feeRecipientDefaultsToDeployerOutsideALaunch() (gas: 18012)\n[PASS] test_feeRecipientHandOffIsTwoStep() (gas: 380979)\n[PASS] test_feeRoundsDownForDustAmounts() (gas: 237618)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 203828)\n[PASS] test_knownLimit_relayThroughPoolManagerIsFeeFree() (gas: 1221910)\n[PASS] test_launchDeploymentBindsTheFeeToTheRequesterNotTheFactory() (gas: 174364)\n[PASS] test_launchPartiesRecorded() (gas: 51961)\n[PASS] test_malformedFactoryLookupDoesNotBrickTransfers() (gas: 233512)\n[PASS] test_metadata() (gas: 34940)\n[PASS] test_mistypedProposalDoesNotStrandTheFeeStream() (gas: 133073)\n[PASS] test_noMintSurface() (gas: 106048)\n[PASS] test_ordinaryTransferPaysEightPercentToFeeRecipient() (gas: 200605)\n[PASS] test_ordinaryTransfersStillTaxedWhenFactoryHasNoDistributor() (gas: 192360)\n[PASS] test_otherLaunchNumbersDistributorIsNotExempt() (gas: 198251)\n[PASS] test_overlongFactoryAnswerReadsAsNoDistributor() (gas: 103062)\n[PASS] test_poolManagerFlowsAreWholeBothWays() (gas: 347504)\n[PASS] test_previousRecipientLosesTheRoleAfterAcceptance() (gas: 110223)\n[PASS] test_revertingFactoryLookupDoesNotBrickTransfers() (gas: 271273)\n[PASS] test_selfTransferPaysFee() (gas: 146918)\n[PASS] test_supplyMintedOnceToDeployer() (gas: 31340)\n[PASS] test_transferFromPaysFeeAndSpendsAllowance() (gas: 247363)\n[PASS] test_transfersTouchingTheFeeRecipientAreWhole() (gas: 192239)\n[PASS] test_zeroAmountTransferSucceedsWithoutFee() (gas: 51906)\nSuite result: ok. 46 passed; 0 failed; 0 skipped; finished in 8.24ms (16.81ms CPU time)\n\nRan 1 test suite in 9.16ms (8.24ms CPU time): 46 tests passed, 0 failed, 0 skipped (46 total tests)\n","passed":true},{"durationMs":48,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IdentityToken.acceptFeeRecipient()\",\"IdentityToken.approve(address,uint256)\",\"IdentityToken.setFeeRecipient(address)\",\"IdentityToken.transfer(address,uint256)\",\"IdentityToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":192,\"foundry.toml\":20,\"remappings.txt\":1,\"script/DeployIdentityToken.s.sol\":40,\"src/IdentityToken.sol\":245,\"test/IdentityToken.t.sol\":707},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":384,"exitCode":0,"name":"slither","output":"[low/medium] missing-zero-check at src/IdentityToken.sol:104: IdentityToken.constructor(address,address,uint64,address).factory_ (src/IdentityToken.sol#104) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IdentityToken.sol:110: IdentityToken.constructor(address,address,uint64,address).recipient (src/IdentityToken.sol#110) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IdentityToken.sol:104: IdentityToken.constructor(address,address,uint64,address).poolManager_ (src/IdentityToken.sol#104) lacks a zero-check on :","passed":true},{"durationMs":126,"exitCode":0,"name":"aderyn","output":"[low] large-numeric-literal at src/IdentityToken.sol:43: Large Numeric Literal (2 places)\n[low] state-variable-could-be-immutable at src/IdentityToken.sol:69: State Variable Could Be Immutable","passed":true},{"durationMs":1000,"exitCode":0,"name":"proof 73ddd17a6c15","output":"Compiling 21 files with Solc 0.8.26\nSolc 0.8.26 finished in 540.73ms\nCompiler run successful!\n\nRan 1 test for test/imd-proof-85f4e95f/Proof_73ddd17a6c15.t.sol:ZeroFeeRecipientUnderFactoryTest\n[PASS] test_factoryDeploymentWithZeroRecipientMustNotStrandTheFee() (gas: 30306)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 379.27µs (199.71µs CPU time)\n\nRan 1 test suite in 2.29ms (379.27µ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":"f088be11e941d5371f542739081f76417220452bb5f9fb69776ca68a8a75d363","verifiedTreeHash":"87610a6bf23c6010228beedd032d334ee4e2f34d","verifierVersion":"0.1.0+f8d984f2"}]}