{"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":"d8208131-5e99-4362-9e61-fe6b6e22088e","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"b67c7c9164191d388b46b8d7a0909515aecf1a93a27acf17b47f6f358458ea95","dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"a2cea02e8b33e91eeb784e0e5eaeb571cd62cd69e768d335d5834e24b3aa46e5","skillId":"adapt-contract-project","tools":[]},"key":"adapt_contract_project","kind":"code","role":"implement","skillHash":"a2cea02e8b33e91eeb784e0e5eaeb571cd62cd69e768d335d5834e24b3aa46e5","skillId":"adapt-contract-project","state":"accepted"},{"acceptedSubmissionHash":"8e7d0254f27e20880b704834fa98989af3af7f8305b97a47a137696d635b63a7","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"9d871fb845943a3d9d9de9e30c56c7b82e8653ce89f679f78048a77b251ad3f0","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"0132e8e02ce223a7558f8927ba075b309ffa7cdb74bb68a3553bbe0bf08c6127","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","state":"accepted"},{"acceptedSubmissionHash":"bd147152c173d15ac59a8ccc060467c46d715d893ec6000ca952f3c8444c1d6c","dependsOn":["adapt_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"9e90f603b13a225738f841488ed2c1ba12c14a006229266736bc06f8381ee9a0","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"62711f2d8b9b33c9e95e46fc006d20f56d4f6be9716d2b37abeb0357fb9bfaad","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"e87d7da764328bb5402d000d324af5c7dfa855314d06da8bcf7a26411aa4136d","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"1a6394b385f87a5a8f89bafd0250a9cfa071a2008d886da8a5067ce444e27a4b","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Deploy ShipOrBurnIMD (src/ShipOrBurnIMD.sol) unchanged to Ethereum mainnet. Deploy only ShipOrBurnIMD: no token, distributor or pool. It has no constructor arguments, no owner, no admin, no upgrade and no pause, and the factory receives no privileges. The constructor makes no external calls and needs no code at any other address.\n\nWhat it does: vesting by shipping. A funder locks tranche * tranches of an ERC-20 in a vault for a builder. Each verdict is one IMD oracle attestation of a counter that never goes down (merged pull requests of a GitHub repository). If the count rose since the last counted attestation, one tranche goes to the builder; if it stayed flat, one tranche goes to missTo (0xdEaD, or the funder for refund vaults). After the deadline anyone can call expire, which sends the rest to missTo.\n\nHow it uses IMD: settle() verifies an IMD OracleAttestation. The EIP-712 domain name IdentityMD Oracle and version 2 are IMD's own, with this contract as verifyingContract, and the signer is IMD's oracle attester 0x5598Aa9146215Bc13eb26f2c692Ad1461Fd32982; both are required for IMD's signatures to verify and must not change. questionHash() rebuilds IMD's question hash (keccak256 of the request's canonical JSON, whose last key is the window) from the vault's stored prefix, so only the vault's own question counts.\n\nKeep the code as it is unless the audit reproduces a critical or high issue. A keeper and a web app call this exact ABI: do not change function signatures, the Attestation struct, events, errors, constants or the typehash. forge test must pass (25 tests, including golden vectors from real IMD attestations). The build uses solc 0.8.26, optimizer 200 runs and bytecode_hash none, with dependencies vendored under lib; nothing needs installing.\n\nAccepted by the owner, do not change:\n\n(1) settle and expire are permissionless, and whoever calls settle chooses which valid attestation to submit. A verdict can be taken at any time at least MIN_SPACING (6,000 blocks, about 20 hours) after the last counted one, by anyone who pays IMD for an oracle request. The rule is therefore that a builder must ship within every spacing window, and a flat window costs one tranche. Attestations must be settled in block order; one older than the last counted is rejected.\n\n(2) Any merged pull request counts as a ship; the contract does not judge substance.\n\n(3) The IMD signer and the hash format are fixed. If IMD rotates its signer or changes the format, vaults stop settling and expire to missTo at their deadline. Vaults are meant to be short.\n\n(4) Deposits are balance-checked, so fee-on-transfer tokens are refused at creation; rebasing tokens and tokens that block transfers to 0xdEaD are unsupported and are the funder's risk.\n\n(5) MIN_SPACING assumes 12-second blocks: Ethereum mainnet and Sepolia only.\n\n(6) The question prefix is checked only for its opening (a uint256 answer about this chain) and its ending (the window key). The funder chooses the question and the builder accepts it by taking part.\n\n(7) linkSchedule only emits an event. The same attestation may settle several vaults that share a question.","parentJobId":null,"planHash":"dee43a962f461a205fe1e519f19847fd4752dff738a18e28796bb60a097e21f9","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"d8208131-5e99-4362-9e61-fe6b6e22088e","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51094","feedbackHash":"aad7fc995da31d545ddb2fe68e2abb5a870fcf17ce11554ed33113976e22ea5d","nodeKey":"adapt_contract_project","submissionHash":"b67c7c9164191d388b46b8d7a0909515aecf1a93a27acf17b47f6f358458ea95","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52174","feedbackHash":"ad8d491366ffcf479a1cf24d242aa0c78a133170db123f3d935c925421b064ea","nodeKey":"audit_economics","submissionHash":"8e7d0254f27e20880b704834fa98989af3af7f8305b97a47a137696d635b63a7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52232","feedbackHash":"a348e60e4a6826f5e382304717d23a201b2c1d918824ec458d4e46bbcf152cb6","nodeKey":"audit_flow","submissionHash":"9d871fb845943a3d9d9de9e30c56c7b82e8653ce89f679f78048a77b251ad3f0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52164","feedbackHash":"b1f1ecbceae50e776b90c8a0217d10cf34cc2021cbad4e322c2141cd9405bd59","nodeKey":"audit_imported_code","submissionHash":"0132e8e02ce223a7558f8927ba075b309ffa7cdb74bb68a3553bbe0bf08c6127","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52170","feedbackHash":"8c3013493ca9345e18d41aa5ea5469f63572fd1139b1e0c7529bf9d2d693274a","nodeKey":"audit_judge","submissionHash":"bd147152c173d15ac59a8ccc060467c46d715d893ec6000ca952f3c8444c1d6c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52163","feedbackHash":"5359d662bad47e598b0777e27a62eaa07892ccba434b5f6a593dacc5aa76d201","nodeKey":"audit_math","submissionHash":"9e90f603b13a225738f841488ed2c1ba12c14a006229266736bc06f8381ee9a0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52159","feedbackHash":"481966be3b76d4708159696981cd6e61d2fda363fe7ce9bef94f6abb366b87a3","nodeKey":"audit_permissions","submissionHash":"62711f2d8b9b33c9e95e46fc006d20f56d4f6be9716d2b37abeb0357fb9bfaad","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51205","feedbackHash":"ab84d42432b13193bb777440e9b999257afc66d041a4961ec0df41e9327b388d","nodeKey":"manifest","submissionHash":"e87d7da764328bb5402d000d324af5c7dfa855314d06da8bcf7a26411aa4136d","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51293","feedbackHash":"afdf95460dd4960a4734d7d74771fee36411270f2609120a1d523981f255f3a9","nodeKey":"write_foundry_tests","submissionHash":"1a6394b385f87a5a8f89bafd0250a9cfa071a2008d886da8a5067ce444e27a4b","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"0326c9b33f7163891479531eebcc9784234af034fc96f468986b997212445893","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"63458a1bd2b2d376","findings":[{"citation":"resolved","description":"The owner's accepted rule (1) is that a builder must ship within every MIN_SPACING window and a flat window costs one tranche. The contract does not enforce that rule. The only ordering check is that a.toBlock >= lastToBlock + 6000; nothing ties a.toBlock to block.number, to a.issuedAt, or to when the panel read the counter. With the production question ('the count at the moment you retrieve it; the block window is context only', test/ShipOrBurn.t.sol:66-70) the signed count is a function of the request time, not of toBlock. So two attestations requested minutes apart, with toBlocks 6000 or more blocks apart, are both valid and both carry the same live count: the first is a ship (if anything was merged since the last counted one) and the second is a flat window, whatever the builder merged in that window. Any party who pays IMD for two requests (1 IMD total) can do this once settlement lags a window behind the chain; an honest keeper that catches up after an outage by requesting one attestation per missed window does the same to its own builder. In a refund vault the burned tranche goes to the funder, who is the natural attacker and need only wait for (or cause, by running no keeper) a late settlement. In a burn vault it is pure griefing of the builder. If IMD accepts a block window whose toBlock is in the future, no lag is needed at all: request toBlock = now and toBlock = now + 6000 together and settle both immediately. Minimal contract-side fix that keeps the ABI: bound the staleness of a verdict relative to the chain when it is settled (for example require block.number - a.toBlock <= some MAX_LAG such as 600 blocks, and a.toBlock <= block.number), so the panel's reading, toBlock and the settlement all fall within a few hundred blocks of each other and consecutive verdicts are genuinely 6000 blocks apart in time. A lagging vault then settles one fresh verdict instead of catching up with several, and the rest expires to missTo at the deadline, which is the outcome the owner already accepts for lag. Alternatively the question could be changed to ask for the count as of the window's end (GitHub search supports merged:<=DATE), but the contract cannot verify that and the production prefix in the tests says the opposite.","line":209,"path":"src/ShipOrBurn.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ShipOrBurn} from \"src/ShipOrBurn.sol\";\n\n/// @dev Minimal ERC-20, enough for the vault to pull and pay.\ncontract PlainToken is IERC20 {\n    string public constant name = \"Plain\";\n    string public constant symbol = \"PLN\";\n    uint8 public constant decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n        emit Transfer(address(0), to, amount);\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        emit Approval(msg.sender, spender, amount);\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        _move(msg.sender, to, amount);\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        _move(from, to, amount);\n        return true;\n    }\n\n    function _move(address from, address to, uint256 amount) internal {\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        emit Transfer(from, to, amount);\n    }\n}\n\n/// @title A builder who ships in every spacing window still loses a tranche\n/// @notice `settle` spaces verdicts by `toBlock` but compares counts that the panel reads at\n///         retrieval time, and it never checks `toBlock` against `block.number`. Two attestations\n///         retrieved minutes apart, with `toBlock`s 6,000 blocks apart, are therefore both valid,\n///         and the second one is a guaranteed \"flat\" verdict whatever the builder did in that window.\n///         Fails on the current code (one tranche of a builder who shipped in both windows goes to\n///         missTo); passes once a verdict must be fresh relative to the chain when it is settled.\ncontract StaleVerdictProofTest is Test {\n    uint256 constant SIGNER_PK = 0xA11CE;\n    uint128 constant TRANCHE = 1e18;\n    uint16 constant TRANCHES = 3;\n\n    ShipOrBurn sob;\n    PlainToken token;\n    address signer;\n    address funder = makeAddr(\"funder\");\n    address builder = makeAddr(\"builder\");\n    bytes prefix;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.roll(26_100_000);\n        vm.warp(1_791_500_000);\n        signer = vm.addr(SIGNER_PK);\n        sob = new ShipOrBurn(signer);\n        token = new PlainToken();\n        token.mint(funder, 100e18);\n        vm.prank(funder);\n        token.approve(address(sob), type(uint256).max);\n        // the production question: the merged-PR count as the panel retrieves it, window as context\n        prefix = bytes(\n            string.concat(\n                '{\"answerType\":\"uint256\",\"chainId\":1,\"definitions\":{\"count\":\"The total_count field returned by https://api.github.com/search/issues?q=repo:shiporburn/ship-or-burn+is:pr+is:merged at retrieval.\",\"window\":\"The block window is context only. The answer is the count at the moment you retrieve it.\"},\"evidence\":\"panel\",\"question\":\"How many pull requests in the GitHub repository shiporburn/ship-or-burn have been merged, counted when you retrieve the evidence?\",\"v\":1,\"window\":'\n            )\n        );\n    }\n\n    function _att(uint64 toBlock, uint256 count) internal view returns (ShipOrBurn.Attestation memory a) {\n        uint64 fromBlock = toBlock - 300;\n        a = ShipOrBurn.Attestation({\n            requestId: bytes32(uint256(toBlock)),\n            chainId: 1,\n            questionHash: sob.questionHash(prefix, fromBlock, toBlock),\n            answerType: 3,\n            answer: abi.encode(count),\n            figure: count,\n            fromBlock: fromBlock,\n            toBlock: toBlock,\n            blockHash: keccak256(abi.encode(toBlock)),\n            panelJobId: bytes32(uint256(toBlock) << 128),\n            panelSize: 5,\n            quorum: 4,\n            agreed: 5,\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 7 days)\n        });\n    }\n\n    function _sign(ShipOrBurn.Attestation memory a) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PK, sob.attestationDigest(a));\n        return abi.encodePacked(r, s, v);\n    }\n\n    /// @dev Advances the chain by `blocks` at 12 seconds each, as mainnet does.\n    function _advance(uint64 blocks) internal {\n        vm.roll(block.number + blocks);\n        vm.warp(block.timestamp + uint256(blocks) * 12);\n    }\n\n    function test_builderWhoShippedEveryWindowStillLosesATranche() public {\n        vm.prank(funder);\n        uint256 id = sob.createVault(\n            IERC20(address(token)), builder, false, TRANCHE, TRANCHES, uint64(block.timestamp + 10 days), prefix\n        );\n        uint64 start = uint64(block.number);\n\n        // Baseline, requested right after creation for the window ending at the creation block:\n        // 7 merged pull requests. Settled promptly, 5 blocks later.\n        _advance(5);\n        ShipOrBurn.Attestation memory base = _att(start, 7);\n        sob.settle(id, base, _sign(base), prefix);\n\n        // Window 1 is blocks (start, start+6000]; window 2 is (start+6000, start+12000].\n        // The builder merges PR #8 inside window 1 and PR #9 inside window 2: shipped in both.\n        // Nobody settles during those two windows (a keeper outage, or a funder who simply waits).\n        _advance(12_000 - 5);\n        uint256 liveCount = 9;\n\n        // At block start+12000 two oracle requests are made minutes apart, one per window.\n        // The panel reads the live count for both: 9 and 9.\n        ShipOrBurn.Attestation memory first = _att(start + 6_000, liveCount);\n        bytes memory firstSig = _sign(first);\n        ShipOrBurn.Attestation memory second = _att(start + 12_000, liveCount);\n        bytes memory secondSig = _sign(second);\n\n        bool okFirst;\n        bool okSecond;\n        try sob.settle(id, first, firstSig, prefix) {\n            okFirst = true;\n        } catch {}\n        try sob.settle(id, second, secondSig, prefix) {\n            okSecond = true;\n        } catch {}\n\n        // The vault must still be settleable with a fresh verdict, and the builder must be paid for\n        // shipping (a fix that rejects every attestation would not do).\n        assertTrue(okFirst || okSecond, \"no verdict could be settled at all\");\n        assertGe(token.balanceOf(builder), TRANCHE, \"the builder shipped and was not paid\");\n\n        // The builder shipped in every spacing window, so nothing may have gone to missTo.\n        // On the current code the second attestation is counted as a flat window and one tranche\n        // is sent to 0xdEaD.\n        assertEq(token.balanceOf(sob.DEAD()), 0, \"a builder who shipped in every spacing window lost a tranche\");\n    }\n}","reproduction":"Vault: tranche 1e18, 3 tranches, burn vault, deadline now+10 days, created at block S (mainnet, 12 s blocks). Baseline: attestation for window ending at S, count 7, settled at S+5. Builder merges PR #8 at S+100 (window 1 = (S, S+6000]) and PR #9 at S+7000 (window 2 = (S+6000, S+12000]). Nobody settles until S+12000. At S+12000 two IMD requests are made minutes apart: A with window ending S+6000 and B with window ending S+12000; the panel reads the live counter for both, 9 and 9, and IMD signs both for this contract. settle(id, A): toBlock S+6000 >= S+6000, count 9 > 7 -> Shipped, builder +1e18. settle(id, B): toBlock S+12000 >= S+12000, count 9 == 9 -> Missed, 1e18 to 0xdEaD. Expected: the builder shipped in both windows, so no tranche goes to missTo. Actual: 1e18 of the builder's vesting is burned (or refunded to the funder in a refund vault). Run: forge test --match-path test/scratch/StaleVerdictProof.t.sol (fails on current code with 'a builder who shipped in every spacing window lost a tranche: 1000000000000000000 != 0'; passes with a freshness check such as `if (a.toBlock + 600 < block.number) revert ...;` after line 209).","severity":"high","snippet":"        if (a.toBlock < v.lastToBlock + MIN_SPACING) revert TooSoon();","title":"settle spaces verdicts by toBlock but compares counts read at retrieval time, so a stale attestation plus a fresh one makes a guaranteed 'flat' verdict and burns a builder who shipped in every window"},{"citation":"resolved","description":"Both payout paths are push transfers to a fixed recipient: a flat verdict pays v.missTo (line 225), a ship pays v.builder (line 221), and expire pays v.missTo (line 237). USDC/USDT-class tokens, likely choices for a vesting vault, can blocklist an address after the vault is funded. For a refund vault whose funder is blocklisted, every flat verdict reverts and expire reverts forever, so the vault can neither close nor pay out its remainder: the tokens sit in the contract permanently. A blocklisted builder has the mirror problem: no ship can ever settle, only misses, and at the deadline everything goes to missTo. Accepted item (4) covers fee-on-transfer, rebasing and tokens that block 0xdEaD, not a blocklisted builder or funder. Impact falls on the party whose own address is blocked, so this is reported as low. A pull-based claim (record owed amounts, let the recipient withdraw) would remove the lock, but that is an ABI change the owner has ruled out; at minimum the web app should warn that blocklist tokens are unsupported.","line":237,"path":"src/ShipOrBurn.sol","reproduction":"Refund vault (refundMisses = true) in a blocklist ERC-20, 3 tranches of 1e18, baseline settled with count 7. The token issuer then blocklists the funder. settle with a signed flat attestation (toBlock >= last+6000, count 7): safeTransfer(missTo = funder) reverts -> the whole call reverts, the verdict cannot be recorded. After the deadline, expire(id): safeTransfer(funder, 3e18) reverts -> VaultClosed is never set and the 3e18 stays in the contract with no function that can move it. Reproduced in test/scratch/BlockedRecipient.t.sol (passes on current code, documenting the lock; it is not a proof file).","severity":"low","snippet":"        if (rest > 0) v.token.safeTransfer(v.missTo, rest);","title":"A token blocklist on missTo (refund vault) or on the builder leaves settle and expire reverting, with no other way out of the vault"},{"citation":"resolved","description":"The protocol's reference consumer rejects an attestation whose issuedAt is more than 5 minutes ahead of block.timestamp (AttestationNotYetValid). settle only checks expiresAt. No exploit follows while the IMD signer is honest, since it stamps issuedAt itself; this is a deviation from the canonical verification the oracle-consumer reference asks consumers to copy, noted so the adapter and the final panel know it is deliberate. Adding the check would not change the ABI (a new error would be new ABI, so a revert with the existing AttestationExpired is the ABI-preserving form).","line":194,"path":"src/ShipOrBurn.sol","reproduction":"Attestation signed by the attester with issuedAt = type(uint64).max and expiresAt = block.timestamp + 1 day, otherwise valid for the vault: settle accepts it. Expected under the canonical consumer: revert AttestationNotYetValid. Actual: counted as a verdict.","severity":"info","snippet":"        if (block.timestamp > a.expiresAt) revert AttestationExpired();","title":"issuedAt is never checked, unlike the canonical OracleAttestationConsumer"},{"citation":"resolved","description":"Read in full: src/ShipOrBurn.sol (every external and public function: createVault, linkSchedule, settle, expire, questionHash, attestationDigest, getVault, vaultCount, nextVerdictBlock, and _checkPrefix), src/ShipOrBurnIMD.sol, test/ShipOrBurn.t.sol, test/ShipOrBurnIMD.t.sol, test/GoldenVectors.t.sol, script/Deploy.s.sol, and the vendored OpenZeppelin v5.4.0 files the contract imports (ECDSA, EIP712, SafeERC20, ReentrancyGuard, Strings). No contract was out of reach. Checked and found clean: no DELEGATECALL, CALLCODE, SELFDESTRUCT, proxy, initializer, owner, pause, mint or post-deployment configuration; constructor makes no external calls; ECDSA.recover rejects high-s and zero recoveries; EIP-712 domain, typehash and struct match the protocol reference and the real-signature golden vectors; questionHash matches a real IMD request; replay within a vault is blocked by the spacing rule, across contracts by verifyingContract, across chains by the domain and a.chainId; createVault is balance-checked and nonReentrant; tranche*tranches cannot overflow; settled never exceeds tranches so expire's subtraction cannot underflow; conservation (builder + missTo + contract balance = tranche*tranches) holds in the existing fuzz test. Checks that could not be performed offline: whether IMD accepts a block window whose toBlock is beyond the chain head (it would remove the lag precondition of the high finding), and how IMD's panel resolves 'agreed' for large panelSize with a small quorum. forge test: 25 passed, 0 failed, unchanged.","line":8,"path":"src/ShipOrBurnIMD.sol","reproduction":"Not a defect. forge test passes 25/25 on the unmodified tree; the two scratch tests under test/scratch/ are the only additions and nothing in test/scratch/ is kept.","severity":"info","snippet":"contract ShipOrBurnIMD is ShipOrBurn {","title":"Review coverage: both contracts and all vendored dependencies were read; nothing was unreachable"}],"hash":"0132e8e02ce223a7558f8927ba075b309ffa7cdb74bb68a3553bbe0bf08c6127","nodeId":"a658723d-2e09-4d9d-b2cb-bc3d56d520ef","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with four entries, the source tree is unchanged (git diff is empty), and the existing suite still passes 25 of 25.\n\n**What I found**\n\n- **High, `src/ShipOrBurn.sol:209`.** The spacing rule is on `toBlock`, but the count IMD signs is read at request time, and nothing ties `toBlock` to the current block. Two attestations requested minutes apart with toBlocks 6,000 blocks apart are both valid, and the second is always a flat window. A builder who merged a PR in each window still loses a tranche to missTo. Anyone who pays for two oracle requests can do this once settlement lags one window behind, and an honest keeper catching up after an outage does it to its own builder. In refund vaults the funder profits directly. Proof at `test/scratch/StaleVerdictProof.t.sol` fails on the current code and passes with a freshness bound on `toBlock` versus `block.number`, which I confirmed with a temporary edit that I reverted.\n- **Low, `src/ShipOrBurn.sol:237`.** Push transfers to a fixed recipient mean a blocklisted funder in a refund vault makes flat verdicts and `expire` revert forever, locking the remainder. Impact lands on the blocked party, so low. Reproduced in `test/scratch/BlockedRecipient.t.sol`.\n- **Info, `src/ShipOrBurn.sol:194`.** `issuedAt` is never checked, a deviation from the canonical consumer. No exploit while the signer is honest.\n- **Info, coverage.** Both contracts, all tests, the deploy script and the vendored OpenZeppelin files were read in full. Nothing was unreachable. No delegatecall, selfdestruct, proxy, owner, pause or mint; EIP-712 encoding and the question hash match real IMD vectors; replay, reentrancy, overflow and conservation all check out.\n\n**What I could not verify offline:** whether IMD will sign a block window whose `toBlock` is beyond the chain head. If it does, the high finding needs no lag at all.\n\n**Next for the adapter:** decide between the contract-side fix (reject stale verdicts at settlement) and a question change asking for the count as of the window end. Only the former is enforceable on chain, and it keeps the ABI intact.","treeHash":null,"usage":{"cachedInputTokens":1230597,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":40727,"runtime":"claude","turns":27,"wallClockMs":684244}},{"artifacts":[],"attempt":1,"bundleHash":"52bfe71625d1cba65ec9ed56220160f5154871a739bdb1dfcbf3feb30bc87d3d","device":"0db9dd6ea32a69f2","findings":[],"hash":"1a6394b385f87a5a8f89bafd0250a9cfa071a2008d886da8a5067ce444e27a4b","nodeId":"14cb4422-366f-499b-9b6f-37f0f3cbc1dd","outcome":"completed","summary":"Added failure-path, signature, reentrancy, deployment, and multi-vault custody tests.\n\n`forge build` and `forge test` pass: 58 tests, including 256 invariant sequences totaling 16,384 calls.\n\nAll changes are under `test/`. Production contracts and configuration remain unchanged.","treeHash":"73ce4a1a21a8622b7acc96469dbe408552df86fd","usage":{"cachedInputTokens":1250688,"inputTokens":97216,"model":"gpt-6-astra","outputTokens":23767,"runtime":"codex","turns":6,"wallClockMs":811744}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6b37e4ab65246705","findings":[{"citation":"resolved","description":"Trust-gap (access x asymmetry). settle() is permissionless and the settler picks the attestation (accepted rule 1). The rule that makes this safe is 'a builder must ship within every spacing window', which holds only if the count IMD signs was read at or after the window's toBlock. The contract proves nothing about that: it bounds toBlock against block.number at settlement, ignores a.issuedAt (the time the panel actually read GitHub) and ignores a.blockHash (the sealed hash of toBlock). The canonical OracleAttestationConsumer at least rejects issuedAt more than 5 minutes in the future; this contract accepts any issuedAt, including one hours before toBlock was mined. Consequence if IMD will sign an explicit {fromBlock,toBlock} window whose toBlock is still in the future: a refund-vault funder requests such a window while the count is flat, holds the signature (validForSeconds up to 30 days), lets the builder merge inside the window, and at toBlock submits the pre-signed flat attestation. The tranche is refunded to the funder although the builder shipped within the window. Evidence gathered: both real IMD vectors carry the true mainnet hash of their toBlock (block 26122901 hash 0x22cd78...3059 verified against a public RPC; block 26086885 likewise), and the v2 vector was issued ~70 hours after its toBlock, so IMD resolves a sealed block when it signs and this attack is most likely not reachable against today's service. It is therefore reported as a trust assumption with a cheap on-chain defense, not as a reproduced loss. Defense that keeps the ABI, constants and typehash: require a.issuedAt <= block.timestamp + 5 minutes and a.issuedAt >= block.timestamp - 2 hours (so the read is as fresh as the window), and/or require blockhash(a.toBlock) == a.blockHash when a.toBlock is within 256 blocks, which proves the signer observed the closed window. The second would tighten keeper timing from 600 to 256 blocks, so it is a design decision for the owner.","line":202,"path":"src/ShipOrBurn.sol","reproduction":"Refund vault (refundMisses=true), tranche 1e18, 3 tranches. Baseline at block B0 with count 7. At block B0+4000 obtain a signature from the attester over an attestation with fromBlock=B0+5700, toBlock=B0+6000, answer=abi.encode(7), issuedAt=timestamp(B0+4000), expiresAt=issuedAt+30 days, panelSize 5, quorum 4, agreed 5. The builder merges a PR at B0+4100 (true count 8). At block B0+6000 call settle(id, thatAttestation, sig, prefix). Expected under rule (1): the window [B0, B0+6000] contained a ship, so either revert or pay the builder. Actual: all checks pass (toBlock == block.number, lag 0, toBlock >= lastToBlock+6000, count 7 == lastCount), Missed is emitted and 1e18 is transferred to the funder; issuedAt is 6.7 hours older than block.timestamp and is never read. Scratch test test_preSignedFlatVerdictIsAccepted passes on the current code, demonstrating the contract cannot tell this attestation from a legitimate one.","severity":"low","snippet":"        if (a.toBlock > block.number) revert TooEarly();\n        if (block.number - a.toBlock > 600) revert AttestationExpired();","title":"Verdict freshness is anchored on toBlock only; issuedAt and blockHash are never checked, so a window pre-signed before it closed settles as a flat verdict"},{"citation":"resolved","description":"Trust-gap. The question hash covers answerType, chainId, definitions, evidence, question, v and window, but not panelSize or quorum, so whoever pays for the request (the funder, the builder or a third party, all of whom may settle) chooses them. The check accepts quorum as low as 4 with any panelSize up to 300, i.e. an answer that only 4 seats of 300 agreed on. With panel evidence IMD signs the answer that first reaches quorum; the GitHub search total_count is eventually consistent and panelists may split between adjacent values, so a party wanting a particular verdict can re-request with a tiny quorum and a large panel until the minority value is signed. Whether IMD's panel actually settles that way could not be tested here, so no loss is claimed. A requester-independent floor such as requiring a.quorum * 2 > a.panelSize (no constant or ABI change) would close the gap if the owner wants it.","line":192,"path":"src/ShipOrBurn.sol","reproduction":"Any vault. Attestation with panelSize=300, quorum=4, agreed=4, otherwise valid and signed by the attester. settle() accepts it (scratch test test_fourOfThreeHundredPanelIsAccepted passes and the vault becomes baselined). Expected by the README's 'a real panel agreed': a majority-of-panel requirement; actual: 4 of 300 suffices.","severity":"info","snippet":"        if (a.panelSize < MIN_PANEL || a.agreed < MIN_AGREED || a.agreed < a.quorum) revert WeakPanel();","title":"panelSize and quorum are the requester's choice, outside the question hash; a 4-of-300 panel passes the WeakPanel check"}],"hash":"62711f2d8b9b33c9e95e46fc006d20f56d4f6be9716d2b37abeb0357fb9bfaad","nodeId":"21d32db0-4758-4c32-912a-487f8b889b12","outcome":"completed","summary":"The findings file is in place and intentionally excluded from git by the harness. The scratch tests are removed, so the working tree holds no source changes.\n\n**Outcome: no critical or high defect in the Access Control, Trust Gap or Asymmetry areas. The code should stay as it is.** Two low-value observations are recorded in `.imd-findings.json`, with all eight entry points covered.\n\n**What I verified in the assigned area**\n\n- **Permission model.** There is no role after deployment. The attester is immutable, the factory receives nothing, and the only per-vault power is the funder's event-only `linkSchedule`. Every state-changing entry point is permissionless by the accepted design, and `nonReentrant` covers all three of them, so the aderyn reentrancy lead is guarded.\n- **Asymmetry.** The settle and expire deadline comparisons are complementary. The baseline and verdict branches share the same freshness checks. The closed flag and `settled <= tranches` keep expire from double-paying. The existing conservation fuzz passes.\n- **Replay and binding.** The question hash is pinned per vault, the domain binds chain and contract, and the toBlock ordering rejects replays and older windows. The aderyn `abi.encodePacked` lead is not a collision because the two decimal numbers sit between fixed delimiters, which I confirmed with a test.\n\n**Findings written**\n\n1. **Low.** Freshness is anchored on `toBlock` only. The contract never reads `issuedAt` or `blockHash`, so an attestation signed before its own window closed would settle as a flat verdict and refund a tranche the builder earned. I checked the real golden vectors against a public mainnet RPC. Both carry the true hash of their closing block, and the v2 vector was signed about 70 hours after that block, so IMD resolves a sealed block when it signs and the attack most likely cannot be mounted against today's service. It is reported as a trust assumption with an ABI-preserving defense, not as a loss.\n2. **Info.** Panel size and quorum are the requester's choice and sit outside the question hash. A 4-of-300 panel passes the check. Whether IMD's panel can be steered that way could not be tested here.\n\n**Not reached.** Nothing in the assigned area was left unexamined. Live IMD behaviour for future windows and low-quorum panels could not be exercised without paying for oracle requests, which is why both items stay below medium.","treeHash":null,"usage":{"cachedInputTokens":1116830,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":36011,"runtime":"claude","turns":28,"wallClockMs":492429}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b4f6137e7c93a2b3","findings":[{"citation":"resolved","description":"Economic Security / Invariant. Verdict k cannot be counted before toBlock >= startBlock + k*6000 (baseline >= startBlock, then MIN_SPACING per verdict, lines 207/214), so the last verdict is settleable no earlier than creation + tranches*6000 blocks. The guard assumes 6000 blocks == 20h, i.e. exactly 12.0 s/block. Mainnet's average is slightly above 12 s (missed slots), so the real time for K verdicts is K*6000*bt and the single spare 20h in the guard is exhausted once K*6000*(bt-12) > 72000 s, i.e. K > 12/(bt-12): at 12.1 s/block K >= 121, at 12.05 s K >= 241. Beyond that, the final verdict(s) revert PastDeadline (line 182) no matter how early every verdict is taken, and expire() (lines 241-242) sends those tranches to missTo. In a refund vault the funder, who chose both tranches and the minimum deadline, is the beneficiary; the builder loses tranches without a single flat window. The contract comment on line 135 ('the baseline plus one verdict per tranche, each at least MIN_SPACING blocks (20h+) apart') claims the guard covers this. Related to accepted item (5) (12-second blocks); reported because the accepted assumption is a lower bound on mainnet, not the average, and the loss lands on the builder. Minimal fix without changing the ABI: reserve slack proportional to tranches (e.g. 21 hours per verdict, or require deadline >= now + (tranches+1)*MIN_SPACING*12.5 s), or document that funders must add margin.","line":136,"path":"src/ShipOrBurn.sol","reproduction":"ShipOrBurn(signer) on chainId 1. funder: createVault(token, builder, refundMisses=true, tranche=1e18, tranches=300, deadline = block.timestamp + 301*20 hours (exactly the minimum the guard accepts), prefix). Baseline settled in the creation block (count 0). Then for k = 1..300: roll to startBlock + k*6000, warp to t0 + (k*6000)*12.1 s (12.1 s/block), settle with count k (every window shipped). Expected: 300 Shipped verdicts, builder receives 300e18. Actual: verdicts 1..298 succeed; at k = 299 block.timestamp = t0 + 21,706,200 > deadline = t0 + 21,672,000, settle reverts PastDeadline; expire(id) after the deadline sends 2e18 to the funder. Verified with a Foundry test (test passed: 'verdicts settled before deadline: 298', 'tranches refunded to funder despite shipping: 2e18').","severity":"low","snippet":"        if (deadline < block.timestamp + (uint256(tranches) + 1) * 20 hours) revert DeadlineTooSoon();","title":"DeadlineTooSoon reserves exactly 20h per verdict; at real mainnet block times (>12.0 s avg) a many-tranche vault at the minimum deadline cannot finish, and the unreachable tranches go to missTo even f"},{"citation":"resolved","description":"Economic Security / Flow Gap (execution x periphery x first principles). The production question answers 'the count at the moment you retrieve it' (window is context only), so the number in an attestation is read when IMD processes the request, which is any time between block toBlock (its blockHash must exist) and the settle block, i.e. anywhere in [toBlock, toBlock+600]. Spacing (line 214) is enforced on toBlock only. Verdict k can therefore be requested and settled at toBlock_k+600 (count read ~600 blocks after toBlock_k) while verdict k+1 is requested and settled at toBlock_k+6000 exactly (count read at that block). The two reads are 5,400 blocks apart although both attestations are 6,000 blocks apart on-chain. The stated rule (accepted item 1) is that a builder must ship once per MIN_SPACING window of 6,000 blocks; the enforceable window is 5,400 blocks. A refund-vault funder who settles verdict k late and verdict k+1 at the first eligible block captures one tranche from a builder merging on a steady 18h<cadence<=20h. Cost to the funder: two oracle requests (1 IMD) for 1 tranche; repeatable every other verdict against a builder who keeps a 20h cadence. Mitigation that keeps the ABI: measure spacing from the block the previous verdict was settled in (store block.number instead of a.toBlock for spacing), or state the 18h effective window in the builder-facing docs.","line":203,"path":"src/ShipOrBurn.sol","reproduction":"Vault: refundMisses=true, tranche 1e18, tranches 3, deadline now+10 days. Baseline at toBlock=start (count 7). Builder merges PR #8 at start+6100 and plans PR #9 for start+12100 (one per 20h). Funder requests window toBlock=start+6000 at block start+6600 (panel reads live count 8) and settles at start+6600: accepted (lag 600), Shipped, lastToBlock=start+6000. Funder requests window toBlock=start+12000 at block start+12000 (panel reads 8, PR #9 not merged yet) and settles immediately: toBlock >= lastToBlock+6000 passes, count == lastCount -> Missed, 1e18 to the funder. Expected under the stated rule: the builder shipped in every 6,000-block window and should not be charged. Verified with a Foundry test (both settles accepted, funder balance +1e18, builder +1e18).","severity":"low","snippet":"        if (block.number - a.toBlock > 600) revert AttestationExpired();","title":"The 600-block settlement grace lets two counted verdicts be measured only 5,400 blocks (~18h) apart, so a builder shipping every 20h can be charged a flat verdict"},{"citation":"resolved","description":"Invariant (view/write divergence). The view documents itself as 'The earliest toBlock the next verdict can use' but ignores the staleness bound settle() enforces at line 203 (block.number - toBlock <= 600) and line 202 (toBlock <= block.number). After any gap longer than 6,600 blocks since the last counted toBlock, the returned value is a window no attestation can be settled with. A keeper or the web app that requests IMD for that window spends the 0.5 IMD oracle fee and, if this happens near the deadline, loses the tranche to expire(). The correct value is max(lastToBlock + MIN_SPACING, block.number - 600) capped at block.number (or revert/return 0 when no window is currently usable). Same divergence for the un-baselined branch: startBlock is unusable once block.number > startBlock + 600.","line":301,"path":"src/ShipOrBurn.sol","reproduction":"Vault created at block S, baseline settled with toBlock=S. Roll to S+7000. nextVerdictBlock(id) returns S+6000. Request and obtain a valid IMD attestation for window toBlock=S+6000 (count 8 > 7) and call settle(id, a, sig, prefix) at block S+7000. Expected per the view: accepted. Actual: reverts AttestationExpired (lag 1000 > 600). Verified with a Foundry test.","severity":"low","snippet":"        return v.baselined ? v.lastToBlock + MIN_SPACING : v.startBlock;","title":"nextVerdictBlock() returns a toBlock that settle() rejects once it is more than 600 blocks old; a keeper following it after an outage pays for an unsettleable attestation"},{"citation":"resolved","description":"Economic Security ('break dependencies'). The contract trusts the oracle answer as a monotone counter, but the production question is answered from GitHub's search API total_count, which can over-count transiently (index lag, incomplete_results) even with 4-of-5 panel agreement when all agents query the same endpoint. There is no way to recover from one high answer: every honest attestation below lastCount reverts, so no verdict can be counted at all until the real count reaches lastCount, and the attestation that first reaches it reads count == lastCount and is charged as a miss even though the builder merged (lastCount - realCount) pull requests to get there. If the live count never catches up before the deadline the whole remainder expires to missTo. This is a dependency-failure mode, not a bug in the signature or ordering logic; it is reported so the author can decide whether a lower count should be treated as 'flat' (not revert) or whether vault docs should state that one bad panel answer can forfeit the rest of the vault.","line":215,"path":"src/ShipOrBurn.sol","reproduction":"Refund vault, tranche 1e18, tranches 4. Baseline count 7 at toBlock S. Verdict 1 at S+6000: attestation count 10 (real count 8) -> Shipped, lastCount=10. Builder merges one PR (real 9). Verdict 2 at S+12000 with the honest attestation count 9 -> reverts CountWentDown; nothing can be settled. Builder merges another (real 10). Verdict at S+18000 with count 10 -> count == lastCount -> Missed, 1e18 refunded to the funder although the builder merged two PRs since the last counted verdict. Verified with a Foundry test.","severity":"low","snippet":"        if (count < v.lastCount) revert CountWentDown();","title":"A single over-reported count is sticky: later true counts revert CountWentDown and the builder's real merges are absorbed as a flat verdict until the live count passes the glitch"}],"hash":"8e7d0254f27e20880b704834fa98989af3af7f8305b97a47a137696d635b63a7","nodeId":"fb7a8961-a7ab-44af-9a9f-ff601f1f8ba0","outcome":"completed","summary":"Review complete. `.imd-findings.json` is written (valid JSON, 4 findings, 12 coverage rows, all snippets verified against the tree); the scratch test directory was removed so no repository file is changed.\n\n## Summary\n\n**Scope covered:** Senior Auditor's Mindset, then the Economic Security, Invariant and Flow Gap guides against `src/ShipOrBurn.sol` / `src/ShipOrBurnIMD.sol`. Existing suite passes (35 tests). Each reproduction below was confirmed with a Foundry test before being recorded.\n\n**No critical or high defects.** The core guarantees hold: question binding via `prefixHash` + reconstructed `questionHash`, EIP-712 domain/signer pinning (golden vectors), replay prevention, CEI ordering, `closed` coupling, and per-vault token conservation. The static-analysis leads (encodePacked collision, reentrancy after external call, timestamp use) were traced and are not defects.\n\n**Four low findings, all with concrete reproductions:**\n\n1. `ShipOrBurn.sol:136` — `DeadlineTooSoon` reserves exactly 20h per verdict (12.0 s/block). At a realistic 12.1 s/block, a 300-tranche refund vault at the minimum deadline settles only 298 verdicts even with every window shipped; 2 tranches go back to the funder who chose those parameters. Touches accepted item (5), so reported low for the judge to weigh.\n2. `ShipOrBurn.sol:203` — spacing is enforced on `toBlock`, but the count is read anywhere in `[toBlock, toBlock+600]`. Two counted verdicts can be measured 5,400 blocks apart, so a builder on a steady 20h cadence can be charged one flat verdict by a refund-vault funder.\n3. `ShipOrBurn.sol:301` — `nextVerdictBlock()` ignores the 600-block staleness bound; after an outage it names a window `settle()` rejects, wasting the oracle fee.\n4. `ShipOrBurn.sol:215` — one over-reported count is sticky: honest lower counts revert, and the builder's real merges are absorbed as a \"flat\" verdict when the live count catches up. A dependency-failure mode, reported for the author's decision.\n\n**Noted as trust assumptions, not findings:** in burn vaults nobody is paid to settle flat verdicts (accepted item 1); blocked-recipient lock-ups (accepted item 4, already documented and tested).","treeHash":null,"usage":{"cachedInputTokens":503504,"inputTokens":198,"model":"claude-fable-5-1","outputTokens":31667,"runtime":"claude","turns":21,"wallClockMs":413885}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"82692ab5f7f7e54e","findings":[{"citation":"resolved","description":"MIN_SPACING is enforced on the attestations' toBlock values (line 214: `if (a.toBlock < v.lastToBlock + MIN_SPACING) revert TooSoon();`), but the question is answered with the counter's value at retrieval, not at toBlock, and the freshness rule at line 203 lets a window be settled up to 600 blocks after it ends. A requester who pins an explicit window whose toBlock is 599 blocks in the past has IMD read the count ~600 blocks after toBlock; the next verdict can then name toBlock = previous toBlock + 6,000 and be read at that block. The two counts are therefore read 5,400 blocks (~18 h) apart while the contract treats them as 6,000 blocks apart. A builder who relies on the stated ~20 h per window and merges 18-20 h after the last count can be charged a flat tranche for that window (the merge is counted next window, so one tranche moves from builder to missTo). The compression is at most 600 blocks and is not cumulative (alternating backdated and fresh windows averages 6,000), so impact is bounded to one tranche per such alignment. Fix without ABI change: when a vault has a baseline, also require the settlement block to be at least MIN_SPACING after the block at which the previous attestation was settled (store it in the unused slack of the Vault struct), or document the 18 h effective minimum for builders.","line":203,"path":"src/ShipOrBurn.sol","reproduction":"Vault created at block S with tranche 1e18, tranches 3. (1) At block S+600 settle baseline attestation {fromBlock:S,toBlock:S,count:7} (block.number - toBlock = 600, accepted). The count was read ~S+600. (2) At block S+6000 settle {toBlock:S+6000,count:7}: toBlock >= lastToBlock+6000 passes; block.number - toBlock = 0 passes; count == lastCount so Missed: 1e18 goes to 0xdEaD. Only 5,400 blocks separate the two reads. Expected (per README/brief: one verdict per ~20 h, i.e. 6,000 blocks between counts): the second verdict should not be countable before S+6600. Actual: counted at S+6000. Run test/scratch/RetrievalGap.t.sol (test_retrievalGapIs5400Blocks passes on the current code, demonstrating the gap).","severity":"low","snippet":"        if (block.number - a.toBlock > 600) revert AttestationExpired();","title":"Backdated toBlock lets two counted attestations be retrieved only 5,400 blocks (18 h) apart, not the 6,000 blocks (20 h) MIN_SPACING suggests"},{"citation":"resolved","description":"The signed issuedAt field is not compared with anything (the canonical OracleAttestationConsumer rejects issuedAt more than 5 minutes ahead of block.timestamp). The 600-block freshness check only bounds how long after toBlock a window is settled; nothing bounds how long before toBlock the count was read. If IMD ever signed an explicit window whose toBlock is ahead of the chain head at request time, a requester could pre-buy a series of attestations (toBlock = T, T+6000, T+12000..., all carrying today's count, expiresAt up to 30 days) and settle each in the first block its toBlock becomes current, producing a flat verdict for every window even though the builder merged in each, and winning every race against a builder whose fresh attestation needs panel time. The real version-2 vector (test/GoldenVectors.t.sol) carries a non-zero blockHash of toBlock, which indicates IMD resolves the window against existing blocks and so does not sign a future toBlock; I could not verify the live intake's validation offline, so this is recorded as an unverified assumption rather than a defect. If the author wants a contract-side bound that keeps the ABI, requiring issuedAt to be no earlier than block.timestamp - (block.number - toBlock) * 12 - tolerance would reject counts read before the window ended.","line":194,"path":"src/ShipOrBurn.sol","reproduction":"Contract-side only: vault created at block S; sign (test key) attestation {toBlock:S+6000,count:7,issuedAt:creation timestamp,expiresAt:+30 days} at creation time; roll to S+6000 and settle the baseline then this one: it is accepted with Missed. Whether IMD would sign such an attestation is not reproducible here, so no harm is demonstrated; existing test test_futureIssuedAtIsAcceptedIfSignerApproves already shows issuedAt is ignored.","severity":"info","snippet":"        if (block.timestamp > a.expiresAt) revert AttestationExpired();","title":"issuedAt is never checked, so the contract cannot tell when a count was read relative to the window it names; safety rests entirely on IMD not signing windows that end after the read"}],"hash":"9d871fb845943a3d9d9de9e30c56c7b82e8653ce89f679f78048a77b251ad3f0","nodeId":"7a0f49e9-caa7-40d0-9cd7-d2eaca4039c8","outcome":"completed","summary":"The review is complete. The findings file is valid JSON, both quoted snippets match the tree at the cited lines, and only `.imd-findings.json` and the scratch test exist outside the committed tree (both are expected outputs).\n\n**Result: no critical, high or medium defect in the control flow.** The Execution Trace, Periphery and First Principles passes were applied to `createVault`, `settle`, `expire` and `linkSchedule` on both contracts. All 35 existing tests pass, including the four golden vectors that pin the signer and the version-2 domain.\n\nFindings written to `.imd-findings.json`:\n\n- **Low. Backdated toBlock shortens the effective spacing between counts.** The 6,000-block spacing is enforced on toBlock values, but the count is read at retrieval and a window may be settled up to 600 blocks after it ends. A baseline with toBlock 599 blocks in the past, followed by a verdict at exactly toBlock + 6,000, puts the two reads 5,400 blocks (about 18 hours) apart. A builder who merges 18 to 20 hours after the previous read can lose one tranche. The compression is bounded and not cumulative. The scratch test `test/scratch/RetrievalGap.t.sol` demonstrates it against the current code.\n- **Info. issuedAt is never checked.** Freshness only bounds settlement after toBlock, not reading before it. Pre-signed future windows would be exploitable, but the real version-2 vector carries a genuine blockHash for toBlock, which indicates IMD does not sign a future window. This is recorded as an unverified assumption, not a defect.\n\nLeads that did not reproduce and were left out as findings: the aderyn encodePacked collision (prefix is pinned by hash before use, variable parts are decimals between fixed delimiters), the aderyn reentrancy at vault creation (nonReentrant, id read after the transfer), and the slither timestamp lines (deadline and expiry windows are days wide). Each has a coverage row explaining why.\n\nCoverage: all eight listed entry points have a row, plus five rows for the invariants and periphery I checked. Nothing in the assigned area was left unreached.","treeHash":null,"usage":{"cachedInputTokens":1018383,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":31715,"runtime":"claude","turns":26,"wallClockMs":480135}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"52c98c0dc01791cd","findings":[{"citation":"resolved","description":"Seam: boundary (600-block freshness allowance) x invariant (windows do not overlap). The spacing rule at line 214 (`a.toBlock < v.lastToBlock + MIN_SPACING`) is measured on the attestation's toBlock, but the count IMD signs is the live count at retrieval, which the vault's question defines as 'the count at the moment you retrieve it' with the window as context only. Line 203 accepts any attestation whose toBlock is up to 600 blocks in the past. A settler can therefore request a window ending at block T while the panel reads the counter at T+590, and the contract stores lastToBlock = T. The next verdict may use toBlock T+6000, read at T+6000. Two consecutive counted reads are then 5,410 blocks apart (about 18 hours), not the 6,000 blocks (20 hours) the rule states, and a merge that happened at T+300, inside the next window [T, T+6000), is absorbed into verdict k instead of counting for verdict k+1. The slack is symmetric: a settler who favours the builder can name toBlock T for a read at T+590 that includes a merge at T+500, which was outside window [T-6000, T), and get it paid. In a refund vault the funder is paid for the manufactured flat verdict, so this is an economic incentive, not just a griefing path. Both parties can close the race by settling promptly at each window end, which is why this is rated low and not medium: the prior accepted fix (the 600-block bound) shrank an unbounded version of this problem to a 600-block one but did not remove it. A minimal ABI-preserving mitigation is to measure spacing from the settlement block (store uint64(block.number) in lastToBlock, or require a.toBlock >= v.lastToBlock + MIN_SPACING + (settleBlock_k - toBlock_k)), or to shrink the lag to what delivery actually needs (IMD answers in minutes, about 10 to 30 blocks). The 'Accepted by the owner (1)' note says the rule is 'at least MIN_SPACING after the last counted one', which this interval violates by up to 10%.","line":203,"path":"src/ShipOrBurn.sol","reproduction":"Refund vault, tranche 1e18, 3 tranches. Block S: baseline attestation toBlock=S, count 7, settled. Builder merges PR #8 at S+100 and PR #9 at S+6300. Funder requests at block S+6590 an attestation whose window is [S+5999, S+6000]; IMD reads the live count 9 and signs. settle(id, a{toBlock: S+6000, answer: 9}) at block S+6590: line 203 passes (6590-6000 = 590 <= 600), line 214 passes (6000 >= 0+6000), shipped, lastToBlock = S+6000. At block S+12000 anyone settles an attestation toBlock=S+12000, count 9: count == lastCount, Missed, 1e18 to funder. Expected under the stated rule: the builder merged inside both windows [S, S+6000) and [S+6000, S+12000) and should have been paid twice. Actual: one tranche paid, one refunded to the funder. Mirror case (test_lagLetsBuilderCountLateMerge in test/scratch/Boundary.t.sol): nothing merged in [S, S+6000), #8 merged at S+6500, attestation toBlock S+6000 read at S+6590 with count 8 is settled and pays the builder for a window in which nothing shipped. Both are in test/scratch/Boundary.t.sol (tests test_lagLetsHostileSettlerManufactureFlatVerdict and test_lagLetsBuilderCountLateMerge), both pass on the current code, showing the states above are reachable.","severity":"low","snippet":"        if (block.number - a.toBlock > 600) revert AttestationExpired();","title":"600-block settlement lag lets the settler name a toBlock up to 600 blocks before the count was read, so the effective shipping interval is 5,400 to 6,600 blocks instead of MIN_SPACING"},{"citation":"resolved","description":"Seam: boundary (minimum deadline) x precision (block time). The floor converts MIN_SPACING = 6,000 blocks to 20 hours at exactly 12 s per block and allows one spare spacing. Verdict k needs toBlock >= startBlock + 6000k and must be settled at block.timestamp <= deadline, so all N verdicts fit only if 6000 * N * b <= (N+1) * 72000 s, i.e. b <= 12 * (N+1)/N seconds per block. Mainnet's post-merge average is about 12.05 to 12.1 s because of missed slots, so the floor passes vaults whose last tranche cannot be settled whenever N >= 12/(b-12): about 120 tranches at 12.1 s, 240 at 12.05 s. For N = 200 at b = 12.1 s the 200th verdict's toBlock is reached at 200 * 6000 * 12.1 = 14,520,000 s after creation, but the minimum deadline is 201 * 72000 = 14,472,000 s, so verdict 200 cannot be settled and expire() sends that tranche to missTo although the builder shipped every window. The funder chooses the deadline and the owner accepted that MIN_SPACING assumes 12-second blocks, so this is low: the defect is that the floor gives a false guarantee for large tranche counts (and for any longer chain-wide slowdown). Mitigation: size the floor with a margin per tranche (for example 6,000 blocks * 13 s, or (tranches + 1) * 20 hours + tranches * 1 hour), or document that funders must leave more than the floor for long vaults.","line":136,"path":"src/ShipOrBurn.sol","reproduction":"test/scratch/Boundary.t.sol::test_minDeadlineUnreachableAtRealisticBlockTime (passes on the current code and demonstrates the state): createVault(token, builder, false, 1e18, 200, block.timestamp + 201 * 20 hours, prefix) is accepted. Baseline at startBlock. Advance 6,000 blocks per verdict at 12.1 s per block and settle a rising count each time: 199 verdicts settle; at verdict 200 block.timestamp exceeds the deadline, settle reverts PastDeadline, expire() sends 1e18 to 0xdEaD. Expected: the floor is meant to guarantee 'the baseline plus one verdict per tranche' fit before the deadline (comment at line 135); actual: the last tranche is lost to the builder. With N = 3 the same floor tolerates up to 16 s per block (test_minDeadlineThresholdSmallN), so short vaults are not affected.","severity":"low","snippet":"        if (deadline < block.timestamp + (uint256(tranches) + 1) * 20 hours) revert DeadlineTooSoon();","title":"DeadlineTooSoon floor assumes exactly 12-second blocks; at the floor with many tranches the final verdict is unreachable at realistic mainnet block times and the last tranche goes to missTo although t"}],"hash":"9e90f603b13a225738f841488ed2c1ba12c14a006229266736bc06f8381ee9a0","nodeId":"dfed9fcd-023c-4425-b398-1e071300a4fa","outcome":"completed","summary":"The review is complete. The findings file is written and validated against the tree, and the full suite still passes.\n\n**Outcome.** No critical or high defect in the math, boundary or numerical-gap area. The contract has no division, rounding or scale conversion anywhere, so the Math Precision checklist reduces to overflow and cast questions, all of which hold. Two low findings are recorded in `.imd-findings.json`, each with a reachable state reproduced by a scratch test under `test/scratch/Boundary.t.sol`.\n\n- **Finding 1 (low), settle freshness lag.** The 600-block allowance at `src/ShipOrBurn.sol:203` lets a settler name a toBlock up to 600 blocks before the panel read the counter, while spacing is measured on toBlock. Two counted reads can be 5,410 blocks apart instead of 6,000, so a merge in the first 600 blocks of a window can be absorbed into the previous verdict and the next one comes out flat. In a refund vault the funder profits. The mirror lets a builder get paid for a merge 590 blocks after a window ended. Rated low because either party closes the race by settling promptly at each window end.\n- **Finding 2 (low), deadline floor.** The check at `src/ShipOrBurn.sol:136` converts 6,000 blocks to exactly 20 hours. At realistic mainnet block times of about 12.1 seconds, a vault of 200 tranches at the minimum deadline cannot settle its last verdict and that tranche goes to missTo although the builder shipped every window. Short vaults tolerate up to 16 seconds per block, so this only bites long vaults.\n\n**What held.** Counter fields cannot wrap, total deposit fits uint256 at maximum parameters, the pre-subtraction future check prevents underflow, exact boundaries behave as documented (toBlock equal to startBlock, spacing exactly 6,000, lag exactly 600, expiresAt equal to now, timestamp equal to deadline), conservation holds on every path, and SafeERC20 in the vendored OpenZeppelin rejects no-code tokens. The three static analysis leads (packed-hash collision, reentrancy after push, timestamp comparisons) were traced and rejected with reasons in the coverage record.\n\n**Coverage.** All eight listed entry points have rows, plus four invariant rows. Nothing in the assigned area was left unreached. Not covered: whether the live IMD service signs windows whose toBlock is still in the future, which would widen finding 1 and cannot be tested offline.","treeHash":null,"usage":{"cachedInputTokens":927748,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":32057,"runtime":"claude","turns":24,"wallClockMs":452947}},{"artifacts":[],"attempt":1,"bundleHash":"12a35fdbd90f65fa03c04869176beff14fbcf4445278e289f41521251a003437","device":"ea30e435c4dd5aff","findings":[],"hash":"b67c7c9164191d388b46b8d7a0909515aecf1a93a27acf17b47f6f358458ea95","nodeId":"cf4a11f9-bf44-4d6e-a235-689972befa6c","outcome":"completed","summary":"Implemented the reproduced high-severity fix: settlement rejects future windows and windows over 600 blocks old. ABI, signer, domain, and constructor remain unchanged.\n\n`forge build` and `forge test` pass: 35 retained tests, plus the supplied scratch proof. Factory deployment checks pass.\n\nAll changes and audit dispositions are documented in [ADAPTATION.md](/home/imd/.identitymd/work/d8208131-5e99-4362-9e61-fe6b6e22088e/cf4a11f9-bf44-4d6e-a235-689972befa6c/ADAPTATION.md). Protected files were untouched. Prepared for launch-service handoff; no deployment was broadcast.","treeHash":"81822260bb418dcf6adcfaa91cf3c0f7747e0d2e","usage":{"cachedInputTokens":818944,"inputTokens":93607,"model":"gpt-6-astra","outputTokens":11819,"runtime":"codex","turns":6,"wallClockMs":439307}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa99051b60a858d6","findings":[{"citation":"resolved","description":"Merged from audit_flow, audit_economics and audit_math (same root cause). MIN_SPACING is enforced at line 214 on a.toBlock, but the production question answers the live counter at retrieval, and line 203 lets a window be settled up to 600 blocks after it ends. Verdict k can therefore name toBlock T while IMD reads the counter at ~T+600, and verdict k+1 can name toBlock T+6000 and be read at T+6000. The contract treats the two reads as 6,000 blocks apart when they were 5,400 apart. A builder keeping the stated 20 h cadence can be charged one flat tranche (refund vault: paid to the funder, who controls when to settle). The mirror case lets a settler count a merge made up to 600 blocks after toBlock for the earlier window. The compression is bounded to 600 blocks and is not cumulative, hence low. The contract cannot observe the read time, so this is a documented gap between the stated rule (accepted item 1: at least MIN_SPACING between counted verdicts) and what the on-chain check enforces. ABI-preserving options: measure spacing from the settlement block (store uint64(block.number) in lastToBlock), require a.toBlock >= v.lastToBlock + MIN_SPACING + (settleBlock_k - toBlock_k), shrink the 600-block grace to what delivery needs, or state an 18 h effective window in builder-facing docs.","line":203,"path":"src/ShipOrBurn.sol","reproduction":"Refund vault (refundMisses=true, tranche 1e18, tranches 3) created at block S. Roll to S+600 and settle baseline {toBlock:S, count:7}: accepted (lag 600 <= 600); the count was read ~S+600. Roll to S+6000 and settle {toBlock:S+6000, count:7}: toBlock >= lastToBlock+6000 passes, lag 0 passes, count == lastCount -> Missed, 1e18 to the funder. Expected under the stated rule: the second verdict is countable only once 6,000 blocks separate the two reads (S+6600). Actual: counted at S+6000 (5,400 blocks after the first read). Reproduced with test/scratch/Review.t.sol::test_A_readsCanBe5400BlocksApart (both settles accepted, funder balance +1e18).","severity":"low","snippet":"        if (block.number - a.toBlock > 600) revert AttestationExpired();","title":"Spacing is measured on toBlock while the count is read up to 600 blocks later, so two counted reads can be 5,400 blocks (~18 h) apart instead of MIN_SPACING"},{"citation":"resolved","description":"Merged from audit_economics and audit_math (same root cause). Verdict k cannot be counted before toBlock >= startBlock + k*6000 (lines 207 and 214), so all N verdicts need N*6000 blocks, and the floor reserves (N+1)*20 h, i.e. one spare 20 h window at exactly 12 s/block. Mainnet averages slightly above 12 s (missed slots), so the spare window is exhausted once N*6000*(bt-12) > 72,000 s: N >= 121 at 12.1 s, N >= 241 at 12.05 s. Beyond that settle reverts PastDeadline (line 182) for the final verdict(s) even if every verdict was taken at the first eligible block, and expire (lines 241-242) sends those tranches to missTo. The comment on line 135 claims the floor covers the baseline plus one verdict per tranche, which is false for large N. The funder picks tranches and deadline and the owner accepted the 12 s assumption, so low; the loss lands on the builder in a refund vault. Options that keep the ABI: reserve per-tranche margin (e.g. 21 hours per verdict, or 6,000 blocks * 13 s), or document that funders must leave margin above the floor for long vaults.","line":136,"path":"src/ShipOrBurn.sol","reproduction":"createVault(token, builder, false, 1e18, 200, block.timestamp + 201 * 20 hours, prefix) is accepted (exactly the floor). Baseline {toBlock:startBlock, count:0} in the creation block. For k = 1..200: roll to startBlock + k*6000, warp 12.1 s per block, settle {toBlock: startBlock + k*6000, count: k}. Expected: 200 Shipped verdicts. Actual: verdicts 1..199 settle; at k = 200 block.timestamp exceeds the deadline, settle reverts PastDeadline, and expire(id) sends 1e18 to 0xdEaD. Reproduced with test/scratch/Review.t.sol::test_C_minDeadlineUnreachableAt12_1s (log: last settled verdict 199; burn balance 1e18 after expire).","severity":"low","snippet":"        if (deadline < block.timestamp + (uint256(tranches) + 1) * 20 hours) revert DeadlineTooSoon();","title":"DeadlineTooSoon floor assumes exactly 12.0 s blocks; at realistic mainnet block times a many-tranche vault at the minimum deadline cannot settle its last verdicts and they go to missTo although the bu"},{"citation":"resolved","description":"From audit_economics. The view is documented as 'The earliest toBlock the next verdict can use' but ignores the freshness bound settle() enforces at lines 202-203 (toBlock <= block.number and block.number - toBlock <= 600). After any gap longer than 6,600 blocks since the last counted toBlock (or 600 blocks since creation for an un-baselined vault) the returned window is unsettleable. A keeper or the web app that requests IMD for that window spends the oracle fee for nothing, and near the deadline loses the tranche to expire(). Not an entry point and no funds move on-chain, so low. Fix without ABI change: return max(lastToBlock + MIN_SPACING, block.number - 600) capped at block.number (or 0 when no window is currently usable); same for the startBlock branch.","line":301,"path":"src/ShipOrBurn.sol","reproduction":"Vault created at S, baseline settled with toBlock = S. Roll to S+7000: nextVerdictBlock(id) returns S+6000. Sign a valid attestation {toBlock:S+6000, count:8} and call settle at S+7000. Expected per the view: accepted. Actual: reverts AttestationExpired (lag 1000 > 600). Un-baselined branch: vault created at S+7000, roll to S+7700, nextVerdictBlock returns S+7000, settle with toBlock S+7000 reverts AttestationExpired. Reproduced with test/scratch/Review.t.sol::test_D_nextVerdictBlockStale.","severity":"low","snippet":"        return v.baselined ? v.lastToBlock + MIN_SPACING : v.startBlock;","title":"nextVerdictBlock() returns a toBlock that settle() rejects once it is more than 600 blocks old"},{"citation":"resolved","description":"Merged from audit_flow and audit_permissions (same root cause). settle() bounds toBlock against block.number at settlement (lines 202-203) but reads neither a.issuedAt (the canonical OracleAttestationConsumer at least rejects issuedAt more than 5 minutes ahead of block.timestamp) nor a.blockHash. If IMD ever signed an explicit {fromBlock,toBlock} window whose toBlock was still in the future at request time, a refund-vault funder could pre-buy a flat attestation while the count is flat and submit it at toBlock after the builder merged, taking the tranche. Evidence against reachability today: the oracle-consumer reference says the questionHash covers 'the resolved block window', both real IMD vectors carry the true hash of their toBlock, and the v2 vector was issued ~70 h after its toBlock, so IMD resolves a sealed block when it signs. Recorded as a trust assumption, not a reproduced loss. ABI-preserving hardening if wanted: require a.issuedAt <= block.timestamp + 5 minutes and a.issuedAt >= (a lower bound tied to toBlock), and/or require blockhash(a.toBlock) == a.blockHash when toBlock is within 256 blocks (this would tighten the keeper window from 600 to 256 blocks, a design decision).","line":194,"path":"src/ShipOrBurn.sol","reproduction":"Contract-side only. Refund vault, tranche 1e18, 3 tranches, baseline {toBlock:S, count:7}. At block S+4000 sign (with the attester key) {fromBlock:S+5999, toBlock:S+6000, count:7, issuedAt:timestamp(S+4000), expiresAt:+30 days}. Roll to S+6000 and call settle with that signature. Expected if the read had to post-date the window: revert. Actual: all checks pass (toBlock == block.number, lag 0, spacing 6000, count == lastCount), Missed, 1e18 to the funder; issuedAt is 6.7 h older than block.timestamp and is never read. Reproduced with test/scratch/Review.t.sol::test_B_preSignedFutureWindowAccepted. Whether IMD would sign such a window is not reproducible offline.","severity":"info","snippet":"        if (block.timestamp > a.expiresAt) revert AttestationExpired();","title":"issuedAt and blockHash are never checked, so an attestation signed before its window closed settles as a verdict for that window; safety rests on IMD only signing resolved (existing) windows"},{"citation":"resolved","description":"From audit_economics. The monotone-counter rule is the accepted design (item 1), but it has no recovery from one high answer: once lastCount is above the real count, every honest attestation reverts and nothing can be counted until the live count reaches lastCount, and the attestation that first reaches it reads count == lastCount and is charged as a miss even though the builder merged (lastCount - realCount) pull requests. If the live count never catches up before the deadline the rest expires to missTo. This is an oracle-dependency failure mode (GitHub search total_count can transiently over-count with index lag even under 4-of-5 agreement), not a bug in the ordering or signature logic, and is recorded so the owner can decide whether a lower count should be treated as flat rather than reverting, or whether docs should state that one bad panel answer can forfeit the rest of the vault.","line":215,"path":"src/ShipOrBurn.sol","reproduction":"Refund vault, tranche 1e18, tranches 4. Baseline {toBlock:S, count:7}. Verdict 1 at S+6000 with count 10 (real 8): Shipped, lastCount = 10. Verdict 2 at S+12000 with the honest count 9: reverts CountWentDown. Verdict at S+18000 with count 10: count == lastCount -> Missed, 1e18 refunded to the funder although the builder merged two pull requests since the last counted verdict. Reproduced with test/scratch/Review.t.sol::test_E_overReportedCountIsSticky.","severity":"info","snippet":"        if (count < v.lastCount) revert CountWentDown();","title":"A single over-reported count is sticky: later honest counts revert CountWentDown and the builder's real merges are absorbed as a flat verdict"},{"citation":"resolved","description":"From audit_permissions. The question hash covers answerType, chainId, definitions, evidence, question, v and window but not panelSize or quorum, so whoever pays for the request chooses them. The check accepts quorum as low as 4 with any panelSize up to 300, i.e. an answer that only 4 of 300 seats agreed on. Whether IMD's panel signs the first value that reaches quorum when panelists split between adjacent counts could not be tested offline, so no loss is claimed; constants are fixed by the brief. A requester-independent floor such as a.quorum * 2 > a.panelSize (no constant or ABI change) would close the gap if the owner wants it.","line":192,"path":"src/ShipOrBurn.sol","reproduction":"Any vault. Attestation with panelSize = 300, quorum = 4, agreed = 4, otherwise valid and signed by the attester: settle() accepts it and the vault becomes baselined. Expected by the README's 'a real panel agreed': a majority of the panel; actual: 4 of 300 suffices. Reproduced with test/scratch/Review.t.sol::test_F_fourOfThreeHundred.","severity":"info","snippet":"        if (a.panelSize < MIN_PANEL || a.agreed < MIN_AGREED || a.agreed < a.quorum) revert WeakPanel();","title":"panelSize and quorum are the requester's choice and outside the question hash; a 4-of-300 panel passes WeakPanel"}],"hash":"bd147152c173d15ac59a8ccc060467c46d715d893ec6000ca952f3c8444c1d6c","nodeId":"21f46c52-3b4f-45b0-b12d-4ece0f6473e2","outcome":"completed","summary":"Review complete. `.imd-findings.json` is written (valid JSON, every snippet verified against the tree), the scratch tests are removed, and the working tree is otherwise untouched (`git status` clean apart from the findings file, which is untracked).\n\n## Summary\n\n**No critical or high issue reproduces.** The code stays as it is under the brief's rule. The custody logic, question/prefix binding, EIP-712 domain and signer (confirmed by the real IMD v2 golden vector), baseline/spacing/monotone ordering, effects-before-transfer, and the deadline/expire boundary all hold. The full suite passes (58 tests).\n\n**Specialist findings, after reproduction and merging** — ten reports collapsed into six, all low/info:\n\n| # | Sev | Finding | Merged from |\n|---|---|---|---|\n| 1 | low | Spacing is enforced on `toBlock` (line 214) but the count is read up to 600 blocks later (line 203), so two counted reads can be 5,400 blocks apart; one flat tranche can be charged against a 20 h-cadence builder | audit_flow, audit_economics, audit_math |\n| 2 | low | `DeadlineTooSoon` floor (line 136) assumes exactly 12.0 s blocks; at 12.1 s a 200-tranche vault at the floor cannot settle verdict 200 and it expires to `missTo` | audit_economics, audit_math |\n| 3 | low | `nextVerdictBlock()` (line 301) returns a window `settle()` rejects as `AttestationExpired` once it is >600 blocks old | audit_economics |\n| 4 | info | `issuedAt`/`blockHash` never checked; a pre-signed window settles — only reachable if IMD signs unresolved windows, which the protocol evidence says it does not | audit_flow, audit_permissions |\n| 5 | info | One over-reported count is sticky (`CountWentDown`) and absorbs real merges | audit_economics |\n| 6 | info | 4-of-300 panel passes `WeakPanel`; panel/quorum are outside the question hash | audit_permissions |\n\nEach was reproduced in a scratch Foundry test (`test_A`…`test_F`, all passing on the current code to show the state is reachable; the reproductions are stated in the file). Severity was calibrated by impact: every harm depends on off-chain timing or oracle behaviour the contract cannot observe, and is bounded to one tranche or to the funder's own parameter choices.\n\n**Static-analysis leads dismissed with reasons** (recorded in coverage): the `abi.encodePacked` collision cannot occur because the prefix is pinned by `prefixHash` before hashing and the window suffix has fixed delimiters; the createVault \"reentrancy\" is behind `nonReentrant` with the vault id taken from array length after the transfer; the timestamp comparisons are the intended unix-second deadline design.\n\n**Coverage:** all 8 verifier entry points answered (4 `finding` referencing #1/#2, 4 `holds`), plus 7 extra rows for the constructor, views, hash/domain, the tool leads and custody conservation.","treeHash":null,"usage":{"cachedInputTokens":848690,"inputTokens":330,"model":"claude-fable-5-1","outputTokens":26360,"runtime":"claude","turns":20,"wallClockMs":363173}},{"artifacts":[],"attempt":1,"bundleHash":"391aed4e7daadc4b13a28064e08d446e07941e6089e3b2fc91dd8ad78a35c999","device":"f5975850d31ab58a","findings":[],"hash":"e87d7da764328bb5402d000d324af5c7dfa855314d06da8bcf7a26411aa4136d","nodeId":"9860290b-662c-4d4e-8318-c5a394237503","outcome":"completed","summary":"Created `launch.json` for ShipOrBurnIMD alone with no constructor arguments and Ethereum mainnet deployment notes.\n\nValidated manifest constraints and compiled ABI. Offline `forge build` succeeded; all 35 tests passed.\n\nOnly `launch.json` is changed for submission.","treeHash":"7ef28f598d17541e6df332b42014660abde433bc","usage":{"cachedInputTokens":212992,"inputTokens":43746,"model":"gpt-6-astra","outputTokens":2517,"runtime":"codex","turns":3,"wallClockMs":110580}}],"verification":[{"checks":[{"durationMs":3007,"exitCode":0,"name":"build","output":"Compiling 51 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.88s\nCompiler run successful!\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:136:13\n    │\n136 │         if (deadline < block.timestamp + (uint256(tranches) + 1) * 20 hours) revert DeadlineTooSoon();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:166:9\n    │\n166 │         emit VaultCreated(id, address(token), builder, msg.sender, missTo, tranche, tranches, deadline, questionPrefix);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/ShipOrBurn.sol:142:13\n    │\n142 │         if (token.balanceOf(address(this)) - balanceBefore != total) revert UnsupportedToken();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/ShipOrBurn.sol:156:29\n    │\n156 │                 startBlock: uint64(block.number),\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint64' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:182:13\n    │\n182 │         if (block.timestamp > v.deadline) revert PastDeadline();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:194:13\n    │\n194 │         if (block.timestamp > a.expiresAt) revert AttestationExpired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:211:13\n    │\n211 │             emit Baseline(id, count, a.toBlock, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:227:13\n    │\n227 │             emit Shipped(id, day, count, v.tranche, streak, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:231:13\n    │\n231 │             emit Missed(id, day, count, v.tranche, v.missTo, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:239:13\n    │\n239 │         if (block.timestamp <= v.deadline) revert NotYet();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n    ╭▸ src/ShipOrBurn.sol:256:13\n    │\n256 │ ┏             abi.encodePacked(\n257 │ ┃                 questionPrefix,\n258 │ ┃                 '{\"fromBlock\":',\n259 │ ┃                 Strings.toString(fromBlock),\n    ‡ ┃\n262 │ ┃                 \"}}\"\n263 │ ┃             )\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/encode-packed-collision\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:135:21\n    │\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ────────━━━━━━━━━━━━━━━────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:144:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n144 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:93:78\n    │\n 93 │         id = sob.createVault(imd, builder, refund, TRANCHE, TRANCHES, uint64(block.timestamp + 10 days), prefix);\n    │                                                                              ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:134:23\n    │\n134 │         if (toBlock > block.number) {\n    │                       ━━━━━━━━━━━━\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:135:59\n    │\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │                                                           ━━━━━━━━━━━━\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:116:30\n    │\n116 │             issuedAt: uint64(block.timestamp),\n    │                              ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:117:31\n    │\n117 │             expiresAt: uint64(block.timestamp + 7 days)\n    │                               ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:169:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n169 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:179:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n179 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:275:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n275 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:293:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n293 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:323:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n323 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:357:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n357 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:379:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n379 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:387:28\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n387 │         _settle(id, uint64(block.number), 9);\n    │                            ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:395:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n395 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:409:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n409 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:426:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n426 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:445:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n445 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:481:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n481 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:93:78\n    │\n 93 │         id = sob.createVault(imd, builder, refund, TRANCHE, TRANCHES, uint64(block.timestamp + 10 days), prefix);\n    │                                                                              ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:116:30\n    │\n116 │             issuedAt: uint64(block.timestamp),\n    │                              ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:117:31\n    │\n117 │             expiresAt: uint64(block.timestamp + 7 days)\n    │                               ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:488:17\n    │\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────━━━━━━━━━━━━━━━─────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:571:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n571 │         uint64 t = uint64(block.number) + 10;\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":7428,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/ShipOrBurnIMD.t.sol:ShipOrBurnIMDTest\n[PASS] test_constructorCreatesOnlyTheApplicationAndMakesNoExternalCalls() (gas: 216146)\n[PASS] test_factoryDeploysOnlyShipOrBurnIMDWithoutDependencies() (gas: 3415318)\n[PASS] test_usesImdSigner() (gas: 14463)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 117.51ms (8.81ms CPU time)\n\nRan 5 tests for test/GoldenVectors.t.sol:GoldenVectorsTest\n[PASS] test_protocolConformanceVectorOnProductionContract() (gas: 81666)\n[PASS] test_questionHash_matchesRealImdRequest() (gas: 9277)\n[PASS] test_realImdSignatureRecovers() (gas: 40221)\n[PASS] test_realImdV2SignatureRecovers() (gas: 77691)\n[PASS] test_v2DigestMatchesReference() (gas: 73873)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 117.55ms (1.20ms CPU time)\n\nRan 10 tests for test/ShipOrBurnAdversarial.t.sol:ShipOrBurnIMDBoundariesTest\n[PASS] testFuzz_fullWidthDepositExpiresExactly(uint128,uint16,bool) (runs: 1000, μ: 770885, ~: 765202)\n[PASS] test_deadlineOneSecondTooShortIsAtomic() (gas: 145585)\n[PASS] test_deploymentDoesNotExposeAdministrationOrAcceptEther() (gas: 177750)\n[PASS] test_expiryTransferFailureRollsBackAndCanRetry() (gas: 770802)\n[PASS] test_failedDepositsCannotCreateVaultOrConsumeFunds() (gas: 390311)\n[PASS] test_insufficientAllowanceAndBalanceAreAtomic() (gas: 297154)\n[PASS] test_noReturnTokenCanFundAndExpire() (gas: 808084)\n[PASS] test_oneWeiAndMaximumDeposit() (gas: 1506503)\n[PASS] test_prefixLengthBoundaries() (gas: 1250353)\n[PASS] test_scheduleEmitsWithoutChangingVaultEvenAfterExpiry() (gas: 808176)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 117.76ms (123.29ms CPU time)\n\nRan 29 tests for test/ShipOrBurn.t.sol:ShipOrBurnTest\n[PASS] testFuzz_conservation(uint256,uint8) (runs: 1000, μ: 820165, ~: 763579)\n[PASS] test_acceptsExactly600BlocksOfLag() (gas: 927527)\n[PASS] test_baselineMustPostdateVault() (gas: 461189)\n[PASS] test_blockedBuilderPreventsShippingButAllowsExpiry() (gas: 1550289)\n[PASS] test_blockedFunderPreventsRefundAndExpiry() (gas: 1519752)\n[PASS] test_emitsVerdictEvents() (gas: 892625)\n[PASS] test_expire_sendsTheRestToMissTo() (gas: 990115)\n[PASS] test_futureIssuedAtIsAcceptedIfSignerApproves() (gas: 511949)\n[PASS] test_onlyFunderLinksSchedule() (gas: 372678)\n[PASS] test_prefixMatchesQuestionTool() (gas: 50765)\n[PASS] test_refundVault_sendsMissesToFunder() (gas: 744113)\n[PASS] test_rejectsAnotherQuestion() (gas: 501068)\n[PASS] test_rejectsBadParams() (gas: 3927)\n[PASS] test_rejectsBadPrefixes() (gas: 735783)\n[PASS] test_rejectsCountGoingDown() (gas: 642181)\n[PASS] test_rejectsExpiredAttestation() (gas: 451373)\n[PASS] test_rejectsFeeOnTransferTokens() (gas: 848097)\n[PASS] test_rejectsForgedSignature() (gas: 457877)\n[PASS] test_rejectsFutureBaselineAndVerdict() (gas: 1068558)\n[PASS] test_rejectsNonUintAnswer() (gas: 449692)\n[PASS] test_rejectsReplayAndTooSoon() (gas: 714439)\n[PASS] test_rejectsShortDeadline() (gas: 106311)\n[PASS] test_rejectsStaleBaselineThenAcceptsFreshBaseline() (gas: 765183)\n[PASS] test_rejectsTamperedWindow() (gas: 449618)\n[PASS] test_rejectsVerdictAt601BlocksOfLag() (gas: 697967)\n[PASS] test_rejectsWeakPanels() (gas: 722696)\n[PASS] test_shipMissShip_thenClosed() (gas: 1290077)\n[PASS] test_staleCatchUpCannotBurnAShippedTranche() (gas: 1215818)\n[PASS] test_staleCatchUpCannotRefundAShippedTranche() (gas: 1195597)\nSuite result: ok. 29 passed; 0 failed; 0 skipped; finished in 117.76ms (142.89ms CPU time)\n\nRan 9 tests for test/ShipOrBurnAdversarial.t.sol:ShipOrBurnAttestationAdversarialTest\n[PASS] testFuzz_failedVerdictTransferCanRetrySameSignature(bool,bool) (runs: 1000, μ: 1079289, ~: 1076591)\n[PASS] testFuzz_rejectsNonWordAnswerLengths(uint8) (runs: 1000, μ: 564396, ~: 564182)\n[PASS] testFuzz_tokenCallbacksCannotReenterCustody(uint8,uint8) (runs: 256, μ: 1286961, ~: 1176135)\n[PASS] test_allFifteenAttestationFieldsAreBoundToSignature() (gas: 3909129)\n[PASS] test_exactSignatureExpiryAndDeadlineAreInclusive() (gas: 960330)\n[PASS] test_rejectsMalformedAndMalleableSignatures() (gas: 1499119)\n[PASS] test_rejectsSignaturesForOtherConsumerOrDomainChain() (gas: 921241)\n[PASS] test_sameAttestationCanSettleTwoVaultsButCannotReplayEither() (gas: 1547959)\n[PASS] test_spacing5999Rejected6000AcceptedAndMaximumCountCannotWrap() (gas: 1455330)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 117.84ms (329.99ms CPU time)\n\nRan 2 tests for test/ShipOrBurnInvariant.t.sol:ShipOrBurnInvariantTest\n[PASS]\nShipOrBurnInvariantTest invariants:\n[PASS] invariant_custodyEqualsOutstandingFundingPlusDonations\n[PASS] invariant_vaultStateMatchesVerdictsAndClosedVaultsStayClosed\n ShipOrBurnInvariantTest invariants (runs: 256, calls: 16384, reverts: 0)\n\n╭-------------------+---------------+-------+---------+----------╮\n| Contract          | Selector      | Calls | Reverts | Discards |\n+================================================================+\n| ShipOrBurnHandler | advanceTime   | 2357  | 0       | 0        |\n|-------------------+---------------+-------+---------+----------|\n| ShipOrBurnHandler | create        | 2364  | 0       | 0        |\n|-------------------+---------------+-------+---------+----------|\n| ShipOrBurnHandler | donate        | 2317  | 0       | 0        |\n|-------------------+---------------+-------+---------+----------|\n| ShipOrBurnHandler | expire        | 2261  | 0       | 0        |\n|-------------------+---------------+-------+---------+----------|\n| ShipOrBurnHandler | linkSchedule  | 2402  | 0       | 0        |\n|-------------------+---------------+-------+---------+----------|\n| ShipOrBurnHandler | rejectInvalid | 2312  | 0       | 0        |\n|-------------------+---------------+-------+---------+----------|\n| ShipOrBurnHandler | verdict       | 2371  | 0       | 0        |\n╰-------------------+---------------+-------+---------+----------╯\n\n[PASS] test_handlerReachesBaselineShipMissExpiryAndRejection() (gas: 2070714)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.34s (7.34s CPU time)\n\nRan 6 test suites in 7.34s (7.93s CPU time): 58 tests passed, 0 failed, 0 skipped (58 total tests)\n","passed":true},{"durationMs":42,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShipOrBurn.createVault(address,address,bool,uint128,uint16,uint64,bytes)\",\"ShipOrBurn.expire(uint256)\",\"ShipOrBurn.linkSchedule(uint256,string)\",\"ShipOrBurn.settle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes,bytes)\",\"ShipOrBurnIMD.createVault(address,address,bool,uint128,uint16,uint64,bytes)\",\"ShipOrBurnIMD.expire(uint256)\",\"ShipOrBurnIMD.linkSchedule(uint256,string)\",\"ShipOrBurnIMD.settle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes,bytes)\"],\"files\":{\".github/workflows/ci.yml\":15,\".gitignore\":5,\"ADAPTATION.md\":99,\"README.md\":54,\"foundry.toml\":21,\"script/Deploy.s.sol\":14,\"src/ShipOrBurn.sol\":315,\"src/ShipOrBurnIMD.sol\":13,\"test/GoldenVectors.t.sol\":177,\"test/ShipOrBurn.t.sol\":597,\"test/ShipOrBurnAdversarial.t.sol\":523,\"test/ShipOrBurnIMD.t.sol\":93,\"test/ShipOrBurnInvariant.t.sol\":386,\"test/TESTING.md\":30,\"test/helpers/ShipOrBurnTestSupport.sol\":67},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"1a6394b385f87a5a8f89bafd0250a9cfa071a2008d886da8a5067ce444e27a4b","verifiedTreeHash":"73ce4a1a21a8622b7acc96469dbe408552df86fd","verifierVersion":"0.1.0+ad90ce4c"},{"checks":[{"durationMs":1932,"exitCode":0,"name":"build","output":"Compiling 48 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.81s\nCompiler run successful!\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:136:13\n    │\n136 │         if (deadline < block.timestamp + (uint256(tranches) + 1) * 20 hours) revert DeadlineTooSoon();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:166:9\n    │\n166 │         emit VaultCreated(id, address(token), builder, msg.sender, missTo, tranche, tranches, deadline, questionPrefix);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/ShipOrBurn.sol:142:13\n    │\n142 │         if (token.balanceOf(address(this)) - balanceBefore != total) revert UnsupportedToken();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/ShipOrBurn.sol:156:29\n    │\n156 │                 startBlock: uint64(block.number),\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint64' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:182:13\n    │\n182 │         if (block.timestamp > v.deadline) revert PastDeadline();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:194:13\n    │\n194 │         if (block.timestamp > a.expiresAt) revert AttestationExpired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:211:13\n    │\n211 │             emit Baseline(id, count, a.toBlock, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:227:13\n    │\n227 │             emit Shipped(id, day, count, v.tranche, streak, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:231:13\n    │\n231 │             emit Missed(id, day, count, v.tranche, v.missTo, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:239:13\n    │\n239 │         if (block.timestamp <= v.deadline) revert NotYet();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n    ╭▸ src/ShipOrBurn.sol:256:13\n    │\n256 │ ┏             abi.encodePacked(\n257 │ ┃                 questionPrefix,\n258 │ ┃                 '{\"fromBlock\":',\n259 │ ┃                 Strings.toString(fromBlock),\n    ‡ ┃\n262 │ ┃                 \"}}\"\n263 │ ┃             )\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/encode-packed-collision\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:135:21\n    │\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ────────━━━━━━━━━━━━━━━────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:144:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n144 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:93:78\n    │\n 93 │         id = sob.createVault(imd, builder, refund, TRANCHE, TRANCHES, uint64(block.timestamp + 10 days), prefix);\n    │                                                                              ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:134:23\n    │\n134 │         if (toBlock > block.number) {\n    │                       ━━━━━━━━━━━━\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:135:59\n    │\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │                                                           ━━━━━━━━━━━━\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:116:30\n    │\n116 │             issuedAt: uint64(block.timestamp),\n    │                              ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:117:31\n    │\n117 │             expiresAt: uint64(block.timestamp + 7 days)\n    │                               ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:169:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n169 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:179:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n179 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:275:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n275 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:293:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n293 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:323:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n323 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:357:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n357 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:379:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n379 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:387:28\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n387 │         _settle(id, uint64(block.number), 9);\n    │                            ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:395:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n395 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:409:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n409 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:426:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n426 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:445:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n445 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:481:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n481 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:93:78\n    │\n 93 │         id = sob.createVault(imd, builder, refund, TRANCHE, TRANCHES, uint64(block.timestamp + 10 days), prefix);\n    │                                                                              ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:116:30\n    │\n116 │             issuedAt: uint64(block.timestamp),\n    │                              ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:117:31\n    │\n117 │             expiresAt: uint64(block.timestamp + 7 days)\n    │                               ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:488:17\n    │\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────━━━━━━━━━━━━━━━─────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:571:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n571 │         uint64 t = uint64(block.number) + 10;\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":152,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/ShipOrBurnIMD.t.sol:ShipOrBurnIMDTest\n[PASS] test_factoryDeploysOnlyShipOrBurnIMDWithoutDependencies() (gas: 3415053)\n[PASS] test_usesImdSigner() (gas: 14282)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 8.03ms (8.57ms CPU time)\n\nRan 4 tests for test/GoldenVectors.t.sol:GoldenVectorsTest\n[PASS] test_questionHash_matchesRealImdRequest() (gas: 9255)\n[PASS] test_realImdSignatureRecovers() (gas: 40210)\n[PASS] test_realImdV2SignatureRecovers() (gas: 77918)\n[PASS] test_v2DigestMatchesReference() (gas: 74089)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 61.79ms (1.53ms CPU time)\n\nRan 29 tests for test/ShipOrBurn.t.sol:ShipOrBurnTest\n[PASS] testFuzz_conservation(uint256,uint8) (runs: 1000, μ: 809547, ~: 762961)\n[PASS] test_acceptsExactly600BlocksOfLag() (gas: 927527)\n[PASS] test_baselineMustPostdateVault() (gas: 461189)\n[PASS] test_blockedBuilderPreventsShippingButAllowsExpiry() (gas: 1550289)\n[PASS] test_blockedFunderPreventsRefundAndExpiry() (gas: 1519752)\n[PASS] test_emitsVerdictEvents() (gas: 892625)\n[PASS] test_expire_sendsTheRestToMissTo() (gas: 990115)\n[PASS] test_futureIssuedAtIsAcceptedIfSignerApproves() (gas: 511949)\n[PASS] test_onlyFunderLinksSchedule() (gas: 372678)\n[PASS] test_prefixMatchesQuestionTool() (gas: 50765)\n[PASS] test_refundVault_sendsMissesToFunder() (gas: 744113)\n[PASS] test_rejectsAnotherQuestion() (gas: 501068)\n[PASS] test_rejectsBadParams() (gas: 3927)\n[PASS] test_rejectsBadPrefixes() (gas: 735783)\n[PASS] test_rejectsCountGoingDown() (gas: 642181)\n[PASS] test_rejectsExpiredAttestation() (gas: 451373)\n[PASS] test_rejectsFeeOnTransferTokens() (gas: 848097)\n[PASS] test_rejectsForgedSignature() (gas: 457877)\n[PASS] test_rejectsFutureBaselineAndVerdict() (gas: 1068558)\n[PASS] test_rejectsNonUintAnswer() (gas: 449692)\n[PASS] test_rejectsReplayAndTooSoon() (gas: 714439)\n[PASS] test_rejectsShortDeadline() (gas: 106311)\n[PASS] test_rejectsStaleBaselineThenAcceptsFreshBaseline() (gas: 765183)\n[PASS] test_rejectsTamperedWindow() (gas: 449618)\n[PASS] test_rejectsVerdictAt601BlocksOfLag() (gas: 697967)\n[PASS] test_rejectsWeakPanels() (gas: 722696)\n[PASS] test_shipMissShip_thenClosed() (gas: 1290077)\n[PASS] test_staleCatchUpCannotBurnAShippedTranche() (gas: 1215818)\n[PASS] test_staleCatchUpCannotRefundAShippedTranche() (gas: 1195597)\nSuite result: ok. 29 passed; 0 failed; 0 skipped; finished in 61.86ms (92.86ms CPU time)\n\nRan 3 test suites in 62.46ms (131.69ms CPU time): 35 tests passed, 0 failed, 0 skipped (35 total tests)\n","passed":true},{"durationMs":42,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShipOrBurn.createVault(address,address,bool,uint128,uint16,uint64,bytes)\",\"ShipOrBurn.expire(uint256)\",\"ShipOrBurn.linkSchedule(uint256,string)\",\"ShipOrBurn.settle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes,bytes)\",\"ShipOrBurnIMD.createVault(address,address,bool,uint128,uint16,uint64,bytes)\",\"ShipOrBurnIMD.expire(uint256)\",\"ShipOrBurnIMD.linkSchedule(uint256,string)\",\"ShipOrBurnIMD.settle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes,bytes)\"],\"files\":{\".github/workflows/ci.yml\":15,\".gitignore\":5,\"ADAPTATION.md\":99,\"README.md\":54,\"foundry.toml\":21,\"script/Deploy.s.sol\":14,\"src/ShipOrBurn.sol\":315,\"src/ShipOrBurnIMD.sol\":13,\"test/GoldenVectors.t.sol\":143,\"test/ShipOrBurn.t.sol\":597,\"test/ShipOrBurnIMD.t.sol\":58},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1960,"exitCode":0,"name":"slither","output":"[low/medium] timestamp at src/ShipOrBurn.sol:236: ShipOrBurn.expire(uint256) (src/ShipOrBurn.sol#236-244) uses timestamp for comparisons\n[low/medium] timestamp at src/ShipOrBurn.sol:123: ShipOrBurn.createVault(IERC20,address,bool,uint128,uint16,uint64,bytes) (src/ShipOrBurn.sol#123-167) uses timestamp for comparisons\n[low/medium] timestamp at src/ShipOrBurn.sol:176: ShipOrBurn.settle(uint256,ShipOrBurn.Attestation,bytes,bytes) (src/ShipOrBurn.sol#176-233) uses timestamp for comparisons","passed":true},{"durationMs":509,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/ShipOrBurn.sol:256: `abi.encodePacked()` Hash Collision\n[high] reentrancy-state-change at src/ShipOrBurn.sol:140: Reentrancy: State change after external call (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"b67c7c9164191d388b46b8d7a0909515aecf1a93a27acf17b47f6f358458ea95","verifiedTreeHash":"81822260bb418dcf6adcfaa91cf3c0f7747e0d2e","verifierVersion":"0.1.0+ad90ce4c"},{"checks":[{"durationMs":1876,"exitCode":0,"name":"build","output":"Compiling 48 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.75s\nCompiler run successful!\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:136:13\n    │\n136 │         if (deadline < block.timestamp + (uint256(tranches) + 1) * 20 hours) revert DeadlineTooSoon();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:166:9\n    │\n166 │         emit VaultCreated(id, address(token), builder, msg.sender, missTo, tranche, tranches, deadline, questionPrefix);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value\n    ╭▸ src/ShipOrBurn.sol:142:13\n    │\n142 │         if (token.balanceOf(address(this)) - balanceBefore != total) revert UnsupportedToken();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/incorrect-strict-equality\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/ShipOrBurn.sol:156:29\n    │\n156 │                 startBlock: uint64(block.number),\n    │                             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint64' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:182:13\n    │\n182 │         if (block.timestamp > v.deadline) revert PastDeadline();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:194:13\n    │\n194 │         if (block.timestamp > a.expiresAt) revert AttestationExpired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:211:13\n    │\n211 │             emit Baseline(id, count, a.toBlock, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:227:13\n    │\n227 │             emit Shipped(id, day, count, v.tranche, streak, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/ShipOrBurn.sol:231:13\n    │\n231 │             emit Missed(id, day, count, v.tranche, v.missTo, a.requestId);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/ShipOrBurn.sol:239:13\n    │\n239 │         if (block.timestamp <= v.deadline) revert NotYet();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n    ╭▸ src/ShipOrBurn.sol:256:13\n    │\n256 │ ┏             abi.encodePacked(\n257 │ ┃                 questionPrefix,\n258 │ ┃                 '{\"fromBlock\":',\n259 │ ┃                 Strings.toString(fromBlock),\n    ‡ ┃\n262 │ ┃                 \"}}\"\n263 │ ┃             )\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/encode-packed-collision\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:135:21\n    │\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ────────━━━━━━━━━━━━━━━────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:144:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n144 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:93:78\n    │\n 93 │         id = sob.createVault(imd, builder, refund, TRANCHE, TRANCHES, uint64(block.timestamp + 10 days), prefix);\n    │                                                                              ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:134:23\n    │\n134 │         if (toBlock > block.number) {\n    │                       ━━━━━━━━━━━━\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:135:59\n    │\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │                                                           ━━━━━━━━━━━━\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:116:30\n    │\n116 │             issuedAt: uint64(block.timestamp),\n    │                              ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:117:31\n    │\n117 │             expiresAt: uint64(block.timestamp + 7 days)\n    │                               ━━━━━━━━━━━━━━━\n    ‡\n135 │             vm.warp(block.timestamp + (uint256(toBlock) - block.number) * 12);\n    │             ───────────────────────────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:169:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n169 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:179:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n179 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:275:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n275 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:293:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n293 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:323:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n323 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:357:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n357 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:379:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n379 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:387:28\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n387 │         _settle(id, uint64(block.number), 9);\n    │                            ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:395:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n395 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:409:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n409 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:426:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n426 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:445:31\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n445 │         uint64 start = uint64(block.number);\n    │                               ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:481:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n481 │         uint64 b = uint64(block.number);\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:93:78\n    │\n 93 │         id = sob.createVault(imd, builder, refund, TRANCHE, TRANCHES, uint64(block.timestamp + 10 days), prefix);\n    │                                                                              ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:116:30\n    │\n116 │             issuedAt: uint64(block.timestamp),\n    │                              ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:117:31\n    │\n117 │             expiresAt: uint64(block.timestamp + 7 days)\n    │                               ━━━━━━━━━━━━━━━\n    ‡\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ShipOrBurn.t.sol:488:17\n    │\n488 │         vm.warp(block.timestamp + 10 days + 1);\n    │         ────────━━━━━━━━━━━━━━━─────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/ShipOrBurn.t.sol:571:27\n    │\n136 │             vm.roll(toBlock);\n    │             ──────────────── `vm.roll` changes this environment here\n    ‡\n571 │         uint64 t = uint64(block.number) + 10;\n    │                           ━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":145,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/GoldenVectors.t.sol:GoldenVectorsTest\n[PASS] test_questionHash_matchesRealImdRequest() (gas: 9255)\n[PASS] test_realImdSignatureRecovers() (gas: 40210)\n[PASS] test_realImdV2SignatureRecovers() (gas: 77918)\n[PASS] test_v2DigestMatchesReference() (gas: 74089)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 935.28µs (1.19ms CPU time)\n\nRan 2 tests for test/ShipOrBurnIMD.t.sol:ShipOrBurnIMDTest\n[PASS] test_factoryDeploysOnlyShipOrBurnIMDWithoutDependencies() (gas: 3415053)\n[PASS] test_usesImdSigner() (gas: 14282)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 10.12ms (10.21ms CPU time)\n\nRan 29 tests for test/ShipOrBurn.t.sol:ShipOrBurnTest\n[PASS] testFuzz_conservation(uint256,uint8) (runs: 1000, μ: 809174, ~: 762961)\n[PASS] test_acceptsExactly600BlocksOfLag() (gas: 927527)\n[PASS] test_baselineMustPostdateVault() (gas: 461189)\n[PASS] test_blockedBuilderPreventsShippingButAllowsExpiry() (gas: 1550289)\n[PASS] test_blockedFunderPreventsRefundAndExpiry() (gas: 1519752)\n[PASS] test_emitsVerdictEvents() (gas: 892625)\n[PASS] test_expire_sendsTheRestToMissTo() (gas: 990115)\n[PASS] test_futureIssuedAtIsAcceptedIfSignerApproves() (gas: 511949)\n[PASS] test_onlyFunderLinksSchedule() (gas: 372678)\n[PASS] test_prefixMatchesQuestionTool() (gas: 50765)\n[PASS] test_refundVault_sendsMissesToFunder() (gas: 744113)\n[PASS] test_rejectsAnotherQuestion() (gas: 501068)\n[PASS] test_rejectsBadParams() (gas: 3927)\n[PASS] test_rejectsBadPrefixes() (gas: 735783)\n[PASS] test_rejectsCountGoingDown() (gas: 642181)\n[PASS] test_rejectsExpiredAttestation() (gas: 451373)\n[PASS] test_rejectsFeeOnTransferTokens() (gas: 848097)\n[PASS] test_rejectsForgedSignature() (gas: 457877)\n[PASS] test_rejectsFutureBaselineAndVerdict() (gas: 1068558)\n[PASS] test_rejectsNonUintAnswer() (gas: 449692)\n[PASS] test_rejectsReplayAndTooSoon() (gas: 714439)\n[PASS] test_rejectsShortDeadline() (gas: 106311)\n[PASS] test_rejectsStaleBaselineThenAcceptsFreshBaseline() (gas: 765183)\n[PASS] test_rejectsTamperedWindow() (gas: 449618)\n[PASS] test_rejectsVerdictAt601BlocksOfLag() (gas: 697967)\n[PASS] test_rejectsWeakPanels() (gas: 722696)\n[PASS] test_shipMissShip_thenClosed() (gas: 1290077)\n[PASS] test_staleCatchUpCannotBurnAShippedTranche() (gas: 1215818)\n[PASS] test_staleCatchUpCannotRefundAShippedTranche() (gas: 1195597)\nSuite result: ok. 29 passed; 0 failed; 0 skipped; finished in 60.51ms (86.10ms CPU time)\n\nRan 3 test suites in 61.21ms (71.57ms CPU time): 35 tests passed, 0 failed, 0 skipped (35 total tests)\n","passed":true},{"durationMs":41,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"ShipOrBurn.createVault(address,address,bool,uint128,uint16,uint64,bytes)\",\"ShipOrBurn.expire(uint256)\",\"ShipOrBurn.linkSchedule(uint256,string)\",\"ShipOrBurn.settle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes,bytes)\",\"ShipOrBurnIMD.createVault(address,address,bool,uint128,uint16,uint64,bytes)\",\"ShipOrBurnIMD.expire(uint256)\",\"ShipOrBurnIMD.linkSchedule(uint256,string)\",\"ShipOrBurnIMD.settle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes,bytes)\"],\"files\":{\".github/workflows/ci.yml\":15,\".gitignore\":5,\"ADAPTATION.md\":99,\"README.md\":54,\"foundry.toml\":21,\"launch.json\":10,\"script/Deploy.s.sol\":14,\"src/ShipOrBurn.sol\":315,\"src/ShipOrBurnIMD.sol\":13,\"test/GoldenVectors.t.sol\":143,\"test/ShipOrBurn.t.sol\":597,\"test/ShipOrBurnIMD.t.sol\":58},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e87d7da764328bb5402d000d324af5c7dfa855314d06da8bcf7a26411aa4136d","verifiedTreeHash":"7ef28f598d17541e6df332b42014660abde433bc","verifierVersion":"0.1.0+ad90ce4c"}]}