{"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":"0b8ab633-6dd6-43ea-bf84-5edf3731bb25","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"2c52c26a4d7138be57107281776a5d10a5e84e19f15a100d3673c51a9c00f285","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"37843d05302eedac7a4b6b516acad275ec07aae5e661bf266e915b92ceb2b1b1","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"waiting"},{"acceptedSubmissionHash":"b4eb4cd463ac1203ef7d9bea2cad50477a1916dad10f3e4d8ae02e243eb5bf81","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"a15e62a47faa970ef8856c9208d3008d0cac53e69db35521a736a0624228be61","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"5997c12992c7cb7c01eeeba896bb37c84daeee5d1ee62343d876f5d3e71d86bd","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"816fe39c7efd64504a19175b0f4dd9186d61069f37a22684c90358a18c60bddc","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"816fe39c7efd64504a19175b0f4dd9186d61069f37a22684c90358a18c60bddc","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":null,"dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"failed"},{"acceptedSubmissionHash":"5f7c22a8823b1cae77e18f92a2f2284a3d20705fc22f75a50bd7a23891163ef9","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"ab3f8f2aa4e7f3b3ada558c0dcf522ba733ca3df3b1da92c707a0fe305816d5f","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"ab3f8f2aa4e7f3b3ada558c0dcf522ba733ca3df3b1da92c707a0fe305816d5f","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build and deploy on Ethereum mainnet the payer contract of the IMD burn tracker, a static dashboard the swarm hosts: a Foundry project with BurnTracker and BurnTrackerTest, which inherits BurnTracker and adds nothing, because this first deployment is a test. Deploy BurnTrackerTest with these constructor arguments: intake 0xa43e6f75ee006411f79ac1c84120606c2330de82, asset 0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7, openAction \"job.open@job-1\" and continueAction \"job.continue@job-1\" as bytes32 strings, bodyPrefix and bodySuffix = the UTF-8 bytes of the continuation template before and after its {{PARENT}} placeholder, cooldown 86400, staleAfter 172800; the owner role goes to this launch's payer (the body names no owner). No token, no pool, no other contracts.\n\nWHAT IT DOES. Its owner calls open(bytes body) once: the contract orders job.open@job-1 through IMD Intake v2 (0xa43e6f75ee006411f79ac1c84120606c2330de82) with that body, paying the Intake's live price in IMD (0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7) taken from the owner. That makes the contract the hosted site's payer; the plane accepts job.continue only from the address that paid the project, so only this contract can order later versions. After every completed version anyone may call updateBurntracker(bytes resultJson, uint256 maxPrice) with the result document (result.json) of the last completed job as its proof: the contract accepts the proof only if its keccak256 equals the result hash the Intake handed to onResult for that completion, reads the parent job id from it, and orders job.continue@job-1 with its stored body template around that id, paid by the caller at the live price, never above maxPrice (a higher price reverts before any token moves). Callers supply only this proof and cannot change what the swarm is asked to do.\n\nINTERFACE (exact; existing tools call it): open(bytes) returns (bytes32 requestId); updateBurntracker(bytes,uint256) returns (bytes32); recover(string parentJobId) returns (bytes32); setResultHash(bytes32); onResult(bytes32 requestId, bytes32 resultHash, string uri); onFailure(bytes32 requestId, uint8 status, bytes32 reason, uint16, uint16, bytes); parentJobIdOf(bytes) pure returns (bytes); views price() and nextUpdateAt(); status() returns (uint8 state, uint8 pendingKind, uint40 lastDone, uint40 orderedAt, uint40 staleAt, uint32 cooldown, uint32 staleAfter); public owner, intake, asset, openAction, continueAction, bodyPrefix, bodySuffix, resultHash, lastRequestId; owner-only setBody(bytes,bytes), setActions(bytes32,bytes32), setIntake(address,address), setTiming(uint32,uint32), withdraw(address token, address to, uint256 amount), two-step transferOwnership and acceptOwnership; events Ordered(bytes32 indexed requestId, uint8 kind, address indexed payer, uint256 amount, string parentJobId), Updated(bytes32 indexed requestId, bytes32 resultHash, string uri), Failed(bytes32 indexed requestId, uint8 status, bytes32 reason, uint8 newState). States UNOPENED 1, IDLE 2, PENDING 3, STUCK 4; kinds open 1, continue 2, recover 3.\n\nRULES. One order at a time. updateBurntracker needs IDLE and the cooldown passed since the last completion. Callbacks count only from the Intake, only for the order awaited, only once. onResult stores the hash, returns to IDLE and restarts the cooldown. onFailure status 1 (refused): a refused update returns to IDLE and restarts the cooldown, a refused open returns to UNOPENED, a refused recover goes to STUCK; status 2 (failed) goes to STUCK. open() is allowed again after the hosting job failed (STUCK) or stayed pending past its stale time. Blocked jobs never call back, so once an order has been pending past staleAfter (fixed when it is placed), or in IDLE or STUCK, the owner may recover(jobId), a continuation of the job it names paid by the owner, or setResultHash(hash) to adopt a completion whose callback was lost. A callback for an order no longer awaited is refused. cooldown 1 hour to 30 days, staleAfter 1 hour to 30 days.\n\nMEASURED FACTS (do not adapt them away): the Intake calls back with exactly 200,000 gas, and since Glamsterdam a storage slot written from zero costs about 111,000 of it, so every slot a callback writes must already be nonzero from the constructor on. result.json is canonical key-sorted JSON whose keccak256 is the hash onResult receives, 750 to 4,545 bytes, with the key \"jobId\" exactly once and a lowercase UUID as its value; the contract rejects a document without exactly one such key or with a malformed id. The launch's empty-chain check deploys only this contract, so the constructor must not require code at the Intake or the token.\n\nDECIDE, DO NOT ASK: a question blocks the job and costs the payer; where this text is silent, choose and report it.","parentJobId":null,"planHash":"efb9c209b2a473e5c053ff1fd591a937a5d6774a1a4ea1f3f37a128a2513e141","previousHash":"db1ad170555b7fbcc68935b5d89944d64a8df17fc8164ae0a05336208440e610","projectId":"0b8ab633-6dd6-43ea-bf84-5edf3731bb25","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2528-build-deploy-ethereum-mainnet"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52139","feedbackHash":"08fc68a461204089d19761ce7e981842be4f080942c2ece0082e907e1af06898","nodeKey":"audit_economics","submissionHash":"2c52c26a4d7138be57107281776a5d10a5e84e19f15a100d3673c51a9c00f285","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51509","feedbackHash":"87c002cc9f2d8ffa19543b0978d92aa49194a4cf810ddd2746becb5544658c8f","nodeKey":"audit_flow","submissionHash":"37843d05302eedac7a4b6b516acad275ec07aae5e661bf266e915b92ceb2b1b1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52682","feedbackHash":"29c2cdca2c7292f4888039d975f73b3a4d03139852f25353e0f9a2e61b0f6f7b","nodeKey":"audit_math","submissionHash":"b4eb4cd463ac1203ef7d9bea2cad50477a1916dad10f3e4d8ae02e243eb5bf81","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52021","feedbackHash":"c52e614eb9992563dc9d04dc5633b31327f63f6c9f9cd61c2888e6c12fbc9aef","nodeKey":"audit_permissions","submissionHash":"a15e62a47faa970ef8856c9208d3008d0cac53e69db35521a736a0624228be61","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52188","feedbackHash":"ce6eb8378b93721f7431d55da2e86780743074398d848d8a73e1961c305c9f12","nodeKey":"write_foundry_tests","submissionHash":"5f7c22a8823b1cae77e18f92a2f2284a3d20705fc22f75a50bd7a23891163ef9","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"5b4cb76797f8fc3fc39e8009f6f7b6cdd0d7cce3fac0914080a9a8536c00997c","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"079b3bf4557d8811","findings":[{"citation":"resolved","description":"Economic/flow-gap (periphery x first principles). updateBurntracker is the only path where a third party (a keeper) pays, and the brief promises 'Callers supply only this proof and cannot change what the swarm is asked to do'. But _order reads intake, asset, continueAction and the body template from live storage at execution time, and the owner can change all of them in any state with no delay (setIntake L219, setBody L211, setActions L215). A keeper that signs updateBurntracker(doc, maxPrice) against the advertised configuration can be front-run: the payment is pulled from the keeper and approved to whatever intake is configured when the tx lands. This is an owner trust power (the setters are required by the brief), so severity is low; it is reported because the keeper, not the owner, bears the loss, and nothing in the interface lets the keeper bind what they pay for.","line":219,"path":"src/BurnTracker.sol","reproduction":"State: IDLE, cooldown passed, resultHash = keccak256(doc). Keeper K approves 0.5e18 IMD to BurnTracker and submits updateBurntracker(doc, 0.5e18). Owner front-runs with setIntake(E, IMD) where E is a contract whose priceOf(...) returns 0.5e18 and whose requestWithFailure transferFrom's the approved 0.5e18 to the owner and returns a fresh nonzero id. K's tx then passes every check (state, cooldown, proof, priceOf=0.5e18<=maxPrice, balance deltas) and K's 0.5 IMD goes to the owner; no job is ordered at the real Intake 0xa43e...de82 and the state sits PENDING until stale. Expected per brief: the keeper funds only the fixed job.continue@job-1 continuation at the IMD Intake. The same front-run with setBody changes the objective K pays for. Mitigation that keeps the interface: make setIntake/setBody/setActions take effect only after a delay (e.g. >= cooldown) or only while state != IDLE, and emit the pending values so keepers can watch them.","severity":"low","snippet":"    function setIntake(address intake_, address asset_) external onlyOwner nonReentrant {\n        _setIntake(intake_, asset_);\n    }","title":"Keeper's updateBurntracker payment follows whatever intake/body/action the owner set in the same block; maxPrice is the only bound"},{"citation":"resolved","description":"Outside the economics area; recorded as a deploy-policy conflict, not a code defect. The brief explicitly asks for bodyPrefix/bodySuffix as constructor arguments, and the README says the brief wins. The launch recipe says 'Supported arguments are address, uint8…uint256, int8…int256, bool (0/1), bytes32 and structs of these' and 'Each constructorArgs entry is one ABI word'. bodySuffix_ is 790+ bytes (docs/constructor-arguments.json), so it cannot be one ABI word. If the manifest step enforces the recipe's rule, the launch stops at the manifest; whether it does needs service evidence (the manifest schema/encoder).","line":14,"path":"src/BurnTrackerTest.sol","reproduction":"Write launch.json constructorArgs for BurnTrackerTest per the recipe: entry 6 must be the hex of bodyPrefix_ (16 bytes) and entry 7 the ~790-byte hex of bodySuffix_ from docs/constructor-arguments.json. Neither is a single 32-byte ABI word, and the ABI encoding of a dynamic bytes argument is an offset plus a length plus data, not a value in place. Expected: an argument list the manifest encoder accepts. Actual: depends on whether the encoder supports dynamic bytes, which the recipe text says it does not.","severity":"info","snippet":"        bytes memory bodyPrefix_,\n        bytes memory bodySuffix_,","title":"Constructor takes two dynamic bytes arguments while the evm_contracts recipe only lists static one-word constructor arguments"}],"hash":"2c52c26a4d7138be57107281776a5d10a5e84e19f15a100d3673c51a9c00f285","nodeId":"4abaa96d-7f63-4d90-a9e7-2df8b905a6a8","outcome":"completed","summary":"I found no critical, high or medium defects in the economics. I've written two findings and a full coverage record to `.imd-findings.json`, and changed no project files. The test suite passes (53 of 53, fork tests excluded).\n\n**What holds:**\n- **One order at a time:** every path that places an order checks the state first. When the owner replaces a stale order, the old order's callback is refused afterwards.\n- **Payment:** the live price is read before any token call. A price above `maxPrice` reverts before any tokens move. The caller is charged exactly the price, the Intake gets approval for exactly that amount, and nothing is left held or approved afterwards. Tokens sent to the contract by others stay untouched.\n- **Callbacks:** they are accepted only from the Intake that took the awaited order, only for its `requestId`, only while PENDING, and only once. A bad ask from someone else that names this contract is refused, and callbacks move no funds.\n- **The 200,000-gas callback limit:** callbacks write only two storage slots, the result hash and the packed progress slot. Both are nonzero from the constructor on, and a zero hash (or the starting placeholder hash) is rejected.\n- **The state rules** match the brief exactly. That covers refusals, failures, staleness, `recover` and `setResultHash`.\n\n**The slither and aderyn leads don't hold:**\n- **Reentrancy (slither):** a single lock guards every external function.\n- **Strict equalities (slither):** they compare state codes, so they are intended.\n- **\"ETH transferred\" (aderyn):** the contract never sends ETH.\n\n**Findings:**\n1. **Low: the owner can change what a keeper pays for, in the same block.** A keeper is a third party who pays for the next update. When the keeper's `updateBurntracker` runs, it uses whatever intake, body and action are configured at that moment. The owner can submit `setIntake`, `setBody` or `setActions` ahead of it. For example, the owner front-runs with `setIntake(E, IMD)`, where `E` quotes 0.5 IMD and sends the approved payment to the owner. The keeper's 0.5 IMD then goes to the owner and no job is ordered at the real Intake. `maxPrice` is the only limit on the loss. This comes from owner powers the brief requires, so it is low. The suggested fix keeps the interface: setter changes take effect only after a delay.\n2. **Info (outside the economics area): the constructor's two `bytes` arguments may not fit the launch manifest.** The brief requires `bodyPrefix` and `bodySuffix` as constructor arguments. The launch recipe says each argument is a single 32-byte word, and `bodySuffix` is about 790 bytes. Whether the launch stops at the manifest depends on what the manifest step accepts, which I couldn't check.\n\nAll 26 entry points have a coverage row: 18 hold and 8 point to finding 1 (`updateBurntracker` and the three setters, on both contracts). Four invariant rows are added. The JSON parser and access control are other reviewers' areas, so I only spot-checked them.","treeHash":null,"usage":{"cachedInputTokens":797328,"inputTokens":3680,"model":"claude-opus-5-5","outputTokens":23026,"runtime":"claude","turns":13,"wallClockMs":347186}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"4fa6ac0d62fd2e01","findings":[],"hash":"2de546cc1c4ab7d2f382f7e3b3c1f0af441e950ab66670ed5ddf925d17a51f9a","nodeId":"5029b9a9-f962-4b6f-a1b7-2425a420652f","outcome":"failed","summary":"needs input: The accepted source still has the previous attempt's incompatible constructor. The compiled BurnTrackerTest ABI requires dynamic bytes bodyPrefix_ and bodySuffix_; the required template parts are 16 and 721 bytes. The permitted evm_contracts manifest supports only static ABI words, so no launch.json can both match that ABI and supply the approved template. Empty parts also revert with InvalidBody, and initialization calls are forbidden. Only launch.json may be changed, so this assignment cannot repair the accepted constructor. Required resolution: accept a source revision whose constructor takes only static arguments and initializes the exact continuation template internally. forge build succeeded and all 53 tests passed with the specified fuzz seed. — Can this task be reissued with accepted source whose BurnTrackerTest constructor takes only static arguments and initializes the exact approved continuation template internally?","treeHash":null,"usage":{"cachedInputTokens":169728,"inputTokens":41890,"model":"gpt-6.1-sol","outputTokens":2402,"runtime":"codex","turns":3,"wallClockMs":58294}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"74a99f640688d37b","findings":[{"citation":"resolved","description":"BurnTrackerTest's constructor (same signature, src/BurnTrackerTest.sol:14-15) declares bodyPrefix_ and bodySuffix_ as dynamic `bytes`. The launch recipe this project is deployed under states that each launch.json constructorArgs entry is one ABI word, that supported argument types are address, uintN, intN, bool, bytes32 and structs of those, and that there are no dynamic arguments. ABI-encoding the nine documented launch arguments (docs/constructor-arguments.json) produces 35 words, not 9: two offsets, two lengths, one word of prefix and 23 words of the 721-byte suffix. A manifest with 9 entries cannot carry that tail; a manifest with 35 entries does not have one entry per constructor parameter. Either way the launch stops at the manifest step, so nothing is deployed and the payer's job is spent. The brief does require the template parts as constructor input, and the README notes the conflict, but the manifest validator is code and does not read the brief. The project already carries the exact template as compiled constants in src/libraries/ContinuationTemplate.sol, so the brief's intent (template seeded at deployment, owner-settable afterwards via setBody) survives a fix that drops the two bytes parameters and seeds storage from ContinuationTemplate.prefix()/suffix() in the constructor; the launch then needs 7 static words. If the manifest step is confirmed to accept arbitrary-length hex strings and ABI-encode by the compiled constructor ABI, this finding is moot; that confirmation should be recorded before the manifest is written.","line":94,"path":"src/BurnTracker.sol","reproduction":"Run: cast abi-encode \"f(address,address,address,bytes32,bytes32,bytes,bytes,uint32,uint32)\" <owner> 0xa43e6F75ee006411F79Ac1C84120606C2330DE82 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 0x6a6f622e6f70656e406a6f622d31000000000000000000000000000000000000 0x6a6f622e636f6e74696e7565406a6f622d310000000000000000000000000000 0x7b22706172656e744a6f624964223a22 <721-byte suffix hex from docs/constructor-arguments.json> 86400 172800. Expected if the manifest rule held: 9 words. Actual: 35 words (0x460 bytes); counting from 1, word 6 is offset 0x120 (288), word 7 is offset 0x160 (352), words 8-9 are 86400 and 172800, word 10 is length 16, word 11 is the prefix data, word 12 is length 721, words 13-35 are the 23 suffix data words. A launch.json entry list of 9 one-word strings cannot reproduce these bytes, and the factory's CREATE2 of creationCode ++ 9 words reverts in the constructor's ABI decoder (offset words point past calldata).","severity":"medium","snippet":"        bytes memory bodyPrefix_,\n        bytes memory bodySuffix_,","title":"Constructor takes two dynamic bytes arguments the contracts-only launch manifest cannot express as one word each"},{"citation":"resolved","description":"The brief allows open() again 'after the hosting job failed (STUCK) or stayed pending past its stale time'. The check at line 126 does not look at which order produced STUCK or the stale PENDING: a failed continuation (onFailure status 2 with pendingKind CONTINUE or RECOVER) or a stale update also unlock open(). Combined with the rule at lines 167-169 that a refused open returns to UNOPENED, this creates a one-way trap: the contract has a completed, hosted project and a valid resultHash, the owner (reasonably, since the function is permitted) calls open() to replace the attempt, the plane refuses that body (body_refused, admission_refused, a content screen), and the state becomes UNOPENED. In UNOPENED, _requireRecoverable (line 304) rejects both recover() and setResultHash(), and updateBurntracker requires IDLE, so the hosted project cannot be continued from this contract until the owner pays for a second successful job.open that creates an unrelated new project, after which recover(oldJobId) becomes possible again from IDLE. The stored resultHash of the hosted project is retained but unreachable in the meantime. Impact is operational and bounded (one extra opening price plus delay), hence low. Minimal fix that keeps the brief's rules: in onFailure, when a refused OPEN arrives and a completion already exists (resultHash != UNSET_RESULT_HASH), return to STUCK instead of UNOPENED so recover()/setResultHash() stay available; alternatively record the kind of the order that produced STUCK/stale and let open() through only when it was OPEN, at the cost of removing the owner's escape to a fresh project after a project the plane permanently refuses.","line":126,"path":"src/BurnTracker.sol","reproduction":"State: owner open() -> Intake onResult(id, H, uri) -> IDLE with resultHash H. Warp past cooldown; keeper updateBurntracker(doc, 1e18) -> PENDING kind 2. Intake onFailure(id, 2, 'failed', 0, 0, '') -> status().state == 4 (STUCK). Owner open(body) succeeds (expected per 'hosting job failed' reading: InvalidState). Intake onFailure(newId, 1, 'body_refused', 0, 0, '') -> status().state == 1 (UNOPENED). Now owner recover('c9822c12-52e3-4d28-b474-a9ab751b10b3') reverts InvalidState, owner setResultHash(H) reverts InvalidState, keeper updateBurntracker(doc, 1e18) reverts InvalidState, while resultHash() still equals H. The only exit is another paid open() (0.5 IMD from the owner). test/scratch/OpenTrap.t.sol (test_refusedReopenAfterFailedUpdateStrandsHostedProject) walks exactly this sequence against a mock Intake and passes, i.e. the trap is reachable on the current code.","severity":"low","snippet":"        if (state != UNOPENED && state != STUCK && !_isStale()) revert InvalidState();","title":"open() is accepted from STUCK/stale states caused by update or recover orders, so a refused re-open drops a hosted project to UNOPENED where recover() and setResultHash() are closed"}],"hash":"37843d05302eedac7a4b6b516acad275ec07aae5e661bf266e915b92ceb2b1b1","nodeId":"775f25ff-0520-4b38-867d-6268d6a33658","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the result. The working tree is unchanged apart from the findings file and the ignored scratch test.\n\n**Outcome.** The control flow of `BurnTracker` is sound in the areas I was assigned. I traced every state-changing entry point through its guards, state writes, external calls and failure paths against the Execution Trace, Periphery and First Principles checklists, and found no fund-loss or callback-authentication defect. The existing suite of 53 tests passes, including the gas-and-storage checks that run both callbacks under exactly 200,000 gas and confirm they write only two slots that are nonzero from construction.\n\n**Findings recorded (2):**\n\n- **Medium, launch-blocking conflict.** The constructor takes two dynamic `bytes` arguments for the template parts. The contracts-only launch recipe allows only one static word per manifest entry and no dynamic arguments. Encoding the nine documented arguments produces 35 words, so a nine-entry `launch.json` cannot deploy this contract. The brief does ask for these as constructor input, so I framed it as a policy conflict to confirm before the manifest step, with a remedy that seeds storage from the existing compiled template constants and keeps `setBody` owner-settable.\n- **Low, operational trap.** `open()` is accepted from any STUCK or stale state, not only one produced by the hosting job. After a failed update, an owner re-open that the plane refuses drops the contract to UNOPENED. There, `recover` and `setResultHash` both revert, so the healthy hosted project cannot be continued until the owner pays for another successful opening. A scratch test under `test/scratch/` reproduces the exact sequence.\n\n**Coverage.** All 26 listed entry points have a row, plus rows for the constructor, three invariants, the parser and vendored libraries, and the static-analysis leads. Both `BurnTrackerTest` and `BurnTracker` share identical runtime, so the rows mirror each other. The owner's power to redirect future orders via `setIntake` is noted as a trust assumption, not a defect. The static-analysis reentrancy and ETH-transfer leads did not hold up on inspection.\n\n**Not reached.** I did not run the online mainnet fork profile, and the real Intake's `complete` path was verified only through the mock and the offline bytecode replay, not a live call.","treeHash":null,"usage":{"cachedInputTokens":1642104,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":45976,"runtime":"claude","turns":41,"wallClockMs":758568}},{"artifacts":[],"attempt":1,"bundleHash":"0ffda0e92d32fb9f8e5503e7ae6da9e8505fbaf8f7288e042f3f920ead7a8cb2","device":"e89087c02a1681bf","findings":[],"hash":"5997c12992c7cb7c01eeeba896bb37c84daeee5d1ee62343d876f5d3e71d86bd","nodeId":"ec2f7d73-7fa8-4ca0-b61e-6edf72cf32fa","outcome":"completed","summary":"Implemented BurnTracker and BurnTrackerTest with tests, real result fixtures, and deployment documentation.\n\n- Build, formatting, and all 53 tests pass.\n- Mainnet fork tests pass.\n- The 4,545-byte update uses 266,914 execution gas.\n- Callback storage/gas checks and runtime equality pass.\n\n[README](README.md) documents launch parameters and operations. Mainnet deployment remains with IMD’s launch process; no transaction was broadcast.","treeHash":"dc20011b9383ca426d7f3941e4d725fd56a49b30","usage":{"cachedInputTokens":10876544,"inputTokens":251376,"model":"gpt-6-astra","outputTokens":107863,"runtime":"codex","turns":10,"wallClockMs":3043521}},{"artifacts":[],"attempt":1,"bundleHash":"b674b1fd2a4e816b355bd1a35337005c10d1511bce404a5731226654718bdba1","device":"d4b53d656a3fbe07","findings":[{"description":"transferOwnership rejects address(0), so once the owner nominates an address it can accept until the owner nominates another address. Ownable2Step allows transferOwnership(address(0)) to clear the nomination. The impact is small because the owner can overwrite the nomination with an address it controls.","line":235,"path":"src/BurnTracker.sol","reproduction":"owner calls transferOwnership(bob), then transferOwnership(address(0)). Expected: the nomination is cleared. Actual: the call reverts InvalidAddress() and bob can still call acceptOwnership().","severity":"info","title":"A pending ownership nomination cannot be cancelled, only replaced"},{"description":"onResult with a zero hash (or the UNSET sentinel) and onFailure with a status other than 1 or 2 both revert. The Intake tries each callback once, so the order stays PENDING until its staleAt (48 hours with the launch timing). After that the owner has to call recover or setResultHash and pay again. The README documents this. The real Intake does not send these values, so it only happens if the Intake misbehaves.","line":165,"path":"src/BurnTracker.sol","reproduction":"Open an order, then the writer completes it with status 0 and resultHash 0. Expected: the callback settles. Actual: MockIntake.complete returns delivered=false, status() stays (3,1), and recover/setResultHash revert InvalidState until block.timestamp >= staleAt (covered by test_failedCallbackIsConsumedAndNeedsOwnerRecovery).","severity":"info","title":"A callback the contract rejects is lost for good, and the order stays PENDING until staleAfter"}],"hash":"5f7c22a8823b1cae77e18f92a2f2284a3d20705fc22f75a50bd7a23891163ef9","nodeId":"a464e78b-b9b9-4156-97b1-c438b142824a","outcome":"completed","summary":"I added invariant tests and more failure-path tests for `BurnTracker`/`BurnTrackerTest`. The full suite passes with the verifier's fuzz seed: 68 tests across 8 suites. Nothing outside `test/` was changed, and I found no defect that needed a failing proof.\n\n**New files**\n- **`test/invariant/BurnTrackerHandler.sol`**: a handler that calls the contract in random order with four actors. Before each call it predicts, from the brief's rules (not from the code), whether the call should succeed or revert. It covers:\n  - `open`, `updateBurntracker` (real proof, another known document, or a one-byte change; `maxPrice` below, at or above the live price), `recover` (valid or malformed ids) and `setResultHash`.\n  - Intake results and failures for the awaited order and for orders that were replaced; fake callbacks from other senders.\n  - Time moves, donations, `withdraw`, `setTiming` (in and out of range), switching `setBody` between the launch template and another one, and price changes including zero.\n  - For each order it checks the exact body bytes the Intake received, that the payer recorded is the contract, and the amount paid.\n- **`test/invariant/BurnTrackerInvariant.t.sol`**: runs against the deployed `BurnTrackerTest` with 256 runs × 128 calls, and the handler must never revert. After every call it checks:\n  - every call matched the prediction;\n  - all `status()` fields, `resultHash` and `lastRequestId` match the model;\n  - the contract holds only donations minus withdrawals and leaves no approval to the Intake;\n  - every payment reached the treasury, and actor balances add up;\n  - the two storage slots a callback writes are never zero;\n  - state and kind stay consistent and timing stays in range;\n  - the owner never changes.\n  - A fixed sequence test shows the handler really reaches every state, both callback types, a malformed but authenticated proof and a replaced order. A probe I ran (not submitted) confirmed random runs do reach real updates and completions.\n- **`test/EdgeCases.t.sol`**: 12 unit tests and one fuzz test (512 runs) for things the earlier suite didn't pin down:\n  - the stale deadline and cooldown checked one second either side;\n  - a refused update restarting the cooldown from the moment of refusal;\n  - `setTiming` leaving a placed deadline alone but changing the cooldown;\n  - `maxPrice` one wei below or at the price, and a price raised after it was quoted;\n  - every malformed shape of job id rejected;\n  - owner-only paths in every state where they're allowed;\n  - late callbacks for replaced or manually adopted orders refused;\n  - the old owner losing all powers after a handover.\n  - The fuzz test checks that an update never charges more than `maxPrice` or before the cooldown, and leaves no balance or approval behind.\n\n**Findings** (`.imd-findings.json`, both info level, no proof needed):\n1. A pending ownership nomination can't be cancelled, because `transferOwnership(address(0))` reverts; it can only be replaced with another address.\n2. A callback the contract rejects (a zero result hash, or a failure status other than 1 or 2) is lost for good. The order then stays pending until its stale time (48 hours here), after which the owner has to recover and pay again. The README documents this, and the real Intake doesn't send these values.\n\n**Choices I made where the brief was silent:**\n- I measured the deadlines as \"at or after\" the stated time, which is how the contract behaves.\n- I treated a duplicate `jobId` inside a nested object as a second key, so such a document is rejected.\n\nThe new files are not committed.","treeHash":"a7b6842eaf5fc8cd07e2a1f226311e3e2aa5d295","usage":{"cachedInputTokens":1917654,"inputTokens":42,"model":"claude-opus-5-5","outputTokens":36570,"runtime":"claude","turns":24,"wallClockMs":708246}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"229c8cb0d9067fbe","findings":[{"citation":"resolved","description":"The launch recipe and manifest rules state that every constructorArgs entry is one ABI word (address, uintN, intN, bool, bytes32, structs of these; no dynamic arguments) and at most 16 entries per contract. BurnTrackerTest's constructor has nine parameters of which two are dynamic `bytes` (bodyPrefix_ 16 bytes, bodySuffix_ 721 bytes). The real ABI encoding of the nine launch arguments is 35 words (9 head words plus offset/length/data tails), so a nine-entry static-word list cannot reproduce it, and chunking the 721-byte suffix into bytes32 words would need 23 extra entries, above the 16-argument cap. The README acknowledges the conflict and asserts the brief wins, but nothing in the tree shows the manifest/deployer can carry dynamic bytes, so the manifest step is likely to park the launch. Trade-off for the author/judge: either obtain evidence that launch.json accepts dynamic bytes, or keep the brief's behaviour (template owner-settable via setBody, exact bytes stored in bodyPrefix/bodySuffix) while seeding the constructor from the already-compiled ContinuationTemplate library instead of two bytes parameters, leaving seven static-word arguments. The second option changes the agreed constructor signature and is a scope decision.","line":14,"path":"src/BurnTrackerTest.sol","reproduction":"In Foundry: bytes memory nineWords = abi.encode(owner, intake, asset, bytes32('job.open@job-1'), bytes32('job.continue@job-1'), bytes32(0), bytes32(0), uint32(86400), uint32(172800)); create(0, creationCode ++ nineWords) returns address(0) (constructor abi-decoding reverts) while creationCode ++ abi.encode(..., ContinuationTemplate.prefix(), ContinuationTemplate.suffix(), ...) (35 words) deploys. Expected: a manifest-expressible argument list deploys the contract; actual: no list of at most 16 one-word entries can.","severity":"medium","snippet":"        bytes memory bodyPrefix_,\n        bytes memory bodySuffix_,","title":"Constructor takes two dynamic bytes arguments that the one-word-per-entry launch manifest cannot encode"},{"citation":"resolved","description":"open() is reachable from any STUCK or stale PENDING state, including those produced by a failed update (kind 2) or recover (kind 3) on a project that already exists and has a stored resultHash. onFailure status 1 for kind OPEN unconditionally returns to UNOPENED regardless of whether a project was already established. In UNOPENED only open() is allowed: recover(), setResultHash() and updateBurntracker() all revert with InvalidState, so the owner's only direct path forward is to pay for a brand new job.open, orphaning the hosted project this contract already paid for. Asymmetry: the refused-recover branch goes to STUCK (recoverable), the refused-open branch goes to UNOPENED even when resultHash != UNSET_RESULT_HASH. The spec text literally says 'a refused open returns to UNOPENED', so this is reported as a state-machine trap for the author to decide on, not a permission bypass. A workaround exists through owner configuration: setActions(continueAction, continueAction) then open(rawContinuationBody). A minimal fix preserving the spec for the first-ever open: when resultHash != UNSET_RESULT_HASH (a project exists), route a refused open to STUCK instead of UNOPENED.","line":168,"path":"src/BurnTracker.sol","reproduction":"1) owner open(body) -> onResult(id, H) -> IDLE. 2) warp to nextUpdateAt(); keeper updateBurntracker(doc, max) -> onFailure(id2, 2) -> STUCK (state 4). 3) owner open(body) -> PENDING kind 1; onFailure(id3, 1) -> state 1 (UNOPENED), resultHash still H. 4) owner recover(uuid) reverts InvalidState; owner setResultHash(x) reverts InvalidState; updateBurntracker(doc,max) reverts InvalidState. Expected: owner can still continue the existing project; actual: only a new job.open is accepted (scratch test test_refusedReopenOrphansExistingProject passes on current code, showing the trap).","severity":"low","snippet":"            if (_progress.pendingKind == OPEN) {\n                newState = UNOPENED;","title":"A refused re-open after STUCK resets to UNOPENED and locks the owner out of recover/setResultHash for the existing project"},{"citation":"resolved","description":"updateBurntracker binds the caller only to the proof bytes and maxPrice; the destination contract (intake), the token (asset), the action id and the body template are all read from owner-settable storage at execution time. An owner (or whoever obtains the key) can front-run a keeper's transaction with setIntake to a contract whose priceOf returns any value <= the keeper's maxPrice and which pulls the approved amount, so the keeper's IMD goes to the owner-controlled address instead of the Intake, and the order never reaches the plane. Access x economics seam: the guard is correct (onlyOwner), the pull is correct (exactly amount from msg.sender), the combination lets the privileged actor extract from a different user class. This is an intended owner power documented in the README and is reported as a trust assumption, not a defect. Mitigations if wanted without changing the interface: keepers approve exactly the live price per call and set maxPrice to it; or a short timelock on setIntake/setActions/setBody.","line":219,"path":"src/BurnTracker.sol","reproduction":"IDLE after cooldown. owner: setIntake(rogue, imd) where rogue.priceOf returns 5e18 and rogue.requestWithFailure transfers the approved 5e18 to owner. keeper (approved 100e18 to the tracker): updateBurntracker(doc, 5e18) succeeds. Keeper balance -5e18, owner balance +5e18, nothing ordered at the real Intake (scratch test test_ownerCanRedirectKeeperPaymentUpToMaxPrice).","severity":"info","snippet":"    function setIntake(address intake_, address asset_) external onlyOwner nonReentrant {","title":"Trust assumption: owner can redirect a public caller's in-flight payment (up to maxPrice) by changing intake, asset, actions or body"},{"citation":"resolved","description":"The brief says the owner role goes to this launch's payer and names no address. README line 37 and docs/constructor-arguments.json present 0x887b86B6B6957F7bbeA88B8CEfD392f39236A88C as 'this launch's payer', a claim nothing in the tree can justify. If the later manifest step copies this literal into constructorArgs instead of the $owner placeholder and the actual paying wallet differs, every onlyOwner power (open, recover, setResultHash, setIntake, withdraw, ownership transfer) is granted to the wrong party with no recovery, since the contract has no other admin. Access-control initialization lead: the role assignment at deployment must come from the launch's resolved payer. Recommendation: the manifest writes \"$owner\" for owner_, and the README/docs mark the literal as example only.","line":9,"path":"script/LaunchParameters.sol","reproduction":"Deploy BurnTrackerTest with owner_ = 0x887b86B6B6957F7bbeA88B8CEfD392f39236A88C while the launch payer is any other wallet P: P calls open(body) -> reverts Unauthorized(); the holder of 0x887b... controls the contract. Expected per brief: owner == payer.","severity":"low","snippet":"    address internal constant OWNER = 0x887b86B6B6957F7bbeA88B8CEfD392f39236A88C;","title":"Tree hardcodes a specific owner address the brief never stated; manifest must use $owner, not this literal"},{"citation":"resolved","description":"The spec allows open() again 'after the hosting job failed (STUCK) or stayed pending past its stale time'. The guard does not look at pendingKind or at whether a project already exists, so a stale pending continuation (kind 2 paid by a public keeper) or a stale/failed recover also unlocks a new job.open. Because the contract is the payer of both projects, a second open silently orphans the first hosted site and its result hash lineage; a late onResult for the first order is then refused (lastRequestId moved). Owner-only, so a trust/operational note rather than a bypass. If the author wants the narrower reading, require pendingKind == OPEN (or resultHash == UNSET_RESULT_HASH) for the STUCK/stale branches of open().","line":126,"path":"src/BurnTracker.sol","reproduction":"IDLE -> warp nextUpdateAt -> keeper updateBurntracker(doc,max) -> PENDING kind 2 -> warp staleAt -> owner open(body) succeeds, status() = (3, 1, ...), new lastRequestId; the still-running continuation's callback is later refused (scratch test test_openAllowedOnStaleContinue).","severity":"info","snippet":"        if (state != UNOPENED && state != STUCK && !_isStale()) revert InvalidState();","title":"open() may start a second project from any STUCK or stale PENDING state, including a stale pending update or recover"}],"hash":"a15e62a47faa970ef8856c9208d3008d0cac53e69db35521a736a0624228be61","nodeId":"04ef8230-57f1-4f6c-8ec2-1252dd2a45d0","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds five findings and thirty coverage rows, all snippets verified against the tree, and only that file plus `test/scratch/` were touched.\n\n**Outcome.** The permission model holds: every owner-only entry has `onlyOwner`, callbacks authenticate the original intake, the awaited request id and the PENDING state exactly once, `updateBurntracker` pulls only from its caller with `maxPrice` checked before any token touch, two-step ownership rejects zero and self, and the full 53-test suite passes. No permission bypass or fund-loss path was found in the Access Control, Trust Gap or Asymmetry guides. What I did find:\n\n- **Medium, deployability.** The constructor takes two dynamic `bytes` parameters (16 and 721 bytes). The manifest rules allow one ABI word per entry and at most 16 entries. The real encoding is 35 words, and a nine-word static list fails to deploy in a scratch test. The README asserts the brief wins, but nothing shows the deployer can carry dynamic bytes. The fix is a scope decision: evidence the manifest accepts bytes, or seed the template from the already-compiled `ContinuationTemplate` library.\n- **Low, state trap.** From STUCK after a failed update, an owner `open()` that the plane refuses drops to UNOPENED. There `recover`, `setResultHash` and `updateBurntracker` all revert, so the existing project is orphaned. Spec-literal, with a workaround through `setActions`, so reported for the author to decide.\n- **Low, owner literal.** The tree hardcodes 0x887b... as \"this launch's payer\" though the brief named no address. The manifest must use `$owner`.\n- **Info, trust assumptions.** The owner can redirect an in-flight keeper payment up to `maxPrice` by swapping the intake, and may open a second project from any stale or STUCK state. Both are intended owner powers, documented with reproductions.\n\n**Not covered.** The assembly JSON extractor in `ResultJson.sol` is outside my area and marked unreached beyond the passing differential fuzz.","treeHash":null,"usage":{"cachedInputTokens":1291496,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":33742,"runtime":"claude","turns":33,"wallClockMs":729323}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a0f4c3f900dedd4a","findings":[{"citation":"resolved","description":"The launch boundary, not the arithmetic, is where this deployment fails. BurnTracker/BurnTrackerTest's constructor is (address,address,address,bytes32,bytes32,bytes,bytes,uint32,uint32). The evm_contracts recipe states each constructorArgs entry is one ABI word, supported types are address, uintN, intN, bool, bytes32 and structs of these, 'No dynamic arguments', and at most 16 entries per contract. The two template parts are dynamic bytes: bodyPrefix_ is 16 bytes and bodySuffix_ is 721 bytes (docs/constructor-arguments.json), so the full ABI encoding is 9 head words + (1 length + 1 data) + (1 length + 23 data) = 35 words, and no static-word list of <=16 entries can express it. The README (line 49) acknowledges this and asserts the brief overrides the recipe; that is a scope decision the manifest tool does not take. If the manifest step rejects bytes arguments, the launch parks after the build is accepted with the contract otherwise correct. This is reported as a policy conflict needing a decision, not as a code bug: the brief literally asked for bytes constructor arguments. Behaviour-preserving alternative if the manifest cannot be extended: drop bodyPrefix_/bodySuffix_ from the constructor and seed bodyPrefix/bodySuffix from ContinuationTemplate.prefix()/suffix() (already compiled into the runtime and byte-identical to the documented template, asserted by test_updateExactTemplateProofPaymentAndEvents), keeping owner-settable setBody so the deployed state and every later behaviour are unchanged. Needed evidence otherwise: a manifest/verifier check that accepts a hex string for a bytes constructor parameter.","line":94,"path":"src/BurnTracker.sol","reproduction":"Inputs: the documented constructor arguments (docs/constructor-arguments.json) for BurnTrackerTest. Compute abi.encode(owner, intake, asset, openAction, continueAction, prefix(16 bytes), suffix(721 bytes), 86400, 172800): 35 words of 32 bytes (verified from the compiled ABI: constructor inputs 5 and 6 have type bytes). Expected by the recipe: one static word per constructor parameter, 9 entries, no dynamic type. Actual: two parameters are dynamic bytes whose encoding needs 26 tail words, so the manifest's 'one ABI word per entry, max 16 entries, no dynamic arguments' rule cannot be met and launch.json for this contract cannot be written as specified.","severity":"medium","snippet":"        bytes memory bodyPrefix_,\n        bytes memory bodySuffix_,","title":"Constructor takes two dynamic bytes arguments that the evm_contracts manifest cannot carry (35 ABI words, limit 16 static words)"},{"citation":"resolved","description":"Boundary x invariant seam in the state machine. open() is allowed from STUCK and from a stale PENDING order, both of which are reachable after the project already exists (a status-2 failure or a never-answered update). If that re-open is refused (status 1, e.g. body_invalid), onFailure maps pendingKind OPEN to UNOPENED regardless of whether a project and a stored resultHash already exist. _requireRecoverable() (line 304) admits only IDLE, STUCK or stale PENDING, so from UNOPENED the owner can neither recover(parentJobId) nor setResultHash(hash), and updateBurntracker needs IDLE. The only exit is another paid open(), which creates a brand-new project (0.5 IMD) rather than continuing the one the contract already pays for. Nothing is lost permanently and only the owner can enter this path, hence low. Fix preserving the brief's rules: in onFailure, send a refused OPEN to UNOPENED only when no completion was ever recorded (resultHash == UNSET_RESULT_HASH), otherwise to STUCK; or let _requireRecoverable() also accept UNOPENED when resultHash != UNSET_RESULT_HASH.","line":168,"path":"src/BurnTracker.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {BurnTracker} from \"src/BurnTracker.sol\";\nimport {BurnTrackerTest} from \"src/BurnTrackerTest.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ncontract ProofToken {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract ProofIntake is IIntake {\n    address public immutable treasury;\n    uint256 private nonce;\n    mapping(bytes32 => mapping(address => uint256)) public priceOf;\n\n    constructor(address treasury_) {\n        treasury = treasury_;\n    }\n\n    function setPrice(bytes32 action, address asset, uint256 amount) external {\n        priceOf[action][asset] = amount;\n    }\n\n    function requestWithFailure(bytes32 action, bytes calldata, Callback calldata, bytes4, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 id)\n    {\n        require(amount == priceOf[action][asset] && amount != 0, \"not priced\");\n        require(IERC20(asset).transferFrom(msg.sender, treasury, amount), \"payment failed\");\n        id = keccak256(abi.encode(address(this), ++nonce, msg.sender));\n    }\n}\n\n/// Fails on the current code: after a project exists, a refused re-open (status 1) sends the\n/// contract to UNOPENED, where the owner can neither recover the existing project nor adopt a\n/// completion. Passes once a refused re-open of an existing project leaves a recoverable state.\ncontract RefusedReopenProofTest is Test {\n    BurnTracker tracker;\n    ProofToken token;\n    ProofIntake intake;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    bytes32 constant OPEN_ACTION = bytes32(\"job.open@job-1\");\n    bytes32 constant CONTINUE_ACTION = bytes32(\"job.continue@job-1\");\n    string constant PARENT = \"c9822c12-52e3-4d28-b474-a9ab751b10b3\";\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        token = new ProofToken();\n        intake = new ProofIntake(makeAddr(\"treasury\"));\n        intake.setPrice(OPEN_ACTION, address(token), 0.5 ether);\n        intake.setPrice(CONTINUE_ACTION, address(token), 0.5 ether);\n        tracker = new BurnTrackerTest(\n            owner, address(intake), address(token), OPEN_ACTION, CONTINUE_ACTION, '{\"parentJobId\":\"', '\"}', 86400, 172800\n        );\n        token.mint(owner, 10 ether);\n        token.mint(alice, 10 ether);\n        vm.prank(owner);\n        token.approve(address(tracker), 10 ether);\n        vm.prank(alice);\n        token.approve(address(tracker), 10 ether);\n    }\n\n    function test_refusedReopenOfExistingProjectStaysRecoverable() public {\n        bytes memory proof = bytes(string.concat('{\"jobId\":\"', PARENT, '\"}'));\n        vm.prank(owner);\n        bytes32 first = tracker.open('{\"objective\":\"site\",\"skill\":\"build-website\",\"host\":true}');\n        vm.prank(address(intake));\n        tracker.onResult(first, keccak256(proof), \"ipfs://x/result.json\");\n        vm.warp(tracker.nextUpdateAt());\n        vm.prank(alice);\n        bytes32 upd = tracker.updateBurntracker(proof, 0.5 ether);\n        vm.prank(address(intake));\n        tracker.onFailure(upd, 2, \"failed\", 0, 0, \"\"); // STUCK\n        vm.prank(owner);\n        bytes32 reopen = tracker.open('{\"objective\":\"site\",\"skill\":\"build-website\",\"host\":true}');\n        vm.prank(address(intake));\n        tracker.onFailure(reopen, 1, \"body_invalid\", 0, 0, \"\"); // refused\n        (uint8 state,,,,,,) = tracker.status();\n        assertTrue(state != tracker.UNOPENED(), \"existing project must not become unrecoverable\");\n        // The owner can still continue the project it already paid for.\n        vm.prank(owner);\n        tracker.recover(PARENT);\n    }\n}","reproduction":"State: owner open() -> onResult(hash H) -> IDLE; warp past cooldown; alice updateBurntracker(proof, 0.5e18) -> PENDING kind 2; Intake onFailure(id, 2, ...) -> STUCK. Owner open(body) -> PENDING kind 1 (allowed from STUCK); Intake onFailure(id, 1, 'body_invalid', 0, 0, '') -> state UNOPENED, resultHash still H. Then owner recover('c9822c12-52e3-4d28-b474-a9ab751b10b3') reverts InvalidState, owner setResultHash(H) reverts InvalidState, anyone updateBurntracker(proof, max) reverts InvalidState even after the cooldown. Expected: the owner can still continue or adopt the existing project (as in STUCK). Actual: only another paid open() (a new project) leaves the state; test/scratch/UnopenedTrap.t.sol::test_refusedReopenTrapsExistingProjectInUnopened shows the owner balance at 100e18 - 3*0.5e18 after the forced third open.","severity":"low","snippet":"            if (_progress.pendingKind == OPEN) {\n                newState = UNOPENED;","title":"A refused re-open of an existing project returns to UNOPENED, where recover() and setResultHash() are unreachable"}],"hash":"b4eb4cd463ac1203ef7d9bea2cad50477a1916dad10f3e4d8ae02e243eb5bf81","nodeId":"1ca982bd-59c2-4d80-85f6-c4e2254eccd2","outcome":"completed","summary":"The review is complete. The findings file is at the repository root with 2 findings and 30 coverage rows, all entry points answered.\n\n**Area covered: Math Precision, Boundary, Numerical Gap.** The contract has almost no arithmetic: uint40 timestamps with explicit overflow guards, inclusive 1 hour to 30 day timing bounds, a body-size bound that matches the order bound and the Intake's 16 KiB limit, and an inclusive maxPrice cap checked before any token read. All of those held at their exact boundaries in scratch tests. The real numerical work is the bit-parallel JSON scanner in `ResultJson`. I verified the byte-zero and control-character masks are carry-free, the quote-parity count is exact for up to 32 markers, the tail word is masked, and `firstByte` constants are correct. I then ran 210 Python-generated canonical documents (nested objects, unicode, escapes, `jobId` decoys, missing and duplicate keys) through the production parser, the repository's bytewise reference parser and Python's own expectation. All three agreed. Callback gas measured 35k to 42k of the 200k stipend across every transition, and the 4,545-byte update costs about 267k cold against the real mainnet bytecode replay.\n\n**Findings:**\n\n- **Medium, launch boundary.** The constructor takes two dynamic `bytes` parameters. Their encoding is 35 ABI words, while the evm_contracts manifest carries one static word per entry, at most 16, and no dynamic types. The README claims the brief overrides the recipe, but the manifest tool does not take that decision, so the launch may park after the build. I framed it as a policy conflict needing a decision, with a behaviour-preserving alternative: seed the template from the already-compiled `ContinuationTemplate` and keep `setBody`.\n- **Low, state machine seam.** After a project exists, a failed update leaves STUCK, the owner may re-open, and if the plane refuses that open the contract returns to UNOPENED. From there neither `recover` nor `setResultHash` is reachable, so the only exit is another paid open that creates a new project. A failing Foundry proof is attached under test/scratch.\n\n**Not reached:** nothing within the assigned area. The real Intake's callback encoding was checked only through the mock and the documented interface, since the fork test cannot run offline.","treeHash":null,"usage":{"cachedInputTokens":1848924,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":45658,"runtime":"claude","turns":38,"wallClockMs":730342}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"13603a08ada71730","findings":[],"hash":"d47fe08d2ac6c60a859cf065de4e8e39d1396c3b33332700a9879a5597fef984","nodeId":"5029b9a9-f962-4b6f-a1b7-2425a420652f","outcome":"failed","summary":"needs input: The accepted BurnTracker/BurnTrackerTest constructor takes bodyPrefix_ and bodySuffix_ as dynamic `bytes` arguments (the suffix is 721 bytes of the continuation template). An evm_contracts manifest accepts only one static ABI word (at most 96 characters) per constructor argument, so no valid launch.json can deploy the accepted source with the brief's body template; fixing this needs a source change (e.g. BurnTrackerTest compiling the template in as constants, or a static-only constructor followed by the owner calling setBody after launch), which a manifest-only task cannot make. — May the accepted source be reworked so the constructor takes only static arguments (recommended: BurnTrackerTest supplies the continuation-template prefix/suffix as compiled-in constants), or should the launch deploy with an empty body template that the owner sets via setBody(prefix, suffix) after deployment?","treeHash":null,"usage":{"cachedInputTokens":442490,"inputTokens":26,"model":"claude-opus-5-5","outputTokens":6535,"runtime":"claude","turns":14,"wallClockMs":142557}}],"verification":[{"checks":[{"durationMs":33215,"exitCode":0,"name":"build","output":"Compiling 40 files with Solc 0.8.26\nSolc 0.8.26 finished in 32.99s\nCompiler run successful!\n","passed":true},{"durationMs":383,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MainnetReplay.t.sol:MainnetReplayTest\n[PASS] test_realIntakeAndTokenOfflineReplayAnd4545ByteUpdateGas() (gas: 772405)\nLogs:\n  4545-byte cold update CALL gas, real mainnet code: 266914\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.13ms (962.07µs CPU time)\n\nRan 13 tests for test/Administration.t.sol:AdministrationTest\n[PASS] testFuzz_timingValidation(uint32,uint32) (runs: 256, μ: 38033, ~: 37597)\n[PASS] test_constructorRejectsZeroOwner() (gas: 10247)\n[PASS] test_openAndRecoveryUseTheirActionsLivePrice() (gas: 590753)\n[PASS] test_ownerCanWithdrawDonationsWithoutSubsidizingCallers() (gas: 929950)\n[PASS] test_ownerMayChangeAllFutureOrderInputs() (gas: 2519860)\n[PASS] test_ownerOnlyOperationsAndTwoStepOwnership() (gas: 522235)\n[PASS] test_ownershipNominationCanBeReplaced() (gas: 223522)\n[PASS] test_recoverRejectsParentInjectionAndPaysOwnerWithoutCooldown() (gas: 565807)\n[PASS] test_templateFastPathRequiresBothExactPartsAndCanBeRestored() (gas: 2820828)\n[PASS] test_timingBoundsAndInvalidConfiguration() (gas: 642655)\n[PASS] test_tokenAndIntakeFailuresRollBackEntireOrder() (gas: 1354726)\n[PASS] test_tokenReentryAndSynchronousCallbackCannotCorruptPendingState() (gas: 2599844)\n[PASS] test_zeroPriceDoesNotMoveTokens() (gas: 751515)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 67.35ms (28.41ms CPU time)\n\nRan 10 tests for test/ResultJson.t.sol:ResultJsonTest\n[PASS] testFuzz_authenticatedProofRejectsSingleByteMutation(uint16,uint8) (runs: 256, μ: 544146, ~: 543961)\nLogs:\n  Bound result 0\n  Bound result 25\n\n[PASS] testFuzz_plainAndEscapedStringCannotForgeJobId(uint16,bool) (runs: 256, μ: 49466, ~: 37025)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_uuidBytes(uint128) (runs: 256, μ: 39932, ~: 39932)\n[PASS] test_authenticatedButMalformedDocumentCannotOrder() (gas: 378731)\n[PASS] test_calldataOutsideTheDocumentCannotForgeOrDuplicateAKey() (gas: 32556)\n[PASS] test_escapedQuotesCannotForgeKeysOrHideDuplicates() (gas: 101969)\n[PASS] test_missingDuplicateAndMalformedJobIds() (gas: 221564)\n[PASS] test_real750ByteProofAndOldProofRejection() (gas: 1240199)\n[PASS] test_threeRealCanonicalResultDocumentsAndOnchainHashes() (gas: 1300335)\n[PASS] test_whitespaceNestedValuesAndMalformedStrings() (gas: 106148)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 71.64ms (101.94ms CPU time)\n\nRan 21 tests for test/BurnTracker.t.sol:BurnTrackerBehaviorTest\n[PASS] test_callbacksAuthenticateSenderRequestAndSingleSettlement() (gas: 606981)\n[PASS] test_constructorOnEmptyChainUsesExplicitLaunchOwner() (gas: 4081680)\n[PASS] test_failedCallbackIsConsumedAndNeedsOwnerRecovery() (gas: 411849)\n[PASS] test_failedHostingJobCanBeOpenedAgain() (gas: 509411)\n[PASS] test_failedUpdateAndRecoverGoStuck() (gas: 1446268)\n[PASS] test_failureAlsoConsumesBothCallbackPaths() (gas: 368949)\n[PASS] test_initialConfigurationAndRuntimeEquality() (gas: 204363)\n[PASS] test_livePriceAndMaximumCheckedBeforeTokenInteraction() (gas: 987908)\n[PASS] test_oneOrderAtATimeAndOpenCannotReplaceIdleSite() (gas: 632471)\n[PASS] test_openPaysOwnerAndRegistersContractAsPayer() (gas: 359080)\n[PASS] test_pendingCallbackUsesOriginalIntakeAfterOwnerChangesConfiguration() (gas: 1120807)\n[PASS] test_recoverAfterStaleTimeUsesSnapshotDeadlineAndRefusesLateCallback() (gas: 711087)\n[PASS] test_refusedOpenReturnsUnopenedAndCanOpenAgain() (gas: 521578)\n[PASS] test_refusedRecoverStaysStuckAndOwnerCanRetry() (gas: 826463)\n[PASS] test_refusedUpdateRestartsCooldownPreservesProofAndAllowsRetry() (gas: 1419323)\n[PASS] test_setResultHashAdoptsStaleLostCallbackAndAllowsProof() (gas: 766498)\n[PASS] test_setResultHashAllowedIdleAndStuckButNotUnopened() (gas: 489538)\n[PASS] test_staleCallbackStillAcceptedUntilOwnerSupersedesIt() (gas: 317587)\n[PASS] test_stalePendingCanBeOpenedAgain() (gas: 486222)\n[PASS] test_updateExactTemplateProofPaymentAndEvents() (gas: 892671)\n[PASS] test_updateNeedsIdleCooldownAndExactHash() (gas: 1055871)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 77.52ms (30.54ms CPU time)\n\nWarning: the following cheatcode(s) are deprecated and will be removed in future versions:\n  lastCallGas(): replaced by `lastFrameGas`\nRan 4 tests for test/GasAndStorage.t.sol:GasAndStorageTest\n[PASS] test_callbackAtTimestampZeroStillHasNoEmptyStorageSlot() (gas: 333616)\nLogs:\n  callback execution gas: 42241\n\n[PASS] test_everyFailureTransitionWithin200kCreatesNoSlot() (gas: 2078791)\nLogs:\n  callback execution gas: 35109\n  callback execution gas: 35070\n  callback execution gas: 35373\n  callback execution gas: 35058\n  callback execution gas: 35137\n  callback execution gas: 35070\n\n[PASS] test_firstResultWithin200kWritesOnlyConstructorSeededNonzeroSlots() (gas: 332858)\nLogs:\n  callback execution gas: 42241\n\n[PASS] test_resultAfterRefusalAndRecoveryNeverRecreatesHashSlot() (gas: 1575425)\nLogs:\n  callback execution gas: 42241\n  callback execution gas: 35373\n  callback execution gas: 39441\n  callback execution gas: 39429\n\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 93.92ms (26.01ms CPU time)\n\nRan 4 tests for test/ParserDifferential.t.sol:ParserDifferentialTest\n[PASS] testFuzz_matchesBytewiseParserWithEscapes(bytes,uint8) (runs: 256, μ: 277022, ~: 276411)\n[PASS] testFuzz_matchesBytewiseParserWithoutEscapes(bytes,uint8) (runs: 256, μ: 131740, ~: 124829)\n[PASS] test_allRealDocumentsAgainstBytewiseParser() (gas: 3512832)\n[PASS] test_keyAndUuidAtEveryWordBoundaryWithEscapedQuoteDecoys() (gas: 10111259)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 93.96ms (147.38ms CPU time)\n\nRan 6 test suites in 99.97ms (434.51ms CPU time): 53 tests passed, 0 failed, 0 skipped (53 total tests)\n","passed":true},{"durationMs":84,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BurnTracker.acceptOwnership()\",\"BurnTracker.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"BurnTracker.onResult(bytes32,bytes32,string)\",\"BurnTracker.open(bytes)\",\"BurnTracker.recover(string)\",\"BurnTracker.setActions(bytes32,bytes32)\",\"BurnTracker.setBody(bytes,bytes)\",\"BurnTracker.setIntake(address,address)\",\"BurnTracker.setResultHash(bytes32)\",\"BurnTracker.setTiming(uint32,uint32)\",\"BurnTracker.transferOwnership(address)\",\"BurnTracker.updateBurntracker(bytes,uint256)\",\"BurnTracker.withdraw(address,address,uint256)\",\"BurnTrackerTest.acceptOwnership()\",\"BurnTrackerTest.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"BurnTrackerTest.onResult(bytes32,bytes32,string)\",\"BurnTrackerTest.open(bytes)\",\"BurnTrackerTest.recover(string)\",\"BurnTrackerTest.setActions(bytes32,bytes32)\",\"BurnTrackerTest.setBody(bytes,bytes)\",\"BurnTrackerTest.setIntake(address,address)\",\"BurnTrackerTest.setResultHash(bytes32)\",\"BurnTrackerTest.setTiming(uint32,uint32)\",\"BurnTrackerTest.transferOwnership(address)\",\"BurnTrackerTest.updateBurntracker(bytes,uint256)\",\"BurnTrackerTest.withdraw(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":103,\"docs/constructor-arguments.json\":51,\"docs/continuation-template.json\":1,\"docs/template-check.json\":1,\"docs/verification.md\":52,\"foundry.toml\":26,\"remappings.txt\":3,\"script/LaunchParameters.sol\":28,\"src/BurnTracker.sol\":358,\"src/BurnTrackerTest.sol\":23,\"src/interfaces/IIntake.sol\":21,\"src/libraries/ContinuationTemplate.sol\":31,\"src/libraries/ResultJson.sol\":348,\"test/Administration.t.sol\":270,\"test/BurnTracker.t.sol\":380,\"test/BurnTrackerBase.sol\":118,\"test/GasAndStorage.t.sol\":97,\"test/MainnetReplay.t.sol\":114,\"test/ParserDifferential.t.sol\":100,\"test/ResultJson.t.sol\":181,\"test/fixtures/0x08f07d6ff48974669d2d2fd3de5d26df9ecad4c1eaee7c71b69e8ebafeed13ad.json\":1,\"test/fixtures/0x1bcce3a77e07bf483b5ff819280ce7f6d57091b573c27e8c6d821ffd02f1f32a.json\":1,\"test/fixtures/0x86deaeac963014bc3c80f6eb02c9cc9ae38965617c46ffd5cc171f94d52868cb.json\":1,\"test/fixtures/0x99d8dd2d6dd91b32209df9414a73878865d8638cd34258ad9442c10fe549c8f5.json\":1,\"test/fixtures/0xb7d61120819d173efef95fd671010c3a2db6a1e46fbc3e80be9103c72303ea2f.json\":1,\"test/fixtures/continuation-template.json\":1,\"test/fixtures/mainnet-state.json\":91,\"test/fixtures/provenance.json\":62,\"test/fork/Mainnet.t.sol\":83,\"test/helpers/ReferenceJson.sol\":165,\"test/mocks/MockIntake.sol\":87,\"test/mocks/MockToken.sol\":60},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":6673,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/BurnTracker.sol:248: Reentrancy in BurnTracker._order(uint8,bytes32,bytes,string,uint256) (src/BurnTracker.sol#248-286):\n[high/medium] reentrancy-balance at src/BurnTracker.sol:248: Reentrancy in BurnTracker._order(uint8,bytes32,bytes,string,uint256) (src/BurnTracker.sol#248-286):\n[high/medium] reentrancy-balance at src/BurnTracker.sol:248: Reentrancy in BurnTracker._order(uint8,bytes32,bytes,string,uint256) (src/BurnTracker.sol#248-286):\n[high/medium] reentrancy-balance at src/BurnTracker.sol:248: Reentrancy in BurnTracker._order(uint8,bytes32,bytes,string,uint256) (src/BurnTracker.sol#248-286):\n[medium/high] incorrect-equality at src/BurnTracker.sol:160: BurnTracker.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes) (src/BurnTracker.sol#160-178) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/BurnTracker.sol:307: BurnTracker._isStale() (src/BurnTracker.sol#307-309) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/BurnTracker.sol:160: BurnTracker.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes) (src/BurnTracker.sol#160-178) uses a dangerous strict equality:\n[low/medium] reentrancy-benign at src/BurnTracker.sol:248: Reentrancy in BurnTracker._order(uint8,bytes32,bytes,string,uint256) (src/BurnTracker.sol#248-286):\n[low/medium] timestamp at src/BurnTracker.sol:311: BurnTracker._now40() (src/BurnTracker.sol#311-314) uses timestamp for comparisons\n[low/medium] timestamp at src/BurnTracker.sol:160: BurnTracker.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes) (src/BurnTracker.sol#160-178) uses timestamp for comparisons\n[low/medium] timestamp at src/BurnTracker.sol:303: BurnTracker._requireRecoverable() (src/BurnTracker.sol#303-305) uses timestamp for comparisons\n[low/medium] timestamp at src/BurnTracker.sol:297: BurnTracker._requireCallback(bytes32) (src/BurnTracker.sol#297-301) uses timestamp for comparisons\n[low/medium] timestamp at src/BurnTracker.sol:124: BurnTracker.open(bytes) (src/BurnTracker.sol#124-128) uses timestamp for comparisons\n[low/medium] timestamp at src/BurnTracker.sol:248: BurnTracker._order(uint8,bytes32,bytes,string,uint256) (src/BurnTracker.sol#248-286) uses timestamp for comparisons\n[low/medium] timestamp at src/BurnTracker.sol:307: BurnTracker._isStale() (src/BurnTracker.sol#307-309) uses timestamp for comparisons\n[low/medium] timestamp at src/BurnTracker.sol:131: BurnTracker.updateBurntracker(bytes,uint256) (src/BurnTracker.sol#131-137) uses timestamp for comparisons","passed":true},{"durationMs":818,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/BurnTracker.sol:131: ETH transferred without address checks\n[low] centralization-risk at src/BurnTracker.sol:124: Centralization Risk (9 places)\n[low] internal-function-used-once at src/libraries/ContinuationTemplate.sol:8: Internal Function Used Only Once (2 places)\n[low] literal-instead-of-constant at src/libraries/ResultJson.sol:153: Literal Instead of Constant (4 places)\n[low] non-reentrant-not-first at src/BurnTracker.sol:124: `nonReentrant` is Not the First Modifier (9 places)\n[low] require-revert-in-loop at src/libraries/ResultJson.sol:339: Loop Contains `require`/`revert`\n[low] uninitialized-local-variable at src/libraries/ResultJson.sol:339: Uninitialized Local Variable\n[low] unused-error at src/libraries/ResultJson.sol:9: Unused Error (3 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"5997c12992c7cb7c01eeeba896bb37c84daeee5d1ee62343d876f5d3e71d86bd","verifiedTreeHash":"dc20011b9383ca426d7f3941e4d725fd56a49b30","verifierVersion":"0.1.0+ad64f8c4"},{"checks":[{"durationMs":48482,"exitCode":0,"name":"build","output":"Compiling 43 files with Solc 0.8.26\nSolc 0.8.26 finished in 48.37s\nCompiler run successful!\n","passed":true},{"durationMs":9626,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/MainnetReplay.t.sol:MainnetReplayTest\n[PASS] test_realIntakeAndTokenOfflineReplayAnd4545ByteUpdateGas() (gas: 772405)\nLogs:\n  4545-byte cold update CALL gas, real mainnet code: 266914\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.82ms (1.10ms CPU time)\n\nRan 4 tests for test/GasAndStorage.t.sol:GasAndStorageTest\n[PASS] test_callbackAtTimestampZeroStillHasNoEmptyStorageSlot() (gas: 333616)\nLogs:\n  callback execution gas: 42241\n\n[PASS] test_everyFailureTransitionWithin200kCreatesNoSlot() (gas: 2078791)\nLogs:\n  callback execution gas: 35109\n  callback execution gas: 35070\n  callback execution gas: 35373\n  callback execution gas: 35058\n  callback execution gas: 35137\n  callback execution gas: 35070\n\n[PASS] test_firstResultWithin200kWritesOnlyConstructorSeededNonzeroSlots() (gas: 332858)\nLogs:\n  callback execution gas: 42241\n\n[PASS] test_resultAfterRefusalAndRecoveryNeverRecreatesHashSlot() (gas: 1575425)\nLogs:\n  callback execution gas: 42241\n  callback execution gas: 35373\n  callback execution gas: 39441\n  callback execution gas: 39429\n\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 36.69ms (9.95ms CPU time)\nWarning: the following cheatcode(s) are deprecated and will be removed in future versions:\n  lastCallGas(): replaced by `lastFrameGas`\n\nRan 10 tests for test/ResultJson.t.sol:ResultJsonTest\n[PASS] testFuzz_authenticatedProofRejectsSingleByteMutation(uint16,uint8) (runs: 256, μ: 544141, ~: 543961)\nLogs:\n  Bound result 0\n  Bound result 25\n\n[PASS] testFuzz_plainAndEscapedStringCannotForgeJobId(uint16,bool) (runs: 256, μ: 48965, ~: 36395)\nLogs:\n  Bound result 2\n\n[PASS] testFuzz_uuidBytes(uint128) (runs: 256, μ: 39932, ~: 39932)\n[PASS] test_authenticatedButMalformedDocumentCannotOrder() (gas: 378731)\n[PASS] test_calldataOutsideTheDocumentCannotForgeOrDuplicateAKey() (gas: 32556)\n[PASS] test_escapedQuotesCannotForgeKeysOrHideDuplicates() (gas: 101969)\n[PASS] test_missingDuplicateAndMalformedJobIds() (gas: 221564)\n[PASS] test_real750ByteProofAndOldProofRejection() (gas: 1240199)\n[PASS] test_threeRealCanonicalResultDocumentsAndOnchainHashes() (gas: 1300335)\n[PASS] test_whitespaceNestedValuesAndMalformedStrings() (gas: 106148)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 40.33ms (94.90ms CPU time)\n\nRan 21 tests for test/BurnTracker.t.sol:BurnTrackerBehaviorTest\n[PASS] test_callbacksAuthenticateSenderRequestAndSingleSettlement() (gas: 606981)\n[PASS] test_constructorOnEmptyChainUsesExplicitLaunchOwner() (gas: 4081680)\n[PASS] test_failedCallbackIsConsumedAndNeedsOwnerRecovery() (gas: 411849)\n[PASS] test_failedHostingJobCanBeOpenedAgain() (gas: 509411)\n[PASS] test_failedUpdateAndRecoverGoStuck() (gas: 1446268)\n[PASS] test_failureAlsoConsumesBothCallbackPaths() (gas: 368949)\n[PASS] test_initialConfigurationAndRuntimeEquality() (gas: 204363)\n[PASS] test_livePriceAndMaximumCheckedBeforeTokenInteraction() (gas: 987908)\n[PASS] test_oneOrderAtATimeAndOpenCannotReplaceIdleSite() (gas: 632471)\n[PASS] test_openPaysOwnerAndRegistersContractAsPayer() (gas: 359080)\n[PASS] test_pendingCallbackUsesOriginalIntakeAfterOwnerChangesConfiguration() (gas: 1120807)\n[PASS] test_recoverAfterStaleTimeUsesSnapshotDeadlineAndRefusesLateCallback() (gas: 711087)\n[PASS] test_refusedOpenReturnsUnopenedAndCanOpenAgain() (gas: 521578)\n[PASS] test_refusedRecoverStaysStuckAndOwnerCanRetry() (gas: 826463)\n[PASS] test_refusedUpdateRestartsCooldownPreservesProofAndAllowsRetry() (gas: 1419323)\n[PASS] test_setResultHashAdoptsStaleLostCallbackAndAllowsProof() (gas: 766498)\n[PASS] test_setResultHashAllowedIdleAndStuckButNotUnopened() (gas: 489538)\n[PASS] test_staleCallbackStillAcceptedUntilOwnerSupersedesIt() (gas: 317587)\n[PASS] test_stalePendingCanBeOpenedAgain() (gas: 486222)\n[PASS] test_updateExactTemplateProofPaymentAndEvents() (gas: 892671)\n[PASS] test_updateNeedsIdleCooldownAndExactHash() (gas: 1055871)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 48.80ms (16.35ms CPU time)\n\nRan 13 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_updateNeverChargesAboveMaxOrBeforeCooldown(uint256,uint256,uint256) (runs: 512, μ: 673292, ~: 617905)\nLogs:\n  Bound result 210573\n  Bound result 216\n  Bound result 8475\n\n[PASS] test_adoptionCancelsTheAwaitedCallback() (gas: 372754)\n[PASS] test_cooldownBoundaryIsExact() (gas: 791824)\n[PASS] test_failureCallbackIgnoresPanelFieldsAndSignature() (gas: 317042)\n[PASS] test_maxPriceOneWeiBelowRevertsAtAndAboveSucceeds() (gas: 1124100)\n[PASS] test_nonOwnerCannotUseOwnerPathsInAnyEligibleState() (gas: 622649)\n[PASS] test_oldOwnerLosesPowersAfterAcceptance() (gas: 447140)\n[PASS] test_priceRaisedAfterQuoteIsCaughtByMaxPrice() (gas: 924903)\n[PASS] test_recoverRejectsEveryMalformedIdShape() (gas: 560093)\n[PASS] test_refusedUpdateRestartsCooldownFromTheRefusal() (gas: 1268595)\n[PASS] test_staleDeadlineIsExactForOpenRecoverAndAdoption() (gas: 577430)\n[PASS] test_supersededOrdersCallbacksAreRefusedInBothDirections() (gas: 570923)\n[PASS] test_timingChangeDoesNotMoveAPlacedDeadlineButMovesCooldown() (gas: 419243)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 62.22ms (53.78ms CPU time)\n\nRan 13 tests for test/Administration.t.sol:AdministrationTest\n[PASS] testFuzz_timingValidation(uint32,uint32) (runs: 256, μ: 38018, ~: 37597)\n[PASS] test_constructorRejectsZeroOwner() (gas: 10247)\n[PASS] test_openAndRecoveryUseTheirActionsLivePrice() (gas: 590753)\n[PASS] test_ownerCanWithdrawDonationsWithoutSubsidizingCallers() (gas: 929950)\n[PASS] test_ownerMayChangeAllFutureOrderInputs() (gas: 2519860)\n[PASS] test_ownerOnlyOperationsAndTwoStepOwnership() (gas: 522235)\n[PASS] test_ownershipNominationCanBeReplaced() (gas: 223522)\n[PASS] test_recoverRejectsParentInjectionAndPaysOwnerWithoutCooldown() (gas: 565807)\n[PASS] test_templateFastPathRequiresBothExactPartsAndCanBeRestored() (gas: 2820828)\n[PASS] test_timingBoundsAndInvalidConfiguration() (gas: 642655)\n[PASS] test_tokenAndIntakeFailuresRollBackEntireOrder() (gas: 1354726)\n[PASS] test_tokenReentryAndSynchronousCallbackCannotCorruptPendingState() (gas: 2599844)\n[PASS] test_zeroPriceDoesNotMoveTokens() (gas: 751515)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 62.32ms (38.96ms CPU time)\n\nRan 4 tests for test/ParserDifferential.t.sol:ParserDifferentialTest\n[PASS] testFuzz_matchesBytewiseParserWithEscapes(bytes,uint8) (runs: 256, μ: 279067, ~: 284685)\n[PASS] testFuzz_matchesBytewiseParserWithoutEscapes(bytes,uint8) (runs: 256, μ: 133790, ~: 126293)\n[PASS] test_allRealDocumentsAgainstBytewiseParser() (gas: 3512832)\n[PASS] test_keyAndUuidAtEveryWordBoundaryWithEscapedQuoteDecoys() (gas: 10111259)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 62.33ms (162.34ms CPU time)\n\nRan 2 tests for test/invariant/BurnTrackerInvariant.t.sol:BurnTrackerInvariantTest\n[PASS]\nBurnTrackerInvariantTest invariants:\n[PASS] invariant_callbackSlotsNeverZero\n[PASS] invariant_everyCallMatchesTheBriefModel\n[PASS] invariant_holdsOnlyDonations\n[PASS] invariant_noAllowanceLeftToIntake\n[PASS] invariant_ownerUnchanged\n[PASS] invariant_paymentsConserved\n[PASS] invariant_stateAndKindAreConsistent\n[PASS] invariant_statusMatchesModel\n BurnTrackerInvariantTest invariants (runs: 256, calls: 32768, reverts: 0)\n\n╭--------------------+-----------------+-------+---------+----------╮\n| Contract           | Selector        | Calls | Reverts | Discards |\n+===================================================================+\n| BurnTrackerHandler | completeFailure | 2569  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | completeResult  | 2602  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | donate          | 2492  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | open            | 2506  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | recover         | 2408  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | setBody         | 2515  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | setPrice        | 2595  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | setResultHash   | 2570  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | setTiming       | 2431  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | strayCallback   | 2492  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | update          | 2472  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | warp            | 2520  | 0       | 0        |\n|--------------------+-----------------+-------+---------+----------|\n| BurnTrackerHandler | withdraw        | 2596  | 0       | 0        |\n╰--------------------+-----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 0\n  Bound result 16\n  Bound result 3664\n  Bound result 4\n  Bound result 0\n  Bound result 1\n  Bound result 786695272171200957\n  Bound result 130326\n  Bound result 169258\n  Bound result 658346\n  Bound result 5668202340902886167\n  Bound result 89242\n  Bound result 21496\n  Bound result 39833\n  Bound result 854376\n  Bound result 296157\n  Bound result 875515433249818981\n  Bound result 1099511627775\n  Bound result 127\n  Bound result 5388136911029464628\n  Bound result 2360211432531643893\n  Bound result 34402\n  Bound result 1731358588717165394\n  Bound result 18245\n  Bound result 2588407\n  Bound result 13\n  Bound result 364356615988489171\n  Bound result 34\n  Bound result 4438467478351409819\n  Bound result 220001\n  Bound result 105752\n  Bound result 9291\n  Bound result 19821\n  Bound result 66872\n  Bound result 303511\n  Bound result 2\n  Bound result 85357\n  Bound result 6685\n  Bound result 6627\n  Bound result 15436\n  Bound result 5932\n  Bound result 5375\n  Bound result 16385\n  Bound result 1312664532011200398\n  Bound result 2268569626446668403\n\n[PASS] test_handlerSequenceReachesEveryStateWithoutMismatch() (gas: 5448593)\nLogs:\n  Bound result 86400\n  Bound result 86400\n  Bound result 172800\n  Bound result 86400\n  Bound result 86400\n  Bound result 3000000000000000000\n  Bound result 1000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 9.43s (9.44s CPU time)\n\nRan 8 test suites in 9.44s (9.75s CPU time): 68 tests passed, 0 failed, 0 skipped (68 total tests)\n","passed":true},{"durationMs":87,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"BurnTracker.acceptOwnership()\",\"BurnTracker.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"BurnTracker.onResult(bytes32,bytes32,string)\",\"BurnTracker.open(bytes)\",\"BurnTracker.recover(string)\",\"BurnTracker.setActions(bytes32,bytes32)\",\"BurnTracker.setBody(bytes,bytes)\",\"BurnTracker.setIntake(address,address)\",\"BurnTracker.setResultHash(bytes32)\",\"BurnTracker.setTiming(uint32,uint32)\",\"BurnTracker.transferOwnership(address)\",\"BurnTracker.updateBurntracker(bytes,uint256)\",\"BurnTracker.withdraw(address,address,uint256)\",\"BurnTrackerTest.acceptOwnership()\",\"BurnTrackerTest.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"BurnTrackerTest.onResult(bytes32,bytes32,string)\",\"BurnTrackerTest.open(bytes)\",\"BurnTrackerTest.recover(string)\",\"BurnTrackerTest.setActions(bytes32,bytes32)\",\"BurnTrackerTest.setBody(bytes,bytes)\",\"BurnTrackerTest.setIntake(address,address)\",\"BurnTrackerTest.setResultHash(bytes32)\",\"BurnTrackerTest.setTiming(uint32,uint32)\",\"BurnTrackerTest.transferOwnership(address)\",\"BurnTrackerTest.updateBurntracker(bytes,uint256)\",\"BurnTrackerTest.withdraw(address,address,uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":103,\"docs/constructor-arguments.json\":51,\"docs/continuation-template.json\":1,\"docs/template-check.json\":1,\"docs/verification.md\":52,\"foundry.toml\":26,\"remappings.txt\":3,\"script/LaunchParameters.sol\":28,\"src/BurnTracker.sol\":358,\"src/BurnTrackerTest.sol\":23,\"src/interfaces/IIntake.sol\":21,\"src/libraries/ContinuationTemplate.sol\":31,\"src/libraries/ResultJson.sol\":348,\"test/Administration.t.sol\":270,\"test/BurnTracker.t.sol\":380,\"test/BurnTrackerBase.sol\":118,\"test/EdgeCases.t.sol\":228,\"test/GasAndStorage.t.sol\":97,\"test/MainnetReplay.t.sol\":114,\"test/ParserDifferential.t.sol\":100,\"test/ResultJson.t.sol\":181,\"test/fixtures/0x08f07d6ff48974669d2d2fd3de5d26df9ecad4c1eaee7c71b69e8ebafeed13ad.json\":1,\"test/fixtures/0x1bcce3a77e07bf483b5ff819280ce7f6d57091b573c27e8c6d821ffd02f1f32a.json\":1,\"test/fixtures/0x86deaeac963014bc3c80f6eb02c9cc9ae38965617c46ffd5cc171f94d52868cb.json\":1,\"test/fixtures/0x99d8dd2d6dd91b32209df9414a73878865d8638cd34258ad9442c10fe549c8f5.json\":1,\"test/fixtures/0xb7d61120819d173efef95fd671010c3a2db6a1e46fbc3e80be9103c72303ea2f.json\":1,\"test/fixtures/continuation-template.json\":1,\"test/fixtures/mainnet-state.json\":91,\"test/fixtures/provenance.json\":62,\"test/fork/Mainnet.t.sol\":83,\"test/helpers/ReferenceJson.sol\":165,\"test/invariant/BurnTrackerHandler.sol\":454,\"test/invariant/BurnTrackerInvariant.t.sol\":167,\"test/mocks/MockIntake.sol\":87,\"test/mocks/MockToken.sol\":60},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"5f7c22a8823b1cae77e18f92a2f2284a3d20705fc22f75a50bd7a23891163ef9","verifiedTreeHash":"a7b6842eaf5fc8cd07e2a1f226311e3e2aa5d295","verifierVersion":"0.1.0+ad64f8c4"}]}