{"assessments":[],"deployments":[{"attestationHash":"7e4df28ef05ad4451bf703fae1464ffb75dccfccf848864819c8e84f8386d955","chainId":11155111,"contracts":[{"address":"0x8af1dd11f24eab3884520607e74176fa9fc9a0db","blockNumber":11920125,"name":"HandleVault","role":"other","txHash":"0x49e99331b883a1313607efc638c5ce6938d4dca1b5018a9a43b3df35af6bb893"},{"address":"0x4ceca141d91e2563bd40da1a3317d078ca14c256","blockNumber":11920125,"name":"MockUSDC","role":"other","txHash":"0x49e99331b883a1313607efc638c5ce6938d4dca1b5018a9a43b3df35af6bb893"}],"id":"5b0b0b40-3d56-4112-a2be-4682fde93290","manifestHash":"6a770f91cb2359992f407e6ebe7165a24afbba5570099ca5691e5a0a7064c85d","status":"live"}],"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":"c0d37569-b47b-4de9-bfed-718a01bba796","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"b2d44c387e646567547fe1e5a877b88e1afb317bde407750be4c4643ba8d43f1","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"905af7aa517121e13c0d08b914b783b670d22b6918386d5581db3fa9a99273a1","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"66f738ade77f1b4cbdc189f148fe75f5f3bfd23492e80d8ac21d2788f98619e3","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"86094598164f431d86336c3438f56580e73df6d7391213c2f2be178ef646ee9f","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"33fbd23fc99cc3f03dda8606368a030ca85f69a810e3596ba62ae87eb5849af9","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"0924085c01816bf398bdb0332271d8afad5fe0967f42bcb4b26b9cc797db02ff","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"1c889dee9a113bada9ec077e9323e3a244661a7136f3629eaae9a23d58a43b60","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"f435ce5e1619011e57945f2c66a9b8d38fa33611a4cc721b92b2b3632ac89802","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build HandleVault, a non-upgradeable multi-token escrow for sending funds to people identified only by an opaque bytes32 recipientId (a salted hash computed off-chain; the contract never sees social data). Deploy to Sepolia with the owner wallet as admin and authorizer, plus a MockUSDC (6 decimals, mintable) for tests. Solidity 0.8.x, OpenZeppelin, Foundry.\n\nRoles: DEFAULT_ADMIN_ROLE (a Safe in production), AUTHORIZER_ROLE (off-chain signer that attests a claimant controls the handle; rotatable), PAUSER_ROLE. Two-step admin hand-over. No role can move escrowed funds except through a valid claim or a refund to the original sender.\n\nDeposits: depositFor(recipientId, token, amount) and depositNativeFor(recipientId) record one Deposit{sender, recipientId, token, amount, createdAt, status} per call (per-deposit records, not only an aggregate), enforce an admin-managed token allowlist (initially MockUSDC and native) with a per-token minimum, and emit Deposited(depositId, recipientId, token, amount, sender). Views: total escrowed per (recipientId, token) and the open deposit ids for a recipient (paginated).\n\nClaims: claim(recipientId, dest, tokens[], nonce, deadline, signature) verifies an EIP-712 Claim(bytes32 recipientId,address dest,uint256 nonce,uint256 deadline) signed by an AUTHORIZER_ROLE key, rejects used nonces and expired deadlines, then pays every open deposit for that recipient in the listed tokens to dest, marking them claimed. Cap the number of deposits swept per call and let the caller continue in a later call with a fresh signature, so a spammed recipient can never be made unclaimable. Anyone may relay a claim (gas sponsored off-chain). Emit Claimed(depositId, …) per deposit.\n\nRefunds: a deposit unclaimed for REFUND_DELAY (immutable, 180 days) can be refunded to its original sender by anyone calling refund(depositId); refunds are not blocked by pause. Emit Refunded.\n\nSafety: ReentrancyGuard, checks-effects-interactions, native transfers via call with success check, no transfer-hook tokens assumed (use balance deltas on deposit for fee-on-transfer safety or reject such tokens, your call, documented). Per-token rolling 24-hour outflow cap on claims, admin-settable, initial values in the deploy script, so a compromised authorizer is bounded. feeBps (initial 0, maximum 100) and feeRecipient settable by admin; the fee is taken from each claimed amount, never from refunds. Pausable: pause stops deposits and claims, never refunds. State the trust assumptions (authorizer signs after off-chain verification; admin can raise caps and fee; pause is one-way for claimants until unpaused) in NatSpec and README.\n\nOut of scope, leave hooks only: claimTo with a bridge adapter, ERC-4337 paymaster, multichain same-address deployment.\n\nTests: every path; EIP-712 digest matches a reference implementation; signature by non-authorizer rejected; replay, wrong recipient, wrong dest, expired deadline rejected; multi-token claim; native path; partial sweep continuation; refund before and after the delay; refund of an already-claimed deposit rejected; cap enforcement and window roll-over; fee math at 0 and at max; allowlist and minimums; pause semantics; fuzz on amounts; an invariant that the contract’s balance per token always equals open deposits plus accrued fees. Deliver source, tests, deploy script, README with the Sepolia addresses and the exact EIP-712 domain and typehash the off-chain signer must use.","parentJobId":null,"planHash":"648ef43b93ec29ef1ea4b4e64e001f864f55910e36ce7615254df4489629e88d","previousHash":"3736def2ad686ffff2f4989f9f26ae96d291bc04cc9bedf232731474281a89f7","projectId":"c0d37569-b47b-4de9-bfed-718a01bba796","publication":{"commit":"53ab5a73a4db26f89e6c242809cd78659007f0bf","deliveredAt":"2026-10-15T11:24:57.882Z","repoUrl":"https://github.com/identity-md-launches/launch-1833-build-handlevault-non-upgradeable-multi"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":null,"snapshotHash":"d33ee50076f52513a86f801cc5807ab1e2466c43072ac45b847d56b49e61f3f7","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"323aec17bfc8d652","findings":[{"citation":"resolved","description":"Asymmetry between the deposit-side and claim-side cap checks. _checkDeposit (line 384: `if (amount > outflowCaps[token]) revert DepositExceedsCap(outflowCaps[token]);`) accepts any deposit up to the FULL 24h cap, but _sweep compares the FIFO head against remainingOutflow(token), the cap minus gross claims by ALL recipients in the last 24 hours, and on a miss it `break`s, so no deposit behind that head is ever examined for that (recipientId, token). A deposit equal to the cap therefore fits only in a window with zero protocol-wide claims of that token. Any unprivileged sender can deposit cap-sized (or merely larger than the typical remaining capacity on a busy vault) to a victim's recipientId; every genuine deposit queued after it is unclaimable with any valid authorizer signature as long as anyone claims that token once per 24 hours (on a busy vault bystanders do this for free; otherwise the attacker keeps the window non-empty with a 1 mUSDC zero-fee self-claim per day, see finding 2). The attacker's capital is fully recoverable: refund(blockerId) after REFUND_DELAY, then re-deposit to block again. This violates the brief's requirement that 'a spammed recipient can never be made unclaimable' and the README only documents the admin-lowers-cap variant of a blocked head. Recipient never receives funds sent to them; senders can only recover after 180 days. Four specialists reported this independently (permissions high, flow medium, math medium, economics medium); the test writer noted the FIFO stop as info but assumed a head always fits once the window rolls over, which is only true if the window is completely empty. Minimal fix preserving FIFO and the cap: in _sweep, skip a deposit that does not fit (advance to _links[id].tokenNext) instead of breaking, keeping `blocked` so OutflowCapExceeded still fires when nothing fits; and/or bound single deposits to a fraction of the cap so one deposit can always fit a partly used window.","line":460,"path":"src/HandleVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HandleVault} from \"src/HandleVault.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\n\n/// @notice An unprivileged depositor parks a cap-sized deposit at the head of a victim's FIFO\n/// token queue. depositFor accepts it (amount <= cap), but _sweep stops the whole queue at a head\n/// that does not fit remainingOutflow(token), so every later deposit for that recipient is\n/// unclaimable whenever any claim of that token happened in the last 24 hours.\n/// Fails on the current code with OutflowCapExceeded(); passes once _sweep skips a deposit that\n/// does not fit (or deposits are bounded so a single one can always fit a partly used window).\ncontract HeadOfLineBlockProof is Test {\n    uint256 internal constant OWNER_KEY = 0xA11CE;\n    bytes32 internal constant VICTIM = keccak256(\"victim handle\");\n    bytes32 internal constant OTHER_USER = keccak256(\"some other user\");\n\n    address internal owner;\n    address internal attacker;\n    address internal friend;\n    address internal dest;\n    MockUSDC internal usdc;\n    HandleVault internal vault;\n\n    function setUp() public {\n        vm.chainId(11_155_111);\n        vm.warp(10 days);\n        owner = vm.addr(OWNER_KEY);\n        attacker = makeAddr(\"attacker\");\n        friend = makeAddr(\"friend\");\n        dest = makeAddr(\"victim destination\");\n        usdc = new MockUSDC(owner);\n        // Deploy-script values: 1 mUSDC minimum, 100_000 mUSDC rolling 24h cap, fee 0.\n        vault = new HandleVault(owner, address(usdc), 0.0001 ether, 1e6, 10 ether, 100_000e6);\n        vm.startPrank(owner);\n        usdc.mint(attacker, 100_000e6);\n        usdc.mint(friend, 1_000e6);\n        vm.stopPrank();\n    }\n\n    function _sign(bytes32 recipientId, address destination, uint256 nonce, uint256 deadline)\n        internal\n        view\n        returns (bytes memory)\n    {\n        (uint8 v, bytes32 r, bytes32 s) =\n            vm.sign(OWNER_KEY, vault.claimDigest(recipientId, destination, nonce, deadline));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _deposit(address from, bytes32 recipientId, uint256 amount) internal returns (uint256 id) {\n        vm.startPrank(from);\n        usdc.approve(address(vault), amount);\n        id = vault.depositFor(recipientId, address(usdc), amount);\n        vm.stopPrank();\n    }\n\n    function test_capSizedHeadDepositBlocksEveryLaterDepositForRecipient() public {\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(usdc);\n\n        // 1. Attacker deposits exactly the cap to the victim's handle. Accepted: amount <= cap.\n        uint256 blocker = _deposit(attacker, VICTIM, 100_000e6);\n\n        // 2. A friend later sends the victim 50 mUSDC. This is the deposit the victim wants.\n        uint256 friendDeposit = _deposit(friend, VICTIM, 50e6);\n\n        // 3. Ordinary traffic: any other recipient claims anything in the same 24h window.\n        _deposit(friend, OTHER_USER, 1e6);\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        address otherDest = makeAddr(\"other dest\");\n        vault.claim(OTHER_USER, otherDest, tokens, 0, deadline, _sign(OTHER_USER, otherDest, 0, deadline));\n        assertEq(vault.remainingOutflow(address(usdc)), 100_000e6 - 1e6);\n\n        // 4. Victim relays a valid authorizer signature. Expected: the 50 mUSDC deposit, which fits\n        //    the remaining cap, is paid and the oversized head stays open.\n        //    Actual on current code: revert OutflowCapExceeded(), nothing paid.\n        uint256 count = vault.claim(VICTIM, dest, tokens, 0, deadline, _sign(VICTIM, dest, 0, deadline));\n        assertEq(count, 1, \"claim should sweep the deposit that fits the cap\");\n        assertEq(usdc.balanceOf(dest), 50e6, \"victim should receive the friend's deposit\");\n        (,,,,, HandleVault.Status friendStatus) = vault.deposits(friendDeposit);\n        assertEq(uint8(friendStatus), uint8(HandleVault.Status.Claimed));\n        (,,,,, HandleVault.Status blockerStatus) = vault.deposits(blocker);\n        assertEq(uint8(blockerStatus), uint8(HandleVault.Status.Open));\n        assertEq(usdc.balanceOf(address(vault)), vault.totalOpen(address(usdc)) + vault.accruedFees(address(usdc)));\n\n        // 5. The attacker's capital is fully recoverable after the refund delay.\n        vm.warp(vm.getBlockTimestamp() + 180 days);\n        vault.refund(blocker);\n        assertEq(usdc.balanceOf(attacker), 100_000e6);\n    }\n}","reproduction":"Deploy with the script values (mUSDC minimum 1e6, cap 100_000e6, feeBps 0). 1) attacker: usdc.approve + depositFor(VICTIM, usdc, 100_000e6) -> accepted (amount == cap), id 1. 2) friend: depositFor(VICTIM, usdc, 50e6) -> id 2. 3) friend: depositFor(OTHER, usdc, 1e6); claim(OTHER, otherDest, [usdc], 0, deadline, authorizerSig) -> remainingOutflow(usdc) == 99_999e6. 4) claim(VICTIM, dest, [usdc], 0, deadline, validAuthorizerSig). Expected: count == 1, dest receives 50e6, deposit 1 stays Open. Actual: revert OutflowCapExceeded(); nothing is paid, and this stays true for every later deposit to VICTIM as long as any mUSDC claim occurs in each 24h window. 5) warp +180 days: refund(1) returns 100_000e6 to the attacker. Run: forge test --match-path test/scratch/HeadOfLineBlock.t.sol (fails on current code with OutflowCapExceeded()).","severity":"high","snippet":"            if (d.amount > accounting.available - accounting.gross) {\n                blocked = true;\n                break;\n            }","title":"Cap-sized head-of-line deposit lets any depositor make a recipient's whole token queue unclaimable (merged: permissions, flow, math, economics #2, tests #2)"},{"citation":"resolved","description":"The rolling outflow cap is keyed only by token; remainingOutflow(token) is one budget shared by every recipientId, and a claim to the depositor's own wallet is indistinguishable from any other claim. With the deployed feeBps = 0, a user who legitimately controls any handle (the authorizer attests handle control, nothing more) deposits outflowCaps[token] to their own recipientId and immediately claims it back to themselves: their balance is unchanged, remainingOutflow(token) == 0, and every other recipient's claim in that token reverts OutflowCapExceeded for the next 24 hours. Repeating at each roll-over costs only gas (at the maximum 1% fee it costs 1% of the cap per day). The cap was specified to bound a compromised authorizer; as built it is also an unprivileged denial-of-service lever and the amplifier that makes finding 1 permanent without relying on bystander traffic. Refunds remain available to senders after 180 days, so principal is not lost, but the escrow stops paying anyone in that token. Off-chain the operator can only stop signing for the attacker's handle after detection; on-chain the admin cannot distinguish the traffic. Fix options that keep the brief's global cap: add a second per-(recipientId, token) or per-dest rolling cap (a share of the global cap) so one claimant cannot consume the whole window; or document the shared-budget tradeoff and require a non-zero minimum fee so each exhaustion has a cost.","line":456,"path":"src/HandleVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HandleVault} from \"src/HandleVault.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\n\n/// @notice The per-token rolling cap is one budget shared by every recipient. With feeBps = 0 an\n/// unprivileged user who controls any handle deposits the cap to their own handle and claims it\n/// straight back, ending with the same balance and remainingOutflow(token) == 0. Every other\n/// recipient's claim in that token then reverts for 24 hours, and the cycle repeats at each\n/// roll-over. Fails on the current code with OutflowCapExceeded(); passes once a claimant cannot\n/// consume the whole global window alone (per-recipient share or second per-recipient cap).\ncontract SharedCapExhaustionProof is Test {\n    uint256 internal constant AUTHORIZER_KEY = 0xA11CE;\n    uint256 internal constant CAP = 100_000e6;\n    bytes32 internal constant VICTIM = keccak256(\"victim handle\");\n    bytes32 internal constant ATTACKER_HANDLE = keccak256(\"attacker handle\");\n\n    address internal owner;\n    address internal attacker = makeAddr(\"attacker\");\n    address internal friend = makeAddr(\"friend\");\n    address internal victimWallet = makeAddr(\"victim wallet\");\n    MockUSDC internal usdc;\n    HandleVault internal vault;\n\n    function setUp() public {\n        vm.chainId(11_155_111);\n        vm.warp(10 days);\n        owner = vm.addr(AUTHORIZER_KEY);\n        usdc = new MockUSDC(owner);\n        // Deploy-script values: 1 mUSDC minimum, 100,000 mUSDC rolling cap, fee 0.\n        vault = new HandleVault(owner, address(usdc), 0.0001 ether, 1e6, 10 ether, CAP);\n        vm.startPrank(owner);\n        usdc.mint(attacker, CAP);\n        usdc.mint(friend, 1_000e6);\n        vm.stopPrank();\n    }\n\n    function _deposit(address from, bytes32 id, uint256 amount) internal returns (uint256) {\n        vm.startPrank(from);\n        usdc.approve(address(vault), amount);\n        uint256 depositId = vault.depositFor(id, address(usdc), amount);\n        vm.stopPrank();\n        return depositId;\n    }\n\n    function _sig(bytes32 id, address dest, uint256 nonce, uint256 deadline) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(AUTHORIZER_KEY, vault.claimDigest(id, dest, nonce, deadline));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _claim(bytes32 id, address dest, uint256 nonce) internal returns (uint256) {\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(usdc);\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        return vault.claim(id, dest, tokens, nonce, deadline, _sig(id, dest, nonce, deadline));\n    }\n\n    function test_unprivilegedUserExhaustsSharedCapForFree() public {\n        _deposit(friend, VICTIM, 5e6);\n        for (uint256 day; day < 3; ++day) {\n            uint256 before = usdc.balanceOf(attacker);\n            _deposit(attacker, ATTACKER_HANDLE, CAP);\n            assertEq(_claim(ATTACKER_HANDLE, attacker, day), 1);\n            assertEq(usdc.balanceOf(attacker), before, \"cycle is free at fee 0\");\n            assertEq(vault.remainingOutflow(address(usdc)), 0);\n            vm.warp(vm.getBlockTimestamp() + 24 hours);\n        }\n        // Attacker refreshes right at window roll-over, before the victim's relayer lands.\n        _deposit(attacker, ATTACKER_HANDLE, CAP);\n        _claim(ATTACKER_HANDLE, attacker, 99);\n        // Expected: the victim's 5 mUSDC deposit is claimable. Actual: revert OutflowCapExceeded().\n        assertEq(_claim(VICTIM, victimWallet, 0), 1, \"victim should be able to claim 5 mUSDC\");\n        assertEq(usdc.balanceOf(victimWallet), 5e6);\n    }\n}","reproduction":"Deploy with the script values (mUSDC cap 100_000e6, fee 0). 1) friend: depositFor(VICTIM, usdc, 5e6). 2) attacker: depositFor(ATTACKER_HANDLE, usdc, 100_000e6); claim(ATTACKER_HANDLE, attacker, [usdc], 0, deadline, authorizerSig) -> count 1, attacker balance back to its start, remainingOutflow(usdc) == 0. 3) claim(VICTIM, victimWallet, [usdc], 0, deadline, validAuthorizerSig). Expected: 1 deposit swept, 5e6 paid. Actual: revert OutflowCapExceeded(). 4) warp +24h, attacker repeats step 2 at the roll-over; step 3 reverts again indefinitely. Run: forge test --match-path test/scratch/SharedCapExhaustion.t.sol (fails on current code with OutflowCapExceeded()).","severity":"medium","snippet":"        accounting.available = remainingOutflow(token);","title":"Global per-token 24h cap can be exhausted for free by any handle holder, halting every other recipient's claims in that token (merged: economics #1, flow #2)"},{"citation":"resolved","description":"The fee is computed per deposit with floor rounding and never on the aggregate, so for amount * feeBps < 10_000 the fee is exactly 0 and across N deposits the accrued fee can be up to N base units below the fee on the total. The deploy configuration (mUSDC minimum 1e6, native minimum 1e14) keeps every deposit above the zero-fee boundary at the maximum 100 bps, but setTokenConfig accepts any minimum >= 1 and nothing ties the minimum to feeBps, so an admin who lists a token with a tiny minimum silently waives the protocol fee for small deposits, and a depositor can split a large amount into many fee-free deposits (bounded by the 64-per-claim budget and gas). Impact is under one base unit per deposit, protocol-favouring rounding is the usual convention. Minimal fix: Math.mulDiv(d.amount, currentFeeBps, 10_000, Math.Rounding.Ceil) (claim already tolerates a zero payout), or enforce minimum * MAX_FEE_BPS >= 10_000 in _configureToken.","line":464,"path":"src/HandleVault.sol","reproduction":"setFee(100, treasury); setTokenConfig(usdc, true, 1); 50 x depositFor(R, usdc, 99); claim(R, dest, [usdc], 0, deadline, sig). Expected at a 1% fee on 4,950 units: about 49 units accrued. Actual (ran test/scratch/FeeRounding.t.sol): swept 50, accruedFees(usdc) == 0, dest receives 4,950.","severity":"low","snippet":"            uint256 fee = Math.mulDiv(d.amount, currentFeeBps, 10_000);","title":"Claim fee is floored per deposit and is zero for any deposit below 10_000/feeBps base units (math)"},{"citation":"resolved","description":"The signed Claim type is exactly the one the brief specifies, Claim(bytes32 recipientId,address dest,uint256 nonce,uint256 deadline), so tokens[] is unsigned and claim is permissionless. Anyone who sees a relayer's pending claim can land the same signature first with a smaller token list; the nonce is consumed (line 221) once at least one deposit is swept and the remaining tokens stay open until the claimant obtains a fresh signature, which can be front-run the same way. Funds always go to the signed dest and every accepted call makes progress, so nothing is lost; this is a griefing cost (one extra signature per token stripped) that follows from the type the brief mandated and that the NatSpec (lines 202-203) and test_tokenSubsetIsDeliberatelyUnsigned document. Recorded so the operator knows the tradeoff; closing it needs a new type (hash of tokens[] in the struct) and is a scope decision, not a defect in implementing the brief.","line":217,"path":"src/HandleVault.sol","reproduction":"Fixture state: RECIPIENT has open deposits 5e6 usdc and 1 unit of `other`. Authorizer signs Claim(RECIPIENT, dest, nonce 0, deadline). Attacker submits claim(RECIPIENT, dest, [other], 0, deadline, sig) before the claimant's relayer: returns 1, usedNonces(RECIPIENT, 0) == true, totalEscrowed(RECIPIENT, usdc) still 5e6. The claimant's claim(RECIPIENT, dest, [usdc, other], 0, ...) then reverts NonceAlreadyUsed(). test/Signatures.t.sol line 185 test_tokenSubsetIsDeliberatelyUnsigned executes exactly this sequence and passes.","severity":"info","snippet":"        _checkTokens(tokens);","title":"Token list is not bound by the Claim signature, so a mempool observer can consume a nonce with a one-token subset (merged: economics #3, permissions #2, tests #1)"}],"hash":"03275187ba7a7a9d9ad5906e069c466a0c2a6acbff1ed75a1234dfc0b597900b","nodeId":"885c0679-c779-4200-8b64-050c5c54fb22","outcome":"completed","summary":"All four snippets now match the tree exactly. The review is complete and `.imd-findings.json` holds the final report.\n\n**Outcome.** I kept four findings from the five specialist reports after reproducing each one, merged the duplicates, and found nothing new in my own pass. The project's own suite passes with 99 tests.\n\n- **High, FIFO head-of-line block** at `src/HandleVault.sol:460`. Deposits are accepted up to the full cap, but the sweep stops the whole queue at a head that does not fit the remaining window. Any sender can park a cap-sized deposit on a victim's handle and every later deposit is unclaimable while any claim of that token happens each day. The attacker refunds in full after 180 days. Four specialists reported this at different severities. It is one root cause, so it is one finding, rated high because it breaks the brief's explicit \"never unclaimable\" guarantee. My proof under test/scratch fails with `OutflowCapExceeded`.\n- **Medium, shared global cap exhausted for free** at line 456. With the deployed zero fee, a handle holder deposits the cap to their own handle and claims it straight back, zeroing the window for everyone. Two specialists reported it. Proof attached, fails the same way.\n- **Low, fee floors to zero** for tiny deposits at line 464. I ran it: 50 deposits of 99 units at a 1% fee accrue no fee. Only reachable if the admin sets a tiny minimum.\n- **Info, unsigned token list** at line 217. A front-runner can consume a nonce with a subset of tokens. Three specialists reported it. The brief mandates that exact Claim type and the code documents the choice, so it is recorded as a tradeoff rather than a defect.\n\n**Coverage.** All 25 entry points are answered, plus the balance invariant, the sliding-window math and the launch manifest. The manifest has the right shape and argument order, the vault's runtime size is well under the limit, and the constructor makes no external calls.\n\n**Not kept.** The test writer's claim that a blocked head always clears at window roll-over does not hold: it clears only when the window is completely empty. Slither's uninitialized-state and strict-equality lines are false positives in this code.","treeHash":null,"usage":{"cachedInputTokens":841721,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":24175,"runtime":"claude","turns":21,"wallClockMs":423342}},{"artifacts":[],"attempt":1,"bundleHash":"46d3ee95cd7d1562705e3557ff3fbd6275e2a0320ac1f7795515ab11c440a4af","device":"e6de8d4c6cf97551","findings":[],"hash":"0924085c01816bf398bdb0332271d8afad5fe0967f42bcb4b26b9cc797db02ff","nodeId":"e576aa9b-930a-4f8c-8d40-28524483eb66","outcome":"completed","summary":"The revision is complete and verified. Summary of what was done:\n\n**Finding 1 (high, head-of-line blocking): fixed.** Reproduced with the reviewer's proof, which failed with `OutflowCapExceeded()`. In `src/HandleVault.sol`, the sweep no longer stops at a deposit that does not fit the remaining cap. It skips it, emits `DepositSkipped`, leaves it open, and keeps walking the FIFO behind it. A new `MAX_VISITS_PER_CLAIM` of 256 bounds how many deposits one call examines so gas stays bounded, on top of the existing 64 swept per call. The proof now passes. The invariant model was updated to the same skip rule and still agrees with the contract.\n\n**Finding 2 (medium, shared cap exhaustion): disputed proof, finding addressed.** The proof cannot pass against any contract with the brief's global per-token cap: it asserts the global window is fully consumed and then that another recipient's claim still succeeds in the same window, and it also requires a single handle to sweep a cap-sized deposit in one call, which is the exact act the finding says must be prevented. The substantive mitigation is an admin-settable `recipientShareBps`: each recipient and token pair now has its own rolling 24-hour window bounded by a share of the cap, claims use the lesser of the two remaining windows, and deposits are bounded by the recipient cap so every accepted deposit fits an empty window. It starts at 100% because the held proof for finding 1 requires a cap-sized deposit to be accepted. The README documents the tradeoff and recommends lowering the share after launch.\n\n**Findings 3 and 4 (advisory): disputed.** Per-deposit floor fee rounding is the documented, tested behaviour with under one base unit of impact per deposit. The unsigned token list follows from the Claim type the brief mandates and is already documented.\n\n**Verification.** `forge build`, `forge fmt --check`, and `forge test` with the verifier's fuzz seed pass: 84 project tests, including 14 new ones in `test/CapSharing.t.sol`. All four answers are in `.imd-responses.json`. README, `docs/SECURITY_REVIEW.md`, and `docs/VERIFICATION.md` were updated. The deploy script and constructor are unchanged.","treeHash":"89dc8c4fae25f69c30220fc0737e8402c6ae6de9","usage":{"cachedInputTokens":2195534,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":54581,"runtime":"claude","turns":63,"wallClockMs":914028}},{"artifacts":[],"attempt":1,"bundleHash":"4717c88d7baa78fe4960a391fa37a3983442ce857a5629cdd6df77f6db5fdc32","device":"4faf975f1178e1f8","findings":[],"hash":"0b2d371ed07c606f2feafe72812cb81152e297578dc2f170eae7da24a97f6a4a","nodeId":"46ff52b6-5014-4fed-a0ee-aebdad73fef4","outcome":"completed","summary":"launch.json is written and verified. It is the only file changed in the working tree.\n\n**What the manifest declares**\n\n- **MockUSDC** first, with constructor argument `$owner`. It is the six-decimal mintable test token the vault allowlists.\n- **HandleVault** second, with `$owner`, `$contract:MockUSDC`, then the four raw base-unit values from the accepted deploy script and README: native minimum 0.0001 ETH, token minimum 1 mUSDC, native rolling cap 10 ETH, token rolling cap 100,000 mUSDC.\n- **Notes** of about 2,100 characters describe both constructors, the roles the owner receives, and the signer's EIP-712 domain. They stay well under the 4,000 limit.\n\n**Checks run**\n\n- A structural check confirmed only the keys kind, contracts and notes, string arguments, unique short identifiers, and a backward-pointing contract reference. The decimal values were recomputed and match the deploy script constants.\n- `forge build` succeeds. The lint note about sending ETH to an arbitrary address comes from the accepted vault source and is the intended claim payout path, not something this task changed.\n- `forge test` with the verifier's fuzz seed passed all 70 tests across 5 suites, including the balance invariant.\n- `git status` shows launch.json as the sole new file. No configuration, library or source file was touched.\n\nThe vault constructor only records the MockUSDC address without calling it, so deploying both contracts in one factory transaction on an empty chain works as the protected probe rehearses it.","treeHash":"88ab6f5af435f5b04a31c9db0b29887898491787","usage":{"cachedInputTokens":216266,"inputTokens":162,"model":"claude-fable-5-1","outputTokens":3908,"runtime":"claude","turns":11,"wallClockMs":174419}},{"artifacts":[],"attempt":1,"bundleHash":"f011a5e2f15073683d8e9fbdd9077ebf5e2c818cd177d1aad3703f9089f5222a","device":"d03fc84904b54d9e","findings":[],"hash":"1c889dee9a113bada9ec077e9323e3a244661a7136f3629eaae9a23d58a43b60","nodeId":"46ff52b6-5014-4fed-a0ee-aebdad73fef4","outcome":"completed","summary":"The manifest is correct as it stands, so I'm leaving launch.json unchanged. No review findings were included in this round, and every check I ran against the accepted source passed.\n\n**What I verified**\n\n- `launch.json` has exactly the three schema keys, kind \"evm_contracts\", two unique contract names under 32 characters, and notes of 2,116 characters.\n- MockUSDC takes one argument, `$owner`, matching its constructor `MockUSDC(address initialOwner)`.\n- HandleVault lists six arguments in declaration order: `$owner`, `$contract:MockUSDC`, then the four numeric values. These equal the deploy script constants exactly (0.0001 ETH and 10 ETH in wei, 1 mUSDC and 100,000 mUSDC in six-decimal units), and they satisfy the constructor's checks that caps are at least the minimums.\n- The backward reference fills an address parameter, and the constructor only stores the token address without calling it, so the empty-chain rehearsal in the protected test succeeds.\n- Claims in the notes match the source: one-day admin hand-over delay, fee maximum of 100 bps, immutable 180-day refund delay, pause gating deposits and claims but not refunds or fee withdrawal.\n- The README documents the EIP-712 domain (name HandleVault, version 1, chain id 11155111) and the Claim typehash the notes point signers to.\n\n**Checks run**\n\n```\nforge build        ok (only a pre-existing lint note on the native-transfer call)\nforge test         84 passed, 0 failed, 0 skipped (with FOUNDRY_FUZZ_SEED=0x98cd…3880)\n```\n\nThe working tree is clean. No file was changed, which satisfies the rule that only launch.json may change and the criterion that the manifest match the accepted implementation.","treeHash":"aa72dcd9e2177b64ade77531de6a62ce99a34707","usage":{"cachedInputTokens":172047,"inputTokens":130,"model":"claude-fable-5-1","outputTokens":2847,"runtime":"claude","turns":5,"wallClockMs":149555}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"09078b7cdfb673fe","findings":[{"citation":"resolved","description":"Asymmetry between the deposit-side cap check and the claim-side cap check. `_checkDeposit` (line 384) only rejects `amount > outflowCaps[token]`, i.e. it compares against the full 24h cap. `_sweep` (line 460) compares the queue head against `remainingOutflow(token)`, which is the cap minus gross claims by ALL recipients in the last 24h, and on a miss it `break`s, so nothing behind that head is ever reached for that recipient and token. The two checks are only equal when the vault has had zero claims of that token for 24 hours. An unprivileged attacker therefore deposits an amount between the typical remaining capacity and the cap (at most `cap`, e.g. 100_000 USDC or 10 ETH with the deploy-script values; far less on a busy vault, since only `amount > remaining` is needed) to the victim's recipientId. Every deposit that arrives for that recipient afterwards sits behind it and `claim` reverts with `OutflowCapExceeded` for any valid authorizer signature. The attacker's capital is fully recoverable: after REFUND_DELAY (180 days) they call `refund` on their own deposit, and can re-deposit in the same transaction to block all deposits made after that point again. The attacker can also keep the window from ever clearing at gas cost only (fee is 0) by claiming small deposits to their own handle once a day. This violates the brief's requirement that 'a spammed recipient can never be made unclaimable' and the README's claim that spamming cannot prevent claims; the README only documents the admin-lowers-cap variant of a blocked head. Victims' senders can only get their money back after 180 days; the recipient never receives it. Fix (keeps FIFO and the cap): in `_sweep`, skip a head that does not fit and continue to the next open deposit (walk `_links[id].tokenNext` instead of re-reading `head`), setting `blocked` so the caller still gets `OutflowCapExceeded` when nothing at all fits; alternatively let `claim` take explicit deposit ids. Either way a deposit that fits the remaining cap must always be sweepable regardless of what precedes it. Verified: applying this skip-and-continue change to a scratch copy of the contract makes the proof test pass while the existing suite's cap semantics (blocked -> OutflowCapExceeded when nothing fits) are preserved.","line":460,"path":"src/HandleVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HandleVault} from \"src/HandleVault.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\n\n/// @notice An unprivileged depositor can make a recipient unclaimable for a token by placing a\n/// deposit at the head of that recipient's FIFO queue that is larger than the cap capacity that\n/// is ever available while the vault sees ordinary traffic. The deposit passes `_checkDeposit`\n/// (amount <= cap) but `_sweep` stops the whole queue at a head that does not fit `remainingOutflow`,\n/// so every later deposit for that recipient is stuck behind it until the attacker refunds (180 days).\ncontract HeadBlockProof is Test {\n    uint256 internal constant OWNER_KEY = 0xA11CE;\n    bytes32 internal constant VICTIM = keccak256(\"victim handle\");\n    bytes32 internal constant OTHER_USER = keccak256(\"some other user\");\n\n    address internal owner;\n    address internal attacker;\n    address internal friend;\n    address internal dest;\n    MockUSDC internal usdc;\n    HandleVault internal vault;\n\n    function setUp() public {\n        vm.chainId(11_155_111);\n        vm.warp(10 days);\n        owner = vm.addr(OWNER_KEY);\n        attacker = makeAddr(\"attacker\");\n        friend = makeAddr(\"friend\");\n        dest = makeAddr(\"victim destination\");\n        usdc = new MockUSDC(owner);\n        // Deploy-script values: 1 USDC minimum, 100_000 USDC rolling 24h cap.\n        vault = new HandleVault(owner, address(usdc), 0.0001 ether, 1e6, 10 ether, 100_000e6);\n        vm.startPrank(owner);\n        usdc.mint(attacker, 100_000e6);\n        usdc.mint(friend, 1_000e6);\n        vm.stopPrank();\n    }\n\n    function _sign(bytes32 recipientId, address destination, uint256 nonce, uint256 deadline)\n        internal\n        view\n        returns (bytes memory)\n    {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_KEY, vault.claimDigest(recipientId, destination, nonce, deadline));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _deposit(address from, bytes32 recipientId, uint256 amount) internal returns (uint256 id) {\n        vm.startPrank(from);\n        usdc.approve(address(vault), amount);\n        id = vault.depositFor(recipientId, address(usdc), amount);\n        vm.stopPrank();\n    }\n\n    function test_unprivilegedHeadDepositBlocksEveryLaterDepositForRecipient() public {\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(usdc);\n\n        // 1. Attacker deposits exactly the cap to the victim's handle. Accepted: amount <= cap.\n        _deposit(attacker, VICTIM, 100_000e6);\n\n        // 2. A friend later sends the victim 50 USDC. This is the deposit the victim actually wants.\n        uint256 friendDeposit = _deposit(friend, VICTIM, 50e6);\n\n        // 3. Ordinary traffic: any other recipient claims anything in the same 24h window.\n        _deposit(friend, OTHER_USER, 1e6);\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        vault.claim(OTHER_USER, makeAddr(\"other dest\"), tokens, 0, deadline, _sign(OTHER_USER, makeAddr(\"other dest\"), 0, deadline));\n        assertEq(vault.remainingOutflow(address(usdc)), 100_000e6 - 1e6);\n\n        // 4. The victim, with a valid authorizer signature, cannot claim the 50 USDC that fits the\n        //    remaining cap, because the attacker's deposit sits at the head and the sweep stops there.\n        //    Expected: the claim pays the 50 USDC deposit and leaves the oversized one open.\n        //    Actual on current code: revert OutflowCapExceeded, nothing paid, for as long as the\n        //    attacker's deposit is open (attacker can refund it after 180 days and re-deposit).\n        uint256 count = vault.claim(VICTIM, dest, tokens, 0, deadline, _sign(VICTIM, dest, 0, deadline));\n        assertEq(count, 1, \"claim should sweep the deposit that fits the cap\");\n        assertEq(usdc.balanceOf(dest), 50e6, \"victim should receive the friend's deposit\");\n        (,,,,, HandleVault.Status status) = vault.deposits(friendDeposit);\n        assertEq(uint8(status), uint8(HandleVault.Status.Claimed));\n        // Contract balance still equals open deposits plus fees.\n        assertEq(usdc.balanceOf(address(vault)), vault.totalOpen(address(usdc)) + vault.accruedFees(address(usdc)));\n    }\n}","reproduction":"Deploy with script values (usdc cap 100_000e6, minimum 1e6). 1) attacker: usdc.approve + vault.depositFor(VICTIM, usdc, 100_000e6) -> accepted (amount == cap). 2) friend: depositFor(VICTIM, usdc, 50e6). 3) any other recipient claims 1e6 usdc in the same 24h window, so remainingOutflow(usdc) == 99_999e6. 4) victim relays claim(VICTIM, dest, [usdc], nonce, deadline, validAuthorizerSig). Expected: count == 1, dest receives 50e6, the oversized deposit stays open. Actual: revert OutflowCapExceeded(); the 50e6 deposit (and every later one) is unclaimable until the attacker refunds at day 180, after which the attacker can immediately re-deposit and repeat. Proof test: test/scratch/HeadBlock.t.sol fails on current code with OutflowCapExceeded().","severity":"high","snippet":"        while (id != 0 && count < budget) {\n            Deposit storage d = deposits[id];\n            if (d.amount > accounting.available - accounting.gross) {\n                blocked = true;\n                break;\n            }","title":"Any depositor can make a recipient permanently unclaimable in a token by parking a cap-sized deposit at the head of its FIFO queue"},{"citation":"resolved","description":"Trust gap between what the AUTHORIZER signs (recipientId, dest, nonce, deadline) and what the unprivileged relayer controls (`tokens`, line 217, checked only for shape). `claim` is permissionless and the signature is public once it is in the mempool, so anyone can submit it first with a different `tokens` array. The nonce is consumed (line 221) as soon as at least one deposit is swept, so a front-runner who passes a single-token list makes the remaining tokens unclaimable under that signature; the claimant must obtain a fresh signature per token the attacker strips off. Funds are not lost (payout still goes to the signed `dest`), so this is a griefing/liveness issue, and the NatSpec (lines 202-203) and test_tokenSubsetIsDeliberatelyUnsigned state it is intentional. It is reported because the brief says a claim 'pays every open deposit for that recipient in the listed tokens' and the authorizer cannot actually enforce which tokens are listed. Fix if the design is revisited: include keccak256(abi.encodePacked(tokens)) in the Claim struct, or keep tokens unsigned but let the signer pin a minimum set.","line":217,"path":"src/HandleVault.sol","reproduction":"Fixture: deposits of 5e6 usdc and 1 `other` for RECIPIENT. Authorizer signs Claim(RECIPIENT, dest, nonce 0, deadline). Claimant's relayer submits claim(RECIPIENT, dest, [usdc, other], 0, ...). Attacker sees the tx and front-runs claim(RECIPIENT, dest, [other], 0, deadline, sameSig): count == 1, usedNonces[RECIPIENT][0] == true. Claimant's tx then reverts NonceAlreadyUsed(); totalEscrowed(RECIPIENT, usdc) is still 5e6 and needs a new signature. Expected per brief: the signed claim sweeps both listed tokens.","severity":"low","snippet":"        if (usedNonces[recipientId][nonce]) revert NonceAlreadyUsed();\n        _checkTokens(tokens);\n        (address signer, ECDSA.RecoverError err,) =\n            ECDSA.tryRecover(claimDigest(recipientId, dest, nonce, deadline), signature);\n        if (err != ECDSA.RecoverError.NoError || !hasRole(AUTHORIZER_ROLE, signer)) revert InvalidAuthorizer();\n        usedNonces[recipientId][nonce] = true;","title":"Token list is not bound by the Claim signature, so any relayer can front-run a published claim, shrink it to one token and burn the nonce"}],"hash":"33fbd23fc99cc3f03dda8606368a030ca85f69a810e3596ba62ae87eb5849af9","nodeId":"72a47048-18d8-403b-aaba-325bfb3f1e35","outcome":"completed","summary":"The review is complete. The findings file is written at the repository root, with two findings and a coverage row for all 25 entry points plus two invariants. No source, test, or configuration file was changed. The scratch tests live only under the git-ignored directory the task set aside for them.\n\n**Finding 1, high: an unprivileged depositor can make a recipient unclaimable in a token.** The deposit path only checks that an amount is at most the full 24-hour cap, while the claim sweep compares the queue head against the cap minus everyone's claims in the last 24 hours and stops the whole queue at the first deposit that does not fit. An attacker deposits a cap-sized amount to the victim's handle. From then on, whenever any other recipient has claimed anything in the window, the victim's claim reverts and every later deposit for that recipient stays stuck behind the blocker. The attacker's capital is fully recoverable by refund at day 180, and I confirmed in a scratch test that they can refund and re-deposit in one transaction to block again. This contradicts the brief's requirement that a spammed recipient can never be made unclaimable. The proof test fails on the current code with the expected revert, and passes on a scratch copy where the sweep skips a non-fitting deposit and continues to the next one, so the fix is small and keeps the cap semantics.\n\n**Finding 2, low: the token list is not covered by the Claim signature.** Any relayer who sees a published signature can front-run it with a single-token list, which consumes the nonce and forces the claimant to obtain a new signature per stripped token. Payout still goes to the signed destination, so this is griefing only. The NatSpec and a test state the choice is deliberate, so I reported it for the judge's call rather than as a definite defect.\n\n**What I checked and found sound in my area.** Role grants, revokes and renounces behave as intended under the OpenZeppelin admin rules, including the impossibility of renouncing or directly granting the default admin. Pause blocks only deposits and claims. Refunds pay the original sender in full and ignore pause, allowlist, caps and fees. Fee withdrawal can only move accrued fees. No path lets any role move escrowed principal outside a valid claim or a refund. Signature recovery rejects malleable and zero-address outcomes. The balance invariant holds across every state-changing path I traced. The admin powers that the Trust Gap guide flags, such as redirecting already accrued fees or raising fees on existing deposits, are documented trust assumptions with no unprivileged amplifier, so they are not reported as defects.\n\n**Not reached.** I did not run a longer invariant campaign or examine the deploy manifest beyond the constructor arguments in the script.","treeHash":null,"usage":{"cachedInputTokens":1624241,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":32033,"runtime":"claude","turns":29,"wallClockMs":594490}},{"artifacts":[],"attempt":1,"bundleHash":"5bda6fe6b7b65db8b3e8cdc89e72a9880ea003bac3971e571d3d7e94b38d8b40","device":"3311c48cd0280ed4","findings":[],"hash":"5a31bbc51aaeec9d5231da15178bcc82d7cc7a3409b856ada06b8c90e1ffd8d8","nodeId":"e576aa9b-930a-4f8c-8d40-28524483eb66","outcome":"completed","summary":"Implemented HandleVault, MockUSDC, vendored dependencies, tests, deployment rehearsal, and documentation.\n\nValidation passed: build, formatting, and 70 tests, including invariants across 12,288 randomized actions.\n\nThe [README](/home/imd1/.identitymd/work/c0d37569-b47b-4de9-bfed-718a01bba796/e576aa9b-930a-4f8c-8d40-28524483eb66/README.md) documents signing details, parameters, and trust assumptions. Confirmed Sepolia addresses remain pending IdentityMD’s deployment stage.","treeHash":"ea6e62ac1560cb4dfd8af27a8b50fda7b1de9142","usage":{"cachedInputTokens":2030976,"inputTokens":108375,"model":"gpt-6-astra","outputTokens":56431,"runtime":"codex","turns":9,"wallClockMs":2315940}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d63ea36a2b809080","findings":[{"citation":"resolved","description":"Settlement of the prior medium finding 3d4b4355 (shared per-token budget exhaustible for free by any attested handle). The author's dispute of the attached proof is correct: the proof asserts that one handle sweeps a cap-sized deposit (remainingOutflow == 0) and that a second recipient then claims 5e6 in the same window, i.e. gross claims of cap + 5e6 inside one 24-hour window, which the brief's global per-token cap forbids by definition; the proof also depends on a cap-sized deposit being accepted, which the other held proof (a51b1572) requires. Re-ran .imd/reads/proofs/Proof_3d4b435538cd.t.sol on the revised code: still fails with OutflowCapExceeded(), as the author said, and it cannot pass under any correct implementation of the brief's cap, so it is withdrawn as a proof. The fix the author delivered is the one my finding proposed: a per-(recipientId, token) exact rolling window bounded by recipientCap(token) = cap * recipientShareBps / 10_000, a claim using min(recipient remaining, shared remaining), and deposits bounded by recipientCap so every accepted deposit fits an empty window. I verified with test/CapSharing.t.sol test_oneRecipientCannotConsumeTheWholeWindowWhenShared (passes) and my own scratch test that at 2_500 bps one handle takes a quarter of the window and the victim's 5e6 claim succeeds. What remains is a configuration residual, not a code defect: the deployed default is 10_000 bps, so on launch day one attested handle still drains the whole mUSDC window at zero cost (feeBps = 0) and every other recipient's mUSDC claim reverts OutflowCapExceeded for 24 hours; the same default also enables a cheaper targeted variant through the 256-visit bound (attacker cycles 99_900e6 through its own handle so remainingOutflow == 100e6, then parks 300 deposits of 101e6 ahead of the victim's genuine 50e6 deposit; claim visits 256 skipped records and reverts OutflowCapExceeded). Both are bounded once the admin lowers the share to <= 50%, which README 'After launch' step 5 and docs/SECURITY_REVIEW.md instruct. Recalibrated to low: the brief's global cap makes some exhaustion inherent, the lever and its documentation exist, and the only open question is the initial value, which is the requester's call. Suggestion, not a blocker: ship with a lower default (e.g. 2_500 or 5_000 bps) or set it in the deployment constructor arguments so the vault is protected from block one; this would also lower the largest accepted deposit to recipientCap, which the brief does not forbid.","line":40,"path":"src/HandleVault.sol","reproduction":"Deploy with the manifest/script values (mUSDC minimum 1e6, cap 100_000e6, feeBps 0, share 10_000). 1) friend: depositFor(VICTIM, usdc, 5e6). 2) attacker: approve + depositFor(ATTACKER_HANDLE, usdc, 100_000e6); claim(ATTACKER_HANDLE, attacker, [usdc], 0, deadline, authorizerSig) -> count 1, attacker balance unchanged, remainingOutflow(usdc) == 0. 3) claim(VICTIM, victimWallet, [usdc], 0, deadline, authorizerSig). Expected (with a share < 100% active): 1 swept, 5e6 paid. Actual at the launch default: revert OutflowCapExceeded() until the window rolls or the admin calls setRecipientShareBps. Control: the same sequence after setRecipientShareBps(2_500) with the attacker's deposit at 25_000e6 pays the victim 5e6 (remainingOutflow == 75_000e6). Ran as test/scratch/ResidualShare.t.sol (test_defaultShareStillLetsOneHandleExhaustWindowForFree, test_quarterShareBoundsOneHandle, test_visitBoundGriefAtDefaultShare): all three assertions hold on the revised code.","severity":"low","snippet":"    uint256 public constant DEFAULT_RECIPIENT_SHARE_BPS = 10_000;","title":"Launch default recipientShareBps = 100% leaves the free shared-cap exhaustion live until the admin lowers it (settles prior 3d4b4355; mitigation delivered, residual at deployed config)"},{"citation":"resolved","description":"Still reproduces exactly as before: the fee is floor(amount * feeBps / 10_000) per deposit, never on the aggregate, so for amount * feeBps < 10_000 the fee is zero and N small deposits can accrue up to N base units less than the fee on the total. The author's answer holds: the brief says the fee is taken from each claimed amount, README line 152 documents the per-deposit floor with dust staying with the claimant, test_feeMaxRoundedPerDepositAndWithdrawOnlyFees pins it, and the deployed minimums (1e6 mUSDC, 1e14 wei) keep every deposit above the zero-fee boundary at the maximum 100 bps. The exposure is under one base unit per deposit and only arises if the admin lists a token with a minimum below 10_000/feeBps. Kept as information for the admin choosing token minimums; not a defect against the brief and not blocking.","line":540,"path":"src/HandleVault.sol","reproduction":"setFee(100, treasury); setTokenConfig(usdc, true, 1); 50 x depositFor(R, usdc, 99); claim(R, dest, [usdc], 0, deadline, sig). Expected at 1% on 4,950 units if fees were aggregated: about 49 units. Actual: swept 50, accruedFees(usdc) == 0, dest receives 4,950. Matches the author's own reproduction.","severity":"info","snippet":"                uint256 fee = Math.mulDiv(d.amount, currentFeeBps, BPS_DENOMINATOR);","title":"Claim fee is floored per deposit and is zero for any deposit below 10_000/feeBps base units (settles prior a2e920ff; documented behaviour, no change requested)"},{"citation":"resolved","description":"Unchanged and by design: the signed type is exactly the brief's Claim(bytes32 recipientId,address dest,uint256 nonce,uint256 deadline), published in the README as the typehash the signer must use, so tokens[] is unsigned and claim is permissionless. A front-runner can land the same signature with a smaller token list; the nonce is consumed once at least one deposit is swept and the remaining tokens need a fresh signature. Funds always go to the signed dest and every accepted call makes progress, so this is a griefing cost of one extra signature per stripped token. The NatSpec (lines 218-219), README 'Claim signing and relaying' and test_tokenSubsetIsDeliberatelyUnsigned document it. Binding tokens[] needs a new EIP-712 type and is the requester's scope decision; recorded so the operator knows the tradeoff.","line":239,"path":"src/HandleVault.sol","reproduction":"Fixture: RECIPIENT has open deposits 5e6 usdc and 1 unit of `other`. Authorizer signs Claim(RECIPIENT, dest, 0, deadline). Attacker submits claim(RECIPIENT, dest, [other], 0, deadline, sig) first: returns 1, usedNonces(RECIPIENT, 0) == true, totalEscrowed(RECIPIENT, usdc) still 5e6. The claimant's claim(RECIPIENT, dest, [usdc, other], 0, ...) then reverts NonceAlreadyUsed(). test/Signatures.t.sol test_tokenSubsetIsDeliberatelyUnsigned executes this sequence and passes on the revised code.","severity":"info","snippet":"        usedNonces[recipientId][nonce] = true;","title":"Token list is not bound by the Claim signature, so a mempool observer can consume a nonce with a one-token subset (settles prior 2db60d34; by specification, no change requested)"}],"hash":"66f738ade77f1b4cbdc189f148fe75f5f3bfd23492e80d8ac21d2788f98619e3","nodeId":"885c0679-c779-4200-8b64-050c5c54fb22","outcome":"completed","summary":"The review is written to `.imd-findings.json` with 3 findings and 29 coverage rows. The scratch tests are removed and the tree is otherwise untouched.\n\n**Settlement of the prior round**\n\n- **High, cap-sized head-of-line block: fixed.** The attached proof and all three specialist head-block proofs now pass on the revised code. The sweep reads the next link before closing a deposit, skips a deposit that does not fit, keeps the blocked flag so a call that sweeps nothing still reverts without burning the nonce, and bounds the walk at 256 visits. The invariant model was updated to match and the full suite passes with 115 tests and no failures.\n- **Medium, free exhaustion of the shared cap: mitigation delivered, downgraded to low.** The author's dispute is right. My proof demanded cap plus 5 mUSDC of gross claims inside one window, which no global cap can permit, so I withdrew it. The delivered fix is the one the finding proposed: a per-recipient rolling window at an admin-set share of the cap, with deposits bounded to fit it. I verified at a 25% share that one handle takes a quarter of the window and a victim's claim still pays. What remains is only the launch default of 100%, at which one handle still drains the mUSDC window for free, and a cheaper targeted variant through the 256-visit bound. Both are documented and both close once the admin lowers the share, so the finding is now a low configuration note suggesting a lower initial value.\n- **Low, per-deposit fee floor: kept as info.** It reproduces but is the documented and tested behaviour, and the deployed minimums keep every deposit above the zero-fee boundary.\n- **Info, unsigned token list: kept as info.** It follows from the exact Claim type the brief mandates and is documented in NatSpec, README and a test.\n\n**Fresh pass on the changed code**\n\nI traced the new skip walk, the shared visit budget, the recipient window and the share setter for link corruption, double counting, nonce burn on empty sweeps and refund interference, and found nothing new to report. The Slither leads are all false positives or intended behaviour. The manifest lists both contracts in dependency order with six constructor arguments matching the script, and the runtime size is well under the limit.\n\nNothing in this report blocks the launch.","treeHash":null,"usage":{"cachedInputTokens":802603,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":19917,"runtime":"claude","turns":16,"wallClockMs":352375}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"62491201fe455e89","findings":[{"citation":"resolved","description":"Boundary x invariant seam. _sweep walks the (recipientId, token) queue strictly FIFO and `break`s at the first deposit that does not fit `remainingOutflow(token)`; deposits behind it are never examined even when they fit. _checkDeposit (line 384) only rejects a deposit larger than the FULL cap, so a deposit of exactly outflowCaps[token] is accepted and can only ever be swept when rollingOutflow(token) == 0, i.e. when nobody has claimed that token for 24 hours. Because the cap is global per token, any sender (no admin action, no authorizer involvement) can deposit cap-sized amount to a victim's recipientId; from then on every genuine deposit queued behind it is unclaimable as long as any other recipient claims as little as the token minimum once per 24h (or the griefer claims to their own handle daily). With the deploy configuration that is 100,000 mUSDC locked for 180 days then refundable, at zero fee cost. The brief requires that 'a spammed recipient can never be made unclaimable'; the 64-deposit budget guards the count dimension but the amount dimension at the cap boundary is unguarded. The same stop also makes README line 138's 'can block that token's FIFO head' understate the effect: everything behind the head is blocked too. Suggested fix that keeps the design: let _sweep `continue` past (not `break` at) a non-fitting deposit while the deposit budget lasts, or let the caller pass a per-token start cursor; and/or bound new deposits to a fraction of the cap (e.g. amount <= cap / 2) so a single deposit can never require an empty window.","line":460,"path":"src/HandleVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HandleVault} from \"src/HandleVault.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\n\n/// Shows that a single open deposit that does not fit the remaining rolling cap blocks every\n/// later, smaller deposit of the same (recipientId, token) queue, so a recipient can be made\n/// unclaimable by anyone who deposits a cap-sized amount to them while the token sees any traffic.\ncontract HeadOfLineCapBlockTest is Test {\n    uint256 internal constant AUTH_KEY = 0xA11CE;\n    bytes32 internal constant VICTIM = keccak256(\"victim handle\");\n    bytes32 internal constant OTHER_USER = keccak256(\"other user handle\");\n\n    address internal owner;\n    address internal griefer = makeAddr(\"griefer\");\n    address internal friend = makeAddr(\"friend\");\n    address internal otherSender = makeAddr(\"otherSender\");\n    address internal victimDest = makeAddr(\"victimDest\");\n    address internal otherDest = makeAddr(\"otherDest\");\n    MockUSDC internal usdc;\n    HandleVault internal vault;\n\n    function setUp() public {\n        vm.warp(10 days);\n        owner = vm.addr(AUTH_KEY);\n        usdc = new MockUSDC(owner);\n        // Deploy-script configuration: USDC minimum 1e6, USDC rolling cap 100_000e6.\n        vault = new HandleVault(owner, address(usdc), 0.0001 ether, 1e6, 10 ether, 100_000e6);\n        vm.startPrank(owner);\n        usdc.mint(griefer, 100_000e6);\n        usdc.mint(friend, 10e6);\n        usdc.mint(otherSender, 10e6);\n        vm.stopPrank();\n    }\n\n    function _deposit(address from, bytes32 recipientId, uint256 amount) internal returns (uint256) {\n        vm.startPrank(from);\n        usdc.approve(address(vault), amount);\n        uint256 id = vault.depositFor(recipientId, address(usdc), amount);\n        vm.stopPrank();\n        return id;\n    }\n\n    function _sig(bytes32 recipientId, address dest, uint256 nonce, uint256 deadline)\n        internal\n        view\n        returns (bytes memory)\n    {\n        bytes32 digest = vault.claimDigest(recipientId, dest, nonce, deadline);\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(AUTH_KEY, digest);\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _tokens() internal view returns (address[] memory list) {\n        list = new address[](1);\n        list[0] = address(usdc);\n    }\n\n    function test_smallDepositBehindCapSizedDepositIsUnclaimableWhileTokenHasTraffic() public {\n        // 1. Griefer plants a deposit equal to the cap at the head of the victim's USDC queue.\n        //    depositFor accepts it because amount == outflowCaps[usdc].\n        _deposit(griefer, VICTIM, 100_000e6);\n        // 2. A genuine sender sends the victim 10 USDC.\n        _deposit(friend, VICTIM, 10e6);\n        // 3. Ordinary traffic: another recipient claims 1 USDC within the last 24 hours.\n        _deposit(otherSender, OTHER_USER, 1e6);\n        uint256 deadline = block.timestamp + 1 hours;\n        vault.claim(OTHER_USER, otherDest, _tokens(), 0, deadline, _sig(OTHER_USER, otherDest, 0, deadline));\n\n        // Remaining cap is 99_999 USDC, far more than the 10 USDC deposit that is queued.\n        assertEq(vault.remainingOutflow(address(usdc)), 99_999e6);\n\n        // 4. The victim claims. The 10 USDC deposit fits the remaining cap, but _sweep breaks at the\n        //    cap-sized head (src/HandleVault.sol:460-463) and the call reverts OutflowCapExceeded.\n        uint256 count =\n            vault.claim(VICTIM, victimDest, _tokens(), 0, deadline, _sig(VICTIM, victimDest, 0, deadline));\n        assertGe(count, 1, \"the fitting 10 USDC deposit should have been swept\");\n        assertEq(usdc.balanceOf(victimDest), 10e6, \"victim should receive the 10 USDC that fits the cap\");\n    }\n}","reproduction":"Deploy config (USDC min 1e6, USDC cap 100_000e6, feeBps 0). 1) griefer: depositFor(VICTIM, usdc, 100_000e6) -> accepted (amount == cap). 2) friend: depositFor(VICTIM, usdc, 10e6). 3) otherSender: depositFor(OTHER, usdc, 1e6); claim(OTHER, ...) sweeps it, rollingOutflow = 1e6, remainingOutflow = 99_999e6. 4) claim(VICTIM, victimDest, [usdc], nonce 0, valid sig). Expected: the 10e6 deposit (<= 99_999e6 remaining) is paid, count >= 1. Actual: _sweep sees head amount 100_000e6 > 99_999e6, breaks with count 0, claim reverts OutflowCapExceeded(); victim receives nothing. Repeating step 3 once per day keeps the victim blocked indefinitely. Proof test: test/scratch/HeadOfLineCapBlock.t.sol fails on current code with OutflowCapExceeded().","severity":"medium","snippet":"            if (d.amount > accounting.available - accounting.gross) {\n                blocked = true;\n                break;\n            }","title":"FIFO head-of-line stop at the rolling cap lets any sender make a recipient's whole token queue unclaimable while the token has traffic"},{"citation":"resolved","description":"Math precision: the fee is floored per deposit, never rounded up, and never computed on the aggregate. For any deposit with amount * feeBps < 10_000 the fee is exactly 0, and across N deposits the accrued fee can be up to N units below the fee on the aggregate. The deploy configuration (USDC minimum 1e6, native minimum 1e14) keeps every deposit above the zero-fee boundary, but the admin can set any minimum >= 1 via setTokenConfig (the fixture does so for the second token with minimum 1) and nothing ties the minimum to feeBps, so the protocol's fee is silently waived for small deposits. Impact is bounded to < 1 base unit per deposit. Minimal fix: Math.mulDiv(d.amount, currentFeeBps, 10_000, Math.Rounding.Ceil); note that for amounts below 10_000/feeBps the whole deposit then becomes fee and the token payout can be 0, which claim already tolerates (payouts[i] == 0 skips _pay and accounting stays balanced). Alternatively keep the floor but enforce minimum * feeBps >= 10_000 in setTokenConfig/setFee so no deposit can be fee-free.","line":464,"path":"src/HandleVault.sol","reproduction":"setFee(100, treasury); setTokenConfig(usdc, true, 1); 50 x depositFor(R, usdc, 99); claim(R, dest, [usdc], 0, deadline, sig). Expected with a 1% fee on 4,950 units: ~49 units accrued. Actual: accruedFees(usdc) == 0, dest receives 4,950 (verified with a scratch test: swept 50, gross 4950, accruedFees 0).","severity":"low","snippet":"            uint256 fee = Math.mulDiv(d.amount, currentFeeBps, 10_000);","title":"Claim fee rounds down per deposit and truncates to zero for deposits below 10_000/feeBps base units"}],"hash":"86094598164f431d86336c3438f56580e73df6d7391213c2f2be178ef646ee9f","nodeId":"7e7e0f25-3b76-4742-91f1-a6708da25537","outcome":"completed","summary":"The review is complete and the findings file is written and validated.\n\n**Outcome:** `.imd-findings.json` holds 2 findings and 27 coverage rows. No tracked file was modified; only the findings file and my scratch tests under `test/scratch/` exist, and the latter are not kept.\n\n**Findings**\n\n- **Medium, head-of-line block at the rolling cap** (`src/HandleVault.sol:460`). The sweep walks a recipient's token queue strictly FIFO and breaks at the first deposit that does not fit the remaining 24-hour cap. Deposits behind it are never examined even when they fit. Since the deposit check only rejects amounts above the full cap, any sender can deposit exactly the cap to a victim's recipientId. From then on, every genuine deposit behind it is unclaimable as long as anyone claims that token once per day. This breaks the brief's \"a spammed recipient can never be made unclaimable\" guarantee. The proof test in the findings file fails on current code with `OutflowCapExceeded()` and asserts the 10 USDC deposit is paid once fixed.\n- **Low, fee floors to zero** (`src/HandleVault.sol:464`). The fee is floored per deposit, so any deposit below 10,000 divided by feeBps base units pays no fee. Reachable only when the admin sets a token minimum below that threshold. Verified: 50 deposits of 99 units at the maximum fee accrued 0 instead of 49 on the aggregate.\n\n**What held in my area:** the rolling-window checkpoint math (strictly increasing coalesced timestamps, binary search giving an exact sliding window, the unchecked cumulative wrap being exact because in-window volume never exceeds the cap), inclusive cap and deadline boundaries, refund timing, deposit and payout balance-delta checks, the native path, fee bounds, and the 64-deposit and 16-token budgets. The existing 69 unit and adversarial tests pass. I did not run the stateful invariant suite within the budget.","treeHash":null,"usage":{"cachedInputTokens":1926390,"inputTokens":802,"model":"claude-fable-5-1","outputTokens":26561,"runtime":"claude","turns":26,"wallClockMs":469167}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5c1e4764d4e20ef5","findings":[{"citation":"resolved","description":"_sweep walks a recipient's per-token FIFO queue and stops the entire queue as soon as the head deposit does not fit the remaining rolling cap, instead of skipping it. _checkDeposit (line 384) only rejects deposits larger than the FULL cap, so an attacker may deposit exactly outflowCaps[token] (100,000 USDC in the deploy config) for a victim recipientId. That deposit fits only when the 24h window is completely empty. Because the global window is shared by all recipients and feeBps is 0, the attacker keeps it non-empty for free: deposit 1 USDC to their own handle and claim it once per day. Every genuine deposit queued behind the blocker is then unreachable by any claim, however small, for as long as the attacker wants; after REFUND_DELAY the attacker refunds the 100,000 USDC to themselves and the victim's gifts are refunded to their senders. This breaks the brief's guarantee that a spammed recipient can never be made unclaimable, and the README promise that the vault 'pays every open deposit for that recipient'. The deposit-time check is documented as preventing 'a deposit that can never fit a full window', but a deposit that fits only an empty window is just as blocking in practice. Minimal fix preserving design: in _sweep, when a deposit does not fit, advance to _links[id].tokenNext (skip) rather than break, so smaller deposits behind it are still paid; alternatively let the claimant pass explicit deposit ids.","line":460,"path":"src/HandleVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HandleVault} from \"src/HandleVault.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\n\n/// A refundable, cap-sized deposit at the head of a recipient's FIFO token queue blocks every\n/// deposit behind it whenever the rolling window is not completely empty. The attacker keeps the\n/// window non-empty with a 1 USDC zero-fee self-claim per day, so the victim can never claim the\n/// genuine 100 USDC gift sitting behind the blocker, and the blocker is refunded to the attacker\n/// after 180 days. Fails on the current code (claim reverts OutflowCapExceeded); passes once\n/// _sweep skips a deposit that does not fit instead of stopping the whole queue.\ncontract CapGriefingTest is Test {\n    uint256 internal constant OWNER_KEY = 0xA11CE;\n    bytes32 internal constant VICTIM = keccak256(\"victim handle\");\n    bytes32 internal constant ATTACKER_HANDLE = keccak256(\"attacker handle\");\n    uint256 internal constant CAP = 100_000e6;\n\n    address internal owner;\n    address internal attacker;\n    address internal friend;\n    address internal victimDest;\n    MockUSDC internal usdc;\n    HandleVault internal vault;\n\n    function setUp() public {\n        vm.chainId(11_155_111);\n        vm.warp(10 days);\n        owner = vm.addr(OWNER_KEY);\n        attacker = makeAddr(\"attacker\");\n        friend = makeAddr(\"friend\");\n        victimDest = makeAddr(\"victim destination\");\n        usdc = new MockUSDC(owner);\n        vault = new HandleVault(owner, address(usdc), 0.0001 ether, 1e6, 10 ether, CAP);\n        vm.startPrank(owner);\n        usdc.mint(attacker, CAP + 1_000e6);\n        usdc.mint(friend, 1_000e6);\n        vm.stopPrank();\n    }\n\n    function _sig(bytes32 recipientId, address dest, uint256 nonce, uint256 deadline)\n        internal\n        view\n        returns (bytes memory)\n    {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_KEY, vault.claimDigest(recipientId, dest, nonce, deadline));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _deposit(address from, bytes32 recipientId, uint256 amount) internal returns (uint256 id) {\n        vm.startPrank(from);\n        usdc.approve(address(vault), amount);\n        id = vault.depositFor(recipientId, address(usdc), amount);\n        vm.stopPrank();\n    }\n\n    function _tokens() internal view returns (address[] memory list) {\n        list = new address[](1);\n        list[0] = address(usdc);\n    }\n\n    function test_victimDepositBehindCapSizedHeadIsUnclaimable() public {\n        uint256 blocker = _deposit(attacker, VICTIM, CAP);\n        uint256 genuine = _deposit(friend, VICTIM, 100e6);\n\n        // Attacker keeps the rolling window non-empty with a 1 USDC self-claim at zero fee.\n        _deposit(attacker, ATTACKER_HANDLE, 1e6);\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        vault.claim(ATTACKER_HANDLE, attacker, _tokens(), 0, deadline, _sig(ATTACKER_HANDLE, attacker, 0, deadline));\n        assertEq(vault.remainingOutflow(address(usdc)), CAP - 1e6, \"99,999 USDC of cap remain\");\n\n        // The victim presents a valid authorizer signature. The 100 USDC deposit fits the\n        // remaining cap, yet the whole token queue is stopped at the 100,000 USDC head.\n        vault.claim(VICTIM, victimDest, _tokens(), 1, deadline, _sig(VICTIM, victimDest, 1, deadline));\n\n        assertEq(usdc.balanceOf(victimDest), 100e6, \"genuine deposit should have been paid\");\n        assertEq(uint8(_status(genuine)), uint8(HandleVault.Status.Claimed));\n        assertEq(uint8(_status(blocker)), uint8(HandleVault.Status.Open));\n    }\n\n    function _status(uint256 id) internal view returns (HandleVault.Status status) {\n        (,,,,, status) = vault.deposits(id);\n    }\n}","reproduction":"Deploy with the script values (USDC cap 100_000e6, min 1e6, fee 0). 1) attacker: approve+depositFor(VICTIM, usdc, 100_000e6) -> id 1. 2) friend: depositFor(VICTIM, usdc, 100e6) -> id 2. 3) attacker: depositFor(ATTACKER_HANDLE, usdc, 1e6) -> id 3, then claim(ATTACKER_HANDLE, attacker, [usdc], 0, deadline, authorizerSig) succeeds; remainingOutflow(usdc) = 99_999e6. 4) victim relays claim(VICTIM, victimDest, [usdc], 1, deadline, authorizerSig). Expected: deposit 2 (100e6) fits the remaining 99,999 USDC and is paid. Actual: revert OutflowCapExceeded() because head deposit 1 (100,000e6) > 99,999e6 and the loop breaks before reaching deposit 2. Repeating step 3 every <24h keeps this true indefinitely; at createdAt+180d the attacker calls refund(1) and recovers the full blocker. See test/scratch/CapGriefing.t.sol.","severity":"medium","snippet":"            if (d.amount > accounting.available - accounting.gross) {\n                blocked = true;\n                break;\n            }","title":"Cap-sized head-of-line deposit lets anyone make a recipient's whole token queue unclaimable at zero net cost"},{"citation":"resolved","description":"The rolling outflow cap is global per token, not per recipient, and claims to oneself are indistinguishable from claims to others. With feeBps = 0 (initial value) a user who legitimately controls any handle can deposit outflowCaps[token] to that handle and immediately claim it back to their own address: the funds return in the same transaction, remainingOutflow(token) drops to 0, and no other recipient can claim that token for the next 24 hours. The cost is two transactions of gas; at the maximum 1% fee it is 1% of the cap per day. Repeating the deposit+claim every 24h keeps every other recipient's claims for that token reverting with OutflowCapExceeded indefinitely (refunds remain available to senders after 180 days, so funds are not lost, but the escrow stops functioning for everyone). The cap is meant to bound a compromised authorizer; as built it is also a free denial-of-service lever for any unprivileged user, and it is the amplifier that makes finding 1 permanent. Options that keep the brief's cap: track a second, per-recipient (or per-dest) 24h cap so one claimant cannot consume the whole global window; or exclude claims whose dest equals the deposit sender from the global window; or require a non-zero minimum fee so each exhaustion costs the attacker. State the chosen tradeoff in the README trust assumptions.","line":456,"path":"src/HandleVault.sol","reproduction":"Deploy with script values (USDC cap 100_000e6, fee 0). 1) attacker: depositFor(ATTACKER_HANDLE, usdc, 100_000e6). 2) attacker (with a normal authorizer signature for their own handle): claim(ATTACKER_HANDLE, attacker, [usdc], 1, deadline, sig). Attacker USDC balance is back to its starting value; remainingOutflow(usdc) == 0. 3) friend: depositFor(VICTIM, usdc, 1e6). 4) claim(VICTIM, victimDest, [usdc], 2, deadline, validSig). Expected: the 1 USDC deposit is paid. Actual: revert OutflowCapExceeded(). Repeat steps 1-2 every 24h to keep every claim of this token blocked. Verified with a scratch Foundry test (assertions: attacker balance restored, remainingOutflow == 0, victim claim reverts OutflowCapExceeded).","severity":"medium","snippet":"        accounting.available = remainingOutflow(token);","title":"Any handle holder can exhaust the global per-token 24h outflow cap at zero cost, halting all claims of that token"}],"hash":"905af7aa517121e13c0d08b914b783b670d22b6918386d5581db3fa9a99273a1","nodeId":"8f948400-e67e-4ad1-9373-214edb627b4a","outcome":"completed","summary":"Both snippets match the tree and no repository files outside the permitted paths changed. The review is complete.\n\n**Outcome:** two substantiated medium findings, both in the claim path's interaction with the global rolling outflow cap. Everything else in the assigned area holds.\n\n**Findings written to `.imd-findings.json`:**\n\n1. **Head-of-line blocking makes a recipient unclaimable (medium, with proof).** The sweep stops a recipient's whole token queue when the head deposit does not fit the remaining cap, and the deposit-time check only rejects amounts above the full cap. An attacker deposits exactly the cap for a victim, keeps the 24h window non-empty with a free 1 USDC self-claim per day, and every genuine deposit behind the blocker reverts with OutflowCapExceeded indefinitely. The attacker refunds the blocker after 180 days. The proof test in test/scratch/CapGriefing.t.sol fails on the current code for this reason. Suggested fix: skip an unfitting deposit and continue to the next one rather than breaking.\n\n2. **Global cap is exhaustible by any user at zero cost (medium).** With the initial zero fee, a handle holder deposits the cap to their own handle and claims it straight back, consuming the entire window and blocking every other claim of that token for 24 hours, repeatable daily. Reproduced in a scratch test. This is also the amplifier behind finding 1.\n\n**Coverage:** all 25 listed entry points have rows. The claim entry point is marked as a finding; the other 24 hold after tracing their guards, state changes and failure paths. Two invariant rows were added: the balance conservation invariant holds across all four writers, and the README's EIP-712 domain and typehash constants match the code when recomputed with cast.\n\n**Static-analysis leads dismissed:** the arbitrary-send-eth lead targets a signed destination, the uninitialized-state lead is a push-only mapping of arrays, the strict-equality lead is intentional checkpoint coalescing, and the missing zero-check on the owner is enforced by the OpenZeppelin base constructor.\n\n**Not reached:** the invariant handler internals and the Deployment test were only skimmed, since they sit outside the Execution Trace, Periphery and First Principles guides. The existing 70-test suite passes.","treeHash":null,"usage":{"cachedInputTokens":1176828,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":30950,"runtime":"claude","turns":25,"wallClockMs":534704}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"672c29f92850efe0","findings":[{"citation":"resolved","description":"The 24-hour outflow cap is keyed only by token. remainingOutflow(token) is the same number for every recipientId, and _sweep stops a queue as soon as its head does not fit that shared remainder. Nothing limits how much of the budget one recipient may take, and with the deployed feeBps = 0 a claim to the depositor's own wallet returns the full amount. An unprivileged user who controls any handle (the authorizer attests it legitimately) deposits the full cap to their own recipientId, claims it back to themselves, and repeats at every window roll-over. Each cycle costs only gas; at the maximum fee of 100 bps it costs 1% of the cap per day. While it runs, claim() for every other recipient in that token reverts with OutflowCapExceeded (claim.sol L233-234), so the brief's guarantee that a recipient can never be made unclaimable does not hold, and the authorizer cannot help because the limit is on-chain. Refunds remain possible only after 180 days and go to the sender, not the recipient. The cap was specified to bound a compromised authorizer; the fix that preserves that design is a second limit per (recipientId, token) over the same window, or at minimum a per-recipient share of the cap, so a single handle cannot consume the whole global budget. Economics: cap 100,000 mUSDC, fee 0: attacker locks 100,000 mUSDC of capital for one block per day and spends ~2 transactions of gas; every other recipient's mUSDC claims fail indefinitely.","line":456,"path":"src/HandleVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HandleVault} from \"src/HandleVault.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\n\n/// @notice Shows that the per-token rolling cap is a single shared budget that any unprivileged\n/// user can consume for free (fee 0), and that a cap-sized deposit blocks a victim's FIFO queue.\ncontract CapGriefingTest is Test {\n    uint256 internal constant AUTHORIZER_KEY = 0xA11CE;\n    uint256 internal constant CAP = 100_000e6;\n    bytes32 internal constant VICTIM = keccak256(\"victim handle\");\n    bytes32 internal constant ATTACKER_HANDLE = keccak256(\"attacker handle\");\n    bytes32 internal constant BYSTANDER = keccak256(\"bystander handle\");\n\n    address internal owner;\n    address internal attacker = makeAddr(\"attacker\");\n    address internal friend = makeAddr(\"friend\");\n    address internal victimWallet = makeAddr(\"victim wallet\");\n    address internal bystanderWallet = makeAddr(\"bystander wallet\");\n    MockUSDC internal usdc;\n    HandleVault internal vault;\n\n    function setUp() public {\n        vm.chainId(11_155_111);\n        vm.warp(10 days);\n        owner = vm.addr(AUTHORIZER_KEY);\n        usdc = new MockUSDC(owner);\n        // Deploy-script values: 1 mUSDC minimum, 100,000 mUSDC rolling cap, fee 0.\n        vault = new HandleVault(owner, address(usdc), 0.0001 ether, 1e6, 10 ether, CAP);\n        vm.startPrank(owner);\n        usdc.mint(attacker, CAP);\n        usdc.mint(friend, 1_000e6);\n        vm.stopPrank();\n    }\n\n    function _deposit(address from, bytes32 id, uint256 amount) internal returns (uint256) {\n        vm.startPrank(from);\n        usdc.approve(address(vault), amount);\n        uint256 depositId = vault.depositFor(id, address(usdc), amount);\n        vm.stopPrank();\n        return depositId;\n    }\n\n    function _sig(bytes32 id, address dest, uint256 nonce, uint256 deadline) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(AUTHORIZER_KEY, vault.claimDigest(id, dest, nonce, deadline));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _claim(bytes32 id, address dest, uint256 nonce) internal returns (uint256) {\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(usdc);\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        return vault.claim(id, dest, tokens, nonce, deadline, _sig(id, dest, nonce, deadline));\n    }\n\n    /// Finding 1: the attacker cycles the full cap through their own legitimately attested handle\n    /// every 24 hours. With feeBps = 0 the attacker ends each cycle with the same balance, so the\n    /// only cost is gas. Every other recipient's claim in that token reverts for as long as it runs.\n    function test_unprivilegedUserExhaustsSharedCapForFree() public {\n        _deposit(friend, VICTIM, 5e6);\n        for (uint256 day; day < 3; ++day) {\n            uint256 before = usdc.balanceOf(attacker);\n            _deposit(attacker, ATTACKER_HANDLE, CAP);\n            assertEq(_claim(ATTACKER_HANDLE, attacker, day), 1);\n            assertEq(usdc.balanceOf(attacker), before, \"cycle is free at fee 0\");\n            assertEq(vault.remainingOutflow(address(usdc)), 0);\n            vm.warp(vm.getBlockTimestamp() + 24 hours);\n        }\n        // Attacker refreshes right at window roll-over, before the victim's relayer lands.\n        _deposit(attacker, ATTACKER_HANDLE, CAP);\n        _claim(ATTACKER_HANDLE, attacker, 99);\n        // Expected: the victim's 5 mUSDC deposit is claimable. Actual: OutflowCapExceeded.\n        assertEq(_claim(VICTIM, victimWallet, 0), 1, \"victim should be able to claim her 5 mUSDC\");\n    }\n\n    /// Finding 2: a single deposit of exactly the cap, which depositFor accepts, sits in the victim's\n    /// FIFO queue and blocks every later deposit for that token whenever any other claim at all has\n    /// happened in the last 24 hours. The attacker gets the full amount back after 180 days.\n    function test_capSizedDepositBlocksEveryLaterDepositBehindIt() public {\n        uint256 spamId = _deposit(attacker, VICTIM, CAP);\n        uint256 realId = _deposit(friend, VICTIM, 5e6);\n        // Ordinary unrelated traffic: a bystander claims 1 mUSDC somewhere in the window.\n        _deposit(friend, BYSTANDER, 1e6);\n        _claim(BYSTANDER, bystanderWallet, 0);\n        assertEq(vault.remainingOutflow(address(usdc)), CAP - 1e6);\n\n        // Expected: the victim can at least sweep the 5 mUSDC that fits. Actual: whole queue blocked.\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(usdc);\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        bytes memory sig = _sig(VICTIM, victimWallet, 0, deadline);\n        (bool ok,) = address(vault).call(\n            abi.encodeCall(HandleVault.claim, (VICTIM, victimWallet, tokens, 0, deadline, sig))\n        );\n        assertTrue(ok, \"victim claim reverted with OutflowCapExceeded although 5 mUSDC fits\");\n        (,,,,, HandleVault.Status realStatus) = vault.deposits(realId);\n        assertEq(uint8(realStatus), uint8(HandleVault.Status.Claimed));\n\n        // The spammer recovers its capital in full after the refund delay.\n        vm.warp(vm.getBlockTimestamp() + 180 days);\n        vault.refund(spamId);\n        assertEq(usdc.balanceOf(attacker), CAP);\n    }\n}","reproduction":"Deploy with the script values (mUSDC cap 100_000e6, fee 0). 1) friend.depositFor(VICTIM, usdc, 5e6). 2) attacker.depositFor(ATTACKER_HANDLE, usdc, 100_000e6); claim(ATTACKER_HANDLE, attacker, [usdc], nonce, deadline, authorizerSig) -> attacker balance unchanged, remainingOutflow(usdc) == 0. 3) claim(VICTIM, victimWallet, [usdc], 0, deadline, authorizerSig) -> expected: 1 deposit swept and 5e6 paid; actual: revert OutflowCapExceeded. 4) warp +24h, attacker repeats step 2 first in the new window; step 3 reverts again, for as many days as the attacker likes. Run: forge test --match-path test/scratch/CapGriefing.t.sol --match-test test_unprivilegedUserExhaustsSharedCapForFree","severity":"medium","snippet":"        accounting.available = remainingOutflow(token);","title":"Per-token rolling cap is one shared budget any attested user can exhaust for free, blocking every other recipient's claims"},{"citation":"resolved","description":"_checkDeposit only rejects amount > outflowCaps[token] (L384), so a deposit of exactly the cap is accepted. _sweep is strictly FIFO and breaks at the first deposit that does not fit the remaining window; deposits behind it are never examined and the relayer cannot skip it. A deposit equal to the cap fits only in a window where the token's total outflow across all recipients is zero. Any other claim of even 1 base unit in the preceding 24 hours therefore makes the victim's whole mUSDC queue, including every smaller deposit queued after the spam deposit, revert with OutflowCapExceeded. Combined with finding 1 the attacker can keep the window non-empty by themselves; in normal traffic the bystanders do it for free. The attacker's capital is not lost: refund(spamId) after 180 days returns it in full, and the only cost is the opportunity cost of locking the cap amount. The README and docs/SECURITY_REVIEW.md describe this head-of-line block only as a consequence of an admin cap reduction, not as something an unprivileged depositor can cause. Fix options that keep the design: let _sweep skip (not stop at) a deposit that does not fit and continue with the next one, or bound single deposits to a fraction of the cap (for example cap / 4) so a deposit always fits a partly used window. Economics: cost to attacker = 100,000 mUSDC locked for 180 days then refunded; victim cannot receive any mUSDC sent after the spam deposit for the same 180 days unless a full day passes with zero mUSDC claims protocol-wide.","line":460,"path":"src/HandleVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HandleVault} from \"src/HandleVault.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\n\n/// @notice Shows that the per-token rolling cap is a single shared budget that any unprivileged\n/// user can consume for free (fee 0), and that a cap-sized deposit blocks a victim's FIFO queue.\ncontract CapGriefingTest is Test {\n    uint256 internal constant AUTHORIZER_KEY = 0xA11CE;\n    uint256 internal constant CAP = 100_000e6;\n    bytes32 internal constant VICTIM = keccak256(\"victim handle\");\n    bytes32 internal constant ATTACKER_HANDLE = keccak256(\"attacker handle\");\n    bytes32 internal constant BYSTANDER = keccak256(\"bystander handle\");\n\n    address internal owner;\n    address internal attacker = makeAddr(\"attacker\");\n    address internal friend = makeAddr(\"friend\");\n    address internal victimWallet = makeAddr(\"victim wallet\");\n    address internal bystanderWallet = makeAddr(\"bystander wallet\");\n    MockUSDC internal usdc;\n    HandleVault internal vault;\n\n    function setUp() public {\n        vm.chainId(11_155_111);\n        vm.warp(10 days);\n        owner = vm.addr(AUTHORIZER_KEY);\n        usdc = new MockUSDC(owner);\n        // Deploy-script values: 1 mUSDC minimum, 100,000 mUSDC rolling cap, fee 0.\n        vault = new HandleVault(owner, address(usdc), 0.0001 ether, 1e6, 10 ether, CAP);\n        vm.startPrank(owner);\n        usdc.mint(attacker, CAP);\n        usdc.mint(friend, 1_000e6);\n        vm.stopPrank();\n    }\n\n    function _deposit(address from, bytes32 id, uint256 amount) internal returns (uint256) {\n        vm.startPrank(from);\n        usdc.approve(address(vault), amount);\n        uint256 depositId = vault.depositFor(id, address(usdc), amount);\n        vm.stopPrank();\n        return depositId;\n    }\n\n    function _sig(bytes32 id, address dest, uint256 nonce, uint256 deadline) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(AUTHORIZER_KEY, vault.claimDigest(id, dest, nonce, deadline));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _claim(bytes32 id, address dest, uint256 nonce) internal returns (uint256) {\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(usdc);\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        return vault.claim(id, dest, tokens, nonce, deadline, _sig(id, dest, nonce, deadline));\n    }\n\n    /// Finding 1: the attacker cycles the full cap through their own legitimately attested handle\n    /// every 24 hours. With feeBps = 0 the attacker ends each cycle with the same balance, so the\n    /// only cost is gas. Every other recipient's claim in that token reverts for as long as it runs.\n    function test_unprivilegedUserExhaustsSharedCapForFree() public {\n        _deposit(friend, VICTIM, 5e6);\n        for (uint256 day; day < 3; ++day) {\n            uint256 before = usdc.balanceOf(attacker);\n            _deposit(attacker, ATTACKER_HANDLE, CAP);\n            assertEq(_claim(ATTACKER_HANDLE, attacker, day), 1);\n            assertEq(usdc.balanceOf(attacker), before, \"cycle is free at fee 0\");\n            assertEq(vault.remainingOutflow(address(usdc)), 0);\n            vm.warp(vm.getBlockTimestamp() + 24 hours);\n        }\n        // Attacker refreshes right at window roll-over, before the victim's relayer lands.\n        _deposit(attacker, ATTACKER_HANDLE, CAP);\n        _claim(ATTACKER_HANDLE, attacker, 99);\n        // Expected: the victim's 5 mUSDC deposit is claimable. Actual: OutflowCapExceeded.\n        assertEq(_claim(VICTIM, victimWallet, 0), 1, \"victim should be able to claim her 5 mUSDC\");\n    }\n\n    /// Finding 2: a single deposit of exactly the cap, which depositFor accepts, sits in the victim's\n    /// FIFO queue and blocks every later deposit for that token whenever any other claim at all has\n    /// happened in the last 24 hours. The attacker gets the full amount back after 180 days.\n    function test_capSizedDepositBlocksEveryLaterDepositBehindIt() public {\n        uint256 spamId = _deposit(attacker, VICTIM, CAP);\n        uint256 realId = _deposit(friend, VICTIM, 5e6);\n        // Ordinary unrelated traffic: a bystander claims 1 mUSDC somewhere in the window.\n        _deposit(friend, BYSTANDER, 1e6);\n        _claim(BYSTANDER, bystanderWallet, 0);\n        assertEq(vault.remainingOutflow(address(usdc)), CAP - 1e6);\n\n        // Expected: the victim can at least sweep the 5 mUSDC that fits. Actual: whole queue blocked.\n        address[] memory tokens = new address[](1);\n        tokens[0] = address(usdc);\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        bytes memory sig = _sig(VICTIM, victimWallet, 0, deadline);\n        (bool ok,) = address(vault).call(\n            abi.encodeCall(HandleVault.claim, (VICTIM, victimWallet, tokens, 0, deadline, sig))\n        );\n        assertTrue(ok, \"victim claim reverted with OutflowCapExceeded although 5 mUSDC fits\");\n        (,,,,, HandleVault.Status realStatus) = vault.deposits(realId);\n        assertEq(uint8(realStatus), uint8(HandleVault.Status.Claimed));\n\n        // The spammer recovers its capital in full after the refund delay.\n        vm.warp(vm.getBlockTimestamp() + 180 days);\n        vault.refund(spamId);\n        assertEq(usdc.balanceOf(attacker), CAP);\n    }\n}","reproduction":"Deploy with the script values (mUSDC cap 100_000e6). 1) attacker.depositFor(VICTIM, usdc, 100_000e6) -> accepted (amount == cap). 2) friend.depositFor(VICTIM, usdc, 5e6). 3) any bystander: depositFor(BYSTANDER, usdc, 1e6) then claim(BYSTANDER, ...) -> remainingOutflow(usdc) == 99_999e6. 4) claim(VICTIM, victimWallet, [usdc], 0, deadline, authorizerSig) -> expected: at least the 5e6 deposit is swept (it fits); actual: revert OutflowCapExceeded because the 100_000e6 head does not fit and the loop breaks. 5) warp +180 days; refund(spamId) returns 100_000e6 to the attacker. Run: forge test --match-path test/scratch/CapGriefing.t.sol --match-test test_capSizedDepositBlocksEveryLaterDepositBehindIt","severity":"medium","snippet":"            if (d.amount > accounting.available - accounting.gross) {\n                blocked = true;\n                break;\n            }","title":"A single cap-sized deposit to a victim's handle blocks every later deposit in that token's FIFO queue"},{"citation":"resolved","description":"The signed Claim type is (recipientId, dest, nonce, deadline). tokens[] is unsigned and the caller may be anyone, so whoever sees the relayer's pending claim can resubmit the same signature with a different, smaller token list. As long as one deposit is swept the nonce is consumed (L221) and the remaining tokens stay open; the claimant must obtain a fresh signature, which can be front-run again the same way. Funds always reach the signed dest, so nothing is stolen, but a griefer pays only gas and can repeat this for every signature issued through a public mempool. The README calls the unsigned subset deliberate, yet the brief's reason for allowing anyone to relay is gas sponsorship, not letting third parties choose what a signature executes. A minimal fix that keeps relaying open is to add keccak256(abi.encodePacked(tokens)) to the Claim struct (new typehash) or to let the signer optionally bind a relayer address.","line":221,"path":"src/HandleVault.sol","reproduction":"State: RECIPIENT has open deposits 5e6 mUSDC and 1 unit of `other`. Authorizer signs (RECIPIENT, dest, nonce 0, deadline). The claimant's relayer broadcasts claim(RECIPIENT, dest, [usdc, other], 0, ...). Attacker copies the signature and lands claim(RECIPIENT, dest, [other], 0, deadline, sig) first: returns 1, usedNonces(RECIPIENT, 0) == true, totalEscrowed(RECIPIENT, usdc) still 5e6. The relayer's transaction then reverts NonceAlreadyUsed. Expected: the signature executes the claimant's intended sweep or nothing; actual: a third party chose the subset and consumed the nonce. The repository test test_tokenSubsetIsDeliberatelyUnsigned (test/Signatures.t.sol L185) executes exactly this sequence from the relayer's address and asserts it succeeds.","severity":"low","snippet":"        usedNonces[recipientId][nonce] = true;","title":"Claim signature does not bind the token list, so any mempool observer can burn a claimant's nonce with a one-token subset"}],"hash":"b2d44c387e646567547fe1e5a877b88e1afb317bde407750be4c4643ba8d43f1","nodeId":"9d5324c9-bce2-4687-98ef-a201b6dacebf","outcome":"completed","summary":"All three snippets verify against the tree, the JSON parses, and the only files I touched are the ignored findings file and the scratch test. The review is complete.\n\n**Outcome:** three substantiated findings in the economics area, written to `.imd-findings.json` with a 28-row coverage record (all 25 entry points plus three invariants). No tracked file was changed. The shipped suite of 70 tests passes as-is.\n\n**Findings**\n\n1. **Medium. The rolling cap is one shared budget per token.** Any user who controls a handle can deposit the full cap to their own recipientId, claim it back to themselves at fee 0, and repeat at each 24-hour roll-over. Every other recipient's claim in that token reverts with OutflowCapExceeded for as long as it runs. Cost is gas only, or 1% of the cap per day at the maximum fee.\n2. **Medium. A cap-sized deposit blocks a victim's FIFO queue.** The deposit check accepts amount equal to the cap, and the sweep stops at the first deposit that does not fit rather than skipping it. One such deposit to a victim's handle makes every later deposit in that token unclaimable whenever anyone has claimed even one unit in the last 24 hours. The attacker recovers the capital in full by refund after 180 days. The docs only describe this block as a consequence of an admin cap reduction.\n3. **Low. The signature does not bind the token list.** Anyone watching the mempool can resubmit a claimant's signature with a one-token subset, consuming the nonce and leaving the rest open, for gas cost alone. The README calls this deliberate, so it is reported as a design concern rather than a bug.\n\n**Proof:** `test/scratch/CapGriefing.t.sol` holds one failing test per medium finding. Both fail on the current code for the stated reason, and the file is embedded as the proof for findings 1 and 2.\n\n**Covered and holding:** fee math and rounding, fee never applied to refunds, the balance-equals-open-plus-fees conservation law on every writer, the exact sliding window including checkpoint coalescing and counter wrap, refund timing and status transitions, pause semantics, checks-effects-interactions ordering, and fee-on-transfer rejection via balance deltas. Deployment manifest content and the Sepolia addresses in the README are outside the economics area and were not verified.","treeHash":null,"usage":{"cachedInputTokens":1538717,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":28836,"runtime":"claude","turns":30,"wallClockMs":536752}},{"artifacts":[],"attempt":1,"bundleHash":"dd7223b4714a7f2c4ba26bbed89b1483b49c831cfc29d9b0e687bb1f1542f49c","device":"42a6bb6aa496cd6b","findings":[{"description":"The EIP-712 Claim type binds recipientId, dest, nonce and deadline but not the tokens array (the brief specifies exactly this type, and the NatSpec documents the choice). Anyone who sees a pending claim can front-run it with the same signature and a smaller token list. The nonce is then burned and the original transaction reverts with NonceAlreadyUsed. Funds always go to the signed dest, and every accepted call pays at least one deposit, so progress is guaranteed and nothing is lost; the cost is that the claimant must obtain a fresh signature. This is a documented design decision matching the requested type, reported for the record rather than as a defect. If the operator wants to close it, a v2 type that hashes the token list (keccak256(abi.encodePacked(tokens))) would do so without changing the relaying model.","line":206,"path":"src/HandleVault.sol","reproduction":"Recipient R has open deposits in USDC and ETH. Authorizer signs Claim(R, dest, nonce 0, deadline). Relayer submits claim(R, dest, [USDC, ETH], 0, deadline, sig). Attacker front-runs with claim(R, dest, [ETH], 0, deadline, sig): succeeds, pays ETH to dest, marks nonce 0 used. Relayer's transaction reverts NonceAlreadyUsed; USDC stays open until a new signature with nonce 1 is issued. Pinned by test/Signatures.t.sol::test_tokenSubsetIsDeliberatelyUnsigned and test/HandleVaultEdges.t.sol::test_sweepBudgetStopsExactlyAtSixtyFourAcrossTokenBoundary.","severity":"info","title":"Token list is not covered by the Claim signature, so a mempool observer can consume a signature with a subset of tokens"},{"description":"_sweep stops a token queue at the first deposit whose amount exceeds the remaining 24-hour allowance, even when a later, smaller deposit would fit. This preserves strict FIFO and keeps claims O(swept), and the NatSpec states it. It cannot be exploited to lock funds: deposits larger than the cap are rejected at deposit time, so the head always fits once the window rolls over, and refunds are unaffected. The only way to create a permanently blocked head is for the admin to lower the cap below an already open deposit, which is a documented trusted power. Reported so the operator understands that lowering a cap can delay existing claims rather than only new ones.","line":460,"path":"src/HandleVault.sol","reproduction":"Cap 1000e6. Claim 900e6 at t0 (remaining 100e6). Deposit 500e6 then 50e6 for the same recipient. claim at t0 reverts OutflowCapExceeded although 50e6 would fit; at t0 + 24h both are swept. Pinned by test/HandleVaultEdges.t.sol::test_capBlockingIsHeadOfLineFifo and test_adminLoweringCapBelowOpenDepositBlocksClaimButNotRefund.","severity":"info","title":"Rolling-cap enforcement is head-of-line FIFO: one deposit that does not fit the remaining window blocks smaller later deposits of the same token"}],"hash":"c6e91ef775d84446f75dc8d241f2c09a9153a4562d914337686ff83686d9296b","nodeId":"107c7f74-424d-46a6-a509-b9061b388086","outcome":"completed","summary":"Wrapping up: the mutation check confirmed the adversary suite catches a removed authorizer check, and the findings file is written. Final state is verified.\n\n**Outcome:** the full suite passes with the verifier's fuzz seed, and the implementation holds up under adversarial review. I found no defect that needed to be reported as a bug; two documented design decisions are recorded as info-level notes.\n\n| Check | Result |\n| --- | --- |\n| `forge build` | clean |\n| `forge test` with `FOUNDRY_FUZZ_SEED=0x98cd…3880` | 99 passed, 0 failed, 0 skipped (7 suites) |\n| Paths changed | `test/HandleVaultEdges.t.sol`, `test/HandleVaultSecurity.invariant.t.sol`, `.imd-findings.json` |\n\n**What I added on top of the existing 70-test suite**\n\n- `test/HandleVaultEdges.t.sol` (28 tests). Boundary and event coverage the base suite did not pin: constructor and admin setter events, per-deposit `Claimed` events in FIFO order plus the summary event, a 16-token by 64-deposit maximal claim with a block-gas bound, the sweep budget stopping exactly at 64 across a token boundary, deposit equal to the cap, a claim sweeping a deposit exactly equal to the remaining cap, head-of-line FIFO blocking, admin cap lowering blocking claims but never refunds, ERC-20 refund under pause plus delist plus cap zero, authorizer and pauser having no admin powers, authorizer self-renounce invalidating signatures, admin unable to extract principal via fee settings, compact 64-byte signatures rejected, and zero recipient id and max nonce as ordinary values.\n- Ten property tests in the same file, each with an independent expected value: fee sums over eight deposits with per-deposit rounding, FIFO sweep up to the first deposit that breaks the cap, the window dropping a claim at exactly 24 hours, a naive-sum oracle over up to 24 checkpoints queried at eight later times, refund timing on both sides of the delay, deadline boundary over random timestamps, any random key without the role rejected, every signed field binding, pagination visiting every open deposit once in order with random closes, and minimum boundaries.\n- `test/HandleVaultSecurity.invariant.t.sol`. An unprivileged adversary drives the vault with forged keys, replayed and redirected and re-scoped issued signatures, random refunds, admin calls, plain ether sends and its own deposits, with every revert checked against the exact expected error. Invariants: the adversary never profits, vault balance equals open deposits plus accrued fees, used nonces stay used and unused stay unused, roles and configuration are untouched, and rolling outflow never exceeds the cap. All adversarial paths are seeded in setUp so the counters checked in afterInvariant cannot be vacuous. A scratch mutant of the vault with the authorizer role check removed fails this suite immediately, so it detects what it is meant to detect.\n\n**Findings file.** `.imd-findings.json` holds two info-level notes, both documented in the NatSpec and pinned by tests rather than asserted as bugs: the token list is outside the signed Claim type, so a mempool observer can burn a signature with a subset (progress is still guaranteed and funds only ever go to the signed dest), and cap enforcement is head-of-line FIFO, so lowering a cap below an open deposit delays that queue until raised while refunds remain available.\n\nNothing was installed, no configuration files were touched, and the scratch mutant lives only under `test/scratch/`.","treeHash":"5702e4ef51708dbb9a95345e82dc79394db6b924","usage":{"cachedInputTokens":1820487,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":53499,"runtime":"claude","turns":37,"wallClockMs":829481}},{"artifacts":[],"attempt":1,"bundleHash":"fe115bee57cce847b9c24ae07f11e86f85992595cb97befc1d39705e68c7e28e","device":"99c6d0bcc495ad61","findings":[],"hash":"f435ce5e1619011e57945f2c66a9b8d38fa33611a4cc721b92b2b3632ac89802","nodeId":"107c7f74-424d-46a6-a509-b9061b388086","outcome":"completed","summary":"All 115 tests pass with the verifier's seed, and the only on-disk change after my edits was the formatter I ran myself. Nothing else is needed.\n\n**Outcome.** The suite is green again. Two of my previously accepted tests had started failing because the other contributor changed the vault's cap handling from stop-at-head to skip-and-continue and added a per-recipient share of each cap. I revised those two tests and extended coverage to the new behaviour. No implementation defect surfaced, so there is no findings file.\n\n**What changed in test/HandleVaultEdges.t.sol**\n- The head-of-line blocking test now pins the skip semantics: an oversized head emits DepositSkipped, the smaller deposit behind it is paid in the same call, a retry with nothing fitting reverts without burning the nonce, and after the window clears the head is paid first.\n- The FIFO fuzz test now models the sweep independently as \"pay each deposit that fits what is left, skip the rest\", and checks statuses, the first open id, payout and rolling outflow against that model over six deposits.\n- A new fuzz test runs the same model with the recipient share active and a second recipient using part of the shared window first. It checks that capacity is the lesser of the two windows, that a claim with nothing fitting reverts cleanly, and that the second recipient's window is untouched.\n- A new unit test pins the visit budget from the passing side: 255 unfit deposits ahead of one that fits still gets it paid, and the next fitting one is reached on the following call.\n- The constructor event test and the role-separation test now cover the recipient share setter and its event.\n\n**What changed in test/HandleVaultSecurity.invariant.t.sol**\n- The attacker's denied-admin-call handler now also tries the recipient share setter.\n- The configuration invariant pins the share at its default, and the outflow invariant checks every recipient's rolling window against the recipient cap and the shared window.\n\nVerification with the verifier's fuzz seed:\n\n| Check | Result |\n| --- | --- |\n| forge build | Compiler run successful |\n| forge test | 115 passed, 0 failed |\n\nThe working tree has the two modified test files uncommitted, as I was not asked to commit.","treeHash":"87f8912b872bf3c219a30e41606c60a4446dfa50","usage":{"cachedInputTokens":812634,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":17997,"runtime":"claude","turns":25,"wallClockMs":401788}}],"verification":[{"checks":[{"durationMs":32564,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 32.48s\nCompiler run successful!\n","passed":true},{"durationMs":6893,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_constructorDoesNotCallUndeployedToken() (gas: 70423)\n[PASS] test_constructorRejectsInvalidConfiguration() (gas: 6264)\n[PASS] test_deploymentConfigurationUsesExplicitOwnerNotCaller() (gas: 17917749)\n[PASS] test_mockMintRestrictedAndOwnershipTwoStep() (gas: 301451)\n[PASS] test_scriptRunIsSepoliaOnlyAndNeedsNoEnvironment() (gas: 8524695)\nLogs:\n  Simulated MockUSDC (not a live deployment): 0x104fBc016F4bb334D775a19E8A6510109AC63E00\n  Simulated HandleVault (not a live deployment): 0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3\n\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 76.60ms (20.61ms CPU time)\n\nRan 34 tests for test/HandleVault.t.sol:HandleVaultTest\n[PASS] testFuzz_amountsAndFeesConserveFunds(uint96,uint16) (runs: 512, μ: 963885, ~: 973744)\nLogs:\n  Bound result 99999000015\n  Bound result 50\n\n[PASS] testFuzz_nativeAmountRefundedInFull(uint96,uint32) (runs: 512, μ: 462801, ~: 462868)\nLogs:\n  Bound result 9999900000000001775\n\n[PASS] test_adminHandoverCancellationAndNoBypassOrRenounce() (gas: 220168)\n[PASS] test_adminOnlySettersAndRoleManagement() (gas: 188402)\n[PASS] test_allowlistAndMinimumsAndMaxDeposit() (gas: 1752139)\n[PASS] test_capBlockedTokenDoesNotBlockAnotherToken() (gas: 1242387)\n[PASS] test_capCountsAllRecipientsAndFeesAndSameTimestampClaims() (gas: 1619996)\n[PASS] test_capIsRollingNotCalendarBucketAndCannotBeResetByConfig() (gas: 1705123)\n[PASS] test_capPartialSweepAndRollover() (gas: 1572222)\n[PASS] test_configurationValidationAndNoCodeToken() (gas: 232968)\n[PASS] test_depositRecordEventAndTotals() (gas: 1325548)\n[PASS] test_feeMaxRoundedPerDepositAndWithdrawOnlyFees() (gas: 1872538)\n[PASS] test_feeRejectsInvalidParameters() (gas: 91154)\n[PASS] test_globalSweepLimitAcrossTokens() (gas: 32390058)\n[PASS] test_initialConfiguration() (gas: 106178)\n[PASS] test_invalidPagination() (gas: 795855)\n[PASS] test_lifetimeOutflowWrapDoesNotDisableClaims() (gas: 2360513)\n[PASS] test_matureDepositsStillClaimableUntilRefunded() (gas: 734549)\n[PASS] test_maxUintAmountFeeDoesNotOverflow() (gas: 1034891)\n[PASS] test_multiTokenAndNativeClaim() (gas: 2414586)\n[PASS] test_nativeCapEnforcement() (gas: 1296968)\n[PASS] test_nativeDirectTransferRejected() (gas: 37000)\n[PASS] test_nativeFeesAndRecipientChange() (gas: 820664)\n[PASS] test_paginationAndConstantTimeMiddleRemoval() (gas: 3745571)\n[PASS] test_partialSweepContinuationIsFIFOAndBounded() (gas: 29711068)\n[PASS] test_pauseSemanticsAndUnpause() (gas: 1442074)\n[PASS] test_pauserRoleSeparatedFromAuthorizer() (gas: 213320)\n[PASS] test_refundClaimedOrUnknownRejected() (gas: 823986)\n[PASS] test_refundExactDelayToOriginalSenderByAnyone() (gas: 709392)\n[PASS] test_refundNativeNoFeeWhilePausedDisabledAndCapZero() (gas: 636654)\n[PASS] test_relayClaimEventsAndZeroFees() (gas: 846914)\n[PASS] test_rollingCapAtTimestampZero() (gas: 716653)\n[PASS] test_twoStepDelayedAdminTransfer() (gas: 315464)\n[PASS] test_unlistedTokensRemainOpen() (gas: 1205970)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 76.65ms (102.24ms CPU time)\n\nRan 14 tests for test/Signatures.t.sol:SignaturesTest\n[PASS] testFuzz_digestMatchesReference(bytes32,address,uint256,uint256) (runs: 512, μ: 10898, ~: 10898)\n[PASS] test_authorizerRotationRevokesOutstandingSignatures() (gas: 912692)\n[PASS] test_badDestinationAndTokenListsRejected() (gas: 205832)\n[PASS] test_chainIdSeparatesSignaturesAndDomainUpdates() (gas: 89791)\n[PASS] test_digestMatchesIndependentReferenceAndTypehash() (gas: 41645)\n[PASS] test_emptyClaimDoesNotBurnNonce() (gas: 378587)\n[PASS] test_expiredRejectedAndExactDeadlineAccepted() (gas: 320816)\n[PASS] test_malformedMalleableAndInvalidVSignaturesRejected() (gas: 173553)\n[PASS] test_nonAuthorizerCannotClaim() (gas: 77214)\n[PASS] test_nonceScopedToRecipientButSharedByAuthorizers() (gas: 1486091)\n[PASS] test_otherVaultSignatureRejected() (gas: 62486)\n[PASS] test_replayRejectedEvenIfNewDepositsArrive() (gas: 773223)\n[PASS] test_tokenSubsetIsDeliberatelyUnsigned() (gas: 761181)\n[PASS] test_wrongRecipientDestinationNonceOrDeadlineRejected() (gas: 606436)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 78.67ms (87.84ms CPU time)\n\nRan 16 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test_depositTokenCallbackCannotReenter() (gas: 687473)\n[PASS] test_failedNativeTransferRollsBackEarlierTokenPayoutAndFees() (gas: 2707311)\n[PASS] test_failedRefundKeepsOriginalRecordAndCanRetry() (gas: 711438)\n[PASS] test_failedTokenClaimRollsBackEveryEffectThenCanRetry() (gas: 1183983)\n[PASS] test_failedTokenRefundKeepsRecord() (gas: 768193)\n[PASS] test_feeOnTransferRejectedWithoutCreatingLiabilities() (gas: 241853)\n[PASS] test_feeWithdrawalCannotReenter() (gas: 933019)\n[PASS] test_missingAllowanceRevertsWithoutDepositRecord() (gas: 79109)\n[PASS] test_nativePayoutCannotReenterClaimAndSeesSettledAccounting() (gas: 1028014)\n[PASS] test_nativeRefundCannotReenterRefund() (gas: 639405)\n[PASS] test_rejectingFeeRecipientDoesNotBlockClaimsAndWithdrawalCanRecover() (gas: 940957)\n[PASS] test_safeERC20RejectsFalseReturnOnDeposit() (gas: 173528)\n[PASS] test_safeERC20SupportsNoReturnTokens() (gas: 1586750)\n[PASS] test_tokenEnablingTaxAfterDepositCannotUnderpayClaimOrRefund() (gas: 1168593)\n[PASS] test_tokenPayoutCallbackCannotReenterRefund() (gas: 965670)\n[PASS] test_unsolicitedTransfersAreUnaccountedSurplusNotWithdrawableFees() (gas: 1477273)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 83.81ms (10.76ms CPU time)\n\nRan 14 tests for test/CapSharing.t.sol:CapSharingTest\n[PASS] testFuzz_recipientNeverExceedsShareOrSharedCapInOneWindow(uint16,uint96[8]) (runs: 512, μ: 3660822, ~: 3644056)\nLogs:\n  Bound result 887\n  Bound result 71722779\n  Bound result 87700007\n  Bound result 53186534\n  Bound result 87700043\n  Bound result 1000001\n  Bound result 66016146\n  Bound result 1000000\n  Bound result 57429121\n\n[PASS] test_defaultShareIsWholeCapAndViewsAgree() (gas: 66710)\n[PASS] test_depositsAreBoundedByRecipientCap() (gas: 915082)\n[PASS] test_loweredCapSkipsOversizedDepositUntilRaised() (gas: 1423483)\n[PASS] test_loweringShareBelowExistingDepositDelaysItUntilRaised() (gas: 2023038)\n[PASS] test_nothingFitsStillRevertsWithCapExceeded() (gas: 1648954)\n[PASS] test_oneRecipientCannotConsumeTheWholeWindowWhenShared() (gas: 2315024)\n[PASS] test_oversizedHeadIsSkippedAndLaterDepositsArePaid() (gas: 3175043)\n[PASS] test_recipientWindowIsExactlyTwentyFourHoursAndCountsFees() (gas: 1682302)\n[PASS] test_recipientWindowsAreIndependentPerToken() (gas: 1883136)\n[PASS] test_setRecipientShareValidationEventAndAccess() (gas: 151973)\n[PASS] test_skipKeepsFifoOrderAmongDepositsThatFit() (gas: 2091348)\n[PASS] test_visitBudgetBoundsSkippedDepositsAndContinues() (gas: 98165739)\n[PASS] test_visitBudgetIsSharedAcrossTokens() (gas: 98216868)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 83.99ms (164.04ms CPU time)\n\nRan 1 test for test/HandleVault.invariant.t.sol:HandleVaultInvariantTest\n[PASS]\nHandleVaultInvariantTest invariants:\n[PASS] invariant_balancesEqualOpenDepositsPlusAccruedFeesAndModel\n[PASS] invariant_openPaginationAndPerRecipientTotalsMatchModel\n HandleVaultInvariantTest invariants (runs: 128, calls: 12288, reverts: 0)\n\n╭--------------+---------------+-------+---------+----------╮\n| Contract     | Selector      | Calls | Reverts | Discards |\n+===========================================================+\n| VaultHandler | advanceTime   | 1332  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | claim         | 1359  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureCap  | 1343  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureFee  | 1362  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | deposit       | 1335  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | refund        | 1382  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | toggleAllowed | 1415  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | togglePause   | 1406  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | withdrawFees  | 1354  | 0       | 0        |\n╰--------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 100\n  Bound result 1000000000000000000\n  Bound result 10000000\n  Bound result 10000000\n  Bound result 7\n  Bound result 1000000000000000000\n  Bound result 1000000\n  Bound result 1000000\n  Bound result 0\n  Bound result 11971\n  Bound result 84\n  Bound result 13\n  Bound result 1\n  Bound result 37143117673\n  Bound result 33\n  Bound result 0\n  Bound result 86400\n  Bound result 97\n  Bound result 2500\n  Bound result 7552\n  Bound result 14\n  Bound result 17\n  Bound result 757\n  Bound result 11905\n  Bound result 7711\n  Bound result 699\n  Bound result 3714247998\n  Bound result 7377\n  Bound result 6\n  Bound result 15617\n  Bound result 466\n  Bound result 2804\n  Bound result 678829032\n  Bound result 1129\n  Bound result 13258\n  Bound result 3\n  Bound result 3843\n  Bound result 2\n  Bound result 86740\n  Bound result 3379\n  Bound result 10406\n  Bound result 10458\n  Bound result 13315\n  Bound result 86707\n  Bound result 1276\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.80s (6.80s CPU time)\n\nRan 6 test suites in 6.80s (7.20s CPU time): 84 tests passed, 0 failed, 0 skipped (84 total tests)\n","passed":true},{"durationMs":43,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HandleVault.acceptDefaultAdminTransfer()\",\"HandleVault.beginDefaultAdminTransfer(address)\",\"HandleVault.cancelDefaultAdminTransfer()\",\"HandleVault.changeDefaultAdminDelay(uint48)\",\"HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes)\",\"HandleVault.depositFor(bytes32,address,uint256)\",\"HandleVault.depositNativeFor(bytes32)\",\"HandleVault.grantRole(bytes32,address)\",\"HandleVault.pause()\",\"HandleVault.refund(uint256)\",\"HandleVault.renounceRole(bytes32,address)\",\"HandleVault.revokeRole(bytes32,address)\",\"HandleVault.rollbackDefaultAdminDelay()\",\"HandleVault.setFee(uint256,address)\",\"HandleVault.setOutflowCap(address,uint256)\",\"HandleVault.setRecipientShareBps(uint256)\",\"HandleVault.setTokenConfig(address,bool,uint256)\",\"HandleVault.unpause()\",\"HandleVault.withdrawFees(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.renounceOwnership()\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":4,\"README.md\":201,\"docs/SECURITY_REVIEW.md\":31,\"docs/VERIFICATION.md\":20,\"foundry.toml\":23,\"remappings.txt\":2,\"script/DeployHandleVault.s.sol\":34,\"src/HandleVault.sol\":589,\"src/MockUSDC.sol\":19,\"src/interfaces/IFutureHandleVault.sol\":19,\"test/Adversarial.t.sol\":268,\"test/CapSharing.t.sol\":318,\"test/Deployment.t.sol\":125,\"test/HandleVault.invariant.t.sol\":371,\"test/HandleVault.t.sol\":635,\"test/Signatures.t.sol\":194,\"test/helpers/Adversaries.sol\":119,\"test/helpers/VaultFixture.sol\":117},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4162,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/HandleVault.sol:573: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#573-588) sends eth to arbitrary user\n[high/high] uninitialized-state at src/HandleVault.sol:424: HandleVault._outflows (src/HandleVault.sol#109) is never initialized. It is used in:\n[medium/high] incorrect-equality at src/HandleVault.sol:558: HandleVault._recordOutflow(bytes32,uint256) (src/HandleVault.sol#558-571) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/HandleVault.sol:431: HandleVault._rolling(bytes32).low (src/HandleVault.sol#431) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HandleVault.sol:242: HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes).capBlocked (src/HandleVault.sol#242) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HandleVault.sol:243: HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes).visited (src/HandleVault.sol#243) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HandleVault.sol:529: HandleVault._sweep(bytes32,address,address,uint256,uint256,uint256).accounting (src/HandleVault.sol#529) is a local variable never initialized\n[medium/medium] unused-return at src/HandleVault.sol:224: HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes) (src/HandleVault.sol#224-267) ignores return value by (signer,err,None) = ECDSA.tryRecover(claimDigest(recipientId,dest,nonce,deadline),signature) (src/HandleVault.sol#236-237)\n[low/medium] missing-zero-check at src/HandleVault.sol:164: HandleVault.constructor(address,address,uint256,uint256,uint256,uint256).initialOwner (src/HandleVault.sol#164) lacks a zero-check on :\n[low/medium] calls-loop at src/HandleVault.sol:573: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#573-588) has external calls inside a loop: beforeVault = IERC20(token).balanceOf(address(this)) (src/HandleVault.sol#578)\n[low/medium] calls-loop at src/HandleVault.sol:573: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#573-588) has external calls inside a loop: beforeDest = IERC20(token).balanceOf(dest) (src/HandleVault.sol#579)\n[low/medium] calls-loop at src/HandleVault.sol:573: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#573-588) has external calls inside a loop: afterVault = IERC20(token).balanceOf(address(this)) (src/HandleVault.sol#581)\n[low/medium] calls-loop at src/HandleVault.sol:573: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#573-588) has external calls inside a loop: (success,None) = address(dest).call{value: amount}() (src/HandleVault.sol#575)\n[low/medium] calls-loop at src/HandleVault.sol:573: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#573-588) has external calls inside a loop: afterDest = IERC20(token).balanceOf(dest) (src/HandleVault.sol#582)\n[low/medium] timestamp at src/HandleVault.sol:558: HandleVault._recordOutflow(bytes32,uint256) (src/HandleVault.sol#558-571) uses timestamp for comparisons\n[low/medium] timestamp at src/HandleVault.sol:224: HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes) (src/HandleVault.sol#224-267) uses timestamp for comparisons\n[low/medium] timestamp at src/HandleVault.sol:270: HandleVault.refund(uint256) (src/HandleVault.sol#270-278) uses timestamp for comparisons\n[low/medium] timestamp at src/HandleVault.sol:362: HandleVault.getOpenDepositIds(bytes32,uint256,uint256) (src/HandleVault.sol#362-383) uses timestamp for comparisons\n[low/medium] timestamp at src/HandleVault.sol:424: HandleVault._rolling(bytes32) (src/HandleVault.sol#424-443) uses timestamp for comparisons","passed":true},{"durationMs":560,"exitCode":0,"name":"aderyn","output":"[low] centralization-risk at src/HandleVault.sol:293: Centralization Risk (9 places)\n[low] costly-loop at src/HandleVault.sol:245: Costly operations inside loop (2 places)\n[low] large-numeric-literal at src/HandleVault.sol:38: Large Numeric Literal (2 places)\n[low] require-revert-in-loop at src/HandleVault.sol:264: Loop Contains `require`/`revert` (3 places)\n[low] state-no-address-check at src/HandleVault.sol:176: Address State Variable Set Without Checks\n[low] unchecked-return at src/HandleVault.sol:177: Unchecked Return (2 places)\n[low] uninitialized-local-variable at src/HandleVault.sol:264: Uninitialized Local Variable (3 places)","passed":true},{"durationMs":4447,"exitCode":0,"name":"proof a51b15725592","output":"Compiling 55 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.88s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-f2714f19/Proof_a51b15725592.t.sol:HeadOfLineBlockProof\n[PASS] test_capSizedHeadDepositBlocksEveryLaterDepositForRecipient() (gas: 1996668)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.77ms (963.70µs CPU time)\n\nRan 1 test suite in 3.19ms (1.77ms 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":"0924085c01816bf398bdb0332271d8afad5fe0967f42bcb4b26b9cc797db02ff","verifiedTreeHash":"89dc8c4fae25f69c30220fc0737e8402c6ae6de9","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":28710,"exitCode":0,"name":"build","output":"Compiling 61 files with Solc 0.8.26\nSolc 0.8.26 finished in 28.62s\nCompiler run successful!\n","passed":true},{"durationMs":7338,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 16 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test_depositTokenCallbackCannotReenter() (gas: 684768)\n[PASS] test_failedNativeTransferRollsBackEarlierTokenPayoutAndFees() (gas: 2353325)\n[PASS] test_failedRefundKeepsOriginalRecordAndCanRetry() (gas: 708533)\n[PASS] test_failedTokenClaimRollsBackEveryEffectThenCanRetry() (gas: 1039848)\n[PASS] test_failedTokenRefundKeepsRecord() (gas: 765563)\n[PASS] test_feeOnTransferRejectedWithoutCreatingLiabilities() (gas: 239104)\n[PASS] test_feeWithdrawalCannotReenter() (gas: 859647)\n[PASS] test_missingAllowanceRevertsWithoutDepositRecord() (gas: 76470)\n[PASS] test_nativePayoutCannotReenterClaimAndSeesSettledAccounting() (gas: 954752)\n[PASS] test_nativeRefundCannotReenterRefund() (gas: 636773)\n[PASS] test_rejectingFeeRecipientDoesNotBlockClaimsAndWithdrawalCanRecover() (gas: 867585)\n[PASS] test_safeERC20RejectsFalseReturnOnDeposit() (gas: 170889)\n[PASS] test_safeERC20SupportsNoReturnTokens() (gas: 1510930)\n[PASS] test_tokenEnablingTaxAfterDepositCannotUnderpayClaimOrRefund() (gas: 1095091)\n[PASS] test_tokenPayoutCallbackCannotReenterRefund() (gas: 892467)\n[PASS] test_unsolicitedTransfersAreUnaccountedSurplusNotWithdrawableFees() (gas: 1331006)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 15.06ms (12.60ms CPU time)\n\nRan 14 tests for test/Signatures.t.sol:SignaturesTest\n[PASS] testFuzz_digestMatchesReference(bytes32,address,uint256,uint256) (runs: 512, μ: 10832, ~: 10832)\n[PASS] test_authorizerRotationRevokesOutstandingSignatures() (gas: 839181)\n[PASS] test_badDestinationAndTokenListsRejected() (gas: 205502)\n[PASS] test_chainIdSeparatesSignaturesAndDomainUpdates() (gas: 89307)\n[PASS] test_digestMatchesIndependentReferenceAndTypehash() (gas: 41227)\n[PASS] test_emptyClaimDoesNotBurnNonce() (gas: 302622)\n[PASS] test_expiredRejectedAndExactDeadlineAccepted() (gas: 250142)\n[PASS] test_malformedMalleableAndInvalidVSignaturesRejected() (gas: 173289)\n[PASS] test_nonAuthorizerCannotClaim() (gas: 77016)\n[PASS] test_nonceScopedToRecipientButSharedByAuthorizers() (gas: 1351889)\n[PASS] test_otherVaultSignatureRejected() (gas: 62420)\n[PASS] test_replayRejectedEvenIfNewDepositsArrive() (gas: 699998)\n[PASS] test_tokenSubsetIsDeliberatelyUnsigned() (gas: 688022)\n[PASS] test_wrongRecipientDestinationNonceOrDeadlineRejected() (gas: 603753)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 28.59ms (11.58ms CPU time)\n\nRan 34 tests for test/HandleVault.t.sol:HandleVaultTest\n[PASS] testFuzz_amountsAndFeesConserveFunds(uint96,uint16) (runs: 512, μ: 891074, ~: 900299)\nLogs:\n  Bound result 99999000015\n  Bound result 50\n\n[PASS] testFuzz_nativeAmountRefundedInFull(uint96,uint32) (runs: 512, μ: 460309, ~: 460377)\nLogs:\n  Bound result 9999900000000000015\n\n[PASS] test_adminHandoverCancellationAndNoBypassOrRenounce() (gas: 219376)\n[PASS] test_adminOnlySettersAndRoleManagement() (gas: 187654)\n[PASS] test_allowlistAndMinimumsAndMaxDeposit() (gas: 1600767)\n[PASS] test_capBlockedTokenDoesNotBlockAnotherToken() (gas: 1159209)\n[PASS] test_capCountsAllRecipientsAndFeesAndSameTimestampClaims() (gas: 1480278)\n[PASS] test_capIsRollingNotCalendarBucketAndCannotBeResetByConfig() (gas: 1573011)\n[PASS] test_capPartialSweepAndRollover() (gas: 1421146)\n[PASS] test_configurationValidationAndNoCodeToken() (gas: 230087)\n[PASS] test_depositRecordEventAndTotals() (gas: 1317521)\n[PASS] test_feeMaxRoundedPerDepositAndWithdrawOnlyFees() (gas: 1793881)\n[PASS] test_feeRejectsInvalidParameters() (gas: 90758)\n[PASS] test_globalSweepLimitAcrossTokens() (gas: 32060168)\n[PASS] test_initialConfiguration() (gas: 105034)\n[PASS] test_invalidPagination() (gas: 722168)\n[PASS] test_lifetimeOutflowWrapDoesNotDisableClaims() (gas: 2206603)\n[PASS] test_matureDepositsStillClaimableUntilRefunded() (gas: 661522)\n[PASS] test_maxUintAmountFeeDoesNotOverflow() (gas: 961260)\n[PASS] test_multiTokenAndNativeClaim() (gas: 2196820)\n[PASS] test_nativeCapEnforcement() (gas: 1143476)\n[PASS] test_nativeDirectTransferRejected() (gas: 37000)\n[PASS] test_nativeFeesAndRecipientChange() (gas: 747314)\n[PASS] test_paginationAndConstantTimeMiddleRemoval() (gas: 3600445)\n[PASS] test_partialSweepContinuationIsFIFOAndBounded() (gas: 29392992)\n[PASS] test_pauseSemanticsAndUnpause() (gas: 1366174)\n[PASS] test_pauserRoleSeparatedFromAuthorizer() (gas: 212484)\n[PASS] test_refundClaimedOrUnknownRejected() (gas: 750827)\n[PASS] test_refundExactDelayToOriginalSenderByAnyone() (gas: 706570)\n[PASS] test_refundNativeNoFeeWhilePausedDisabledAndCapZero() (gas: 633723)\n[PASS] test_relayClaimEventsAndZeroFees() (gas: 773293)\n[PASS] test_rollingCapAtTimestampZero() (gas: 664978)\n[PASS] test_twoStepDelayedAdminTransfer() (gas: 313946)\n[PASS] test_unlistedTokensRemainOpen() (gas: 1130260)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 34.43ms (105.26ms CPU time)\n\nRan 5 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_constructorDoesNotCallUndeployedToken() (gas: 67762)\n[PASS] test_constructorRejectsInvalidConfiguration() (gas: 6264)\n[PASS] test_deploymentConfigurationUsesExplicitOwnerNotCaller() (gas: 17305808)\n[PASS] test_mockMintRestrictedAndOwnershipTwoStep() (gas: 301451)\n[PASS] test_scriptRunIsSepoliaOnlyAndNeedsNoEnvironment() (gas: 8232329)\nLogs:\n  Simulated MockUSDC (not a live deployment): 0x104fBc016F4bb334D775a19E8A6510109AC63E00\n  Simulated HandleVault (not a live deployment): 0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3\n\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 35.69ms (37.05ms CPU time)\n\nRan 1 test for test/HandleVault.invariant.t.sol:HandleVaultInvariantTest\n[PASS]\nHandleVaultInvariantTest invariants:\n[PASS] invariant_balancesEqualOpenDepositsPlusAccruedFeesAndModel\n[PASS] invariant_openPaginationAndPerRecipientTotalsMatchModel\n HandleVaultInvariantTest invariants (runs: 128, calls: 12288, reverts: 0)\n\n╭--------------+---------------+-------+---------+----------╮\n| Contract     | Selector      | Calls | Reverts | Discards |\n+===========================================================+\n| VaultHandler | advanceTime   | 1332  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | claim         | 1359  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureCap  | 1343  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureFee  | 1362  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | deposit       | 1335  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | refund        | 1382  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | toggleAllowed | 1415  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | togglePause   | 1406  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | withdrawFees  | 1354  | 0       | 0        |\n╰--------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 100\n  Bound result 1000000000000000000\n  Bound result 10000000\n  Bound result 10000000\n  Bound result 7\n  Bound result 1000000000000000000\n  Bound result 1000000\n  Bound result 1000000\n  Bound result 0\n  Bound result 11829\n  Bound result 84\n  Bound result 396\n  Bound result 1\n  Bound result 2630\n  Bound result 40\n  Bound result 69\n  Bound result 86400\n  Bound result 49\n  Bound result 172800\n  Bound result 6321\n  Bound result 14\n  Bound result 82\n  Bound result 2376452954\n  Bound result 10990\n  Bound result 10192\n  Bound result 748\n  Bound result 424\n  Bound result 6150\n  Bound result 4\n  Bound result 15617\n  Bound result 2905\n  Bound result 602\n  Bound result 6934\n  Bound result 82969\n  Bound result 3843\n  Bound result 1\n  Bound result 86740\n  Bound result 10910\n  Bound result 9968\n  Bound result 11742\n  Bound result 242\n  Bound result 1276\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.24s (7.23s CPU time)\n\nRan 5 test suites in 7.24s (7.35s CPU time): 70 tests passed, 0 failed, 0 skipped (70 total tests)\n","passed":true},{"durationMs":48,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HandleVault.acceptDefaultAdminTransfer()\",\"HandleVault.beginDefaultAdminTransfer(address)\",\"HandleVault.cancelDefaultAdminTransfer()\",\"HandleVault.changeDefaultAdminDelay(uint48)\",\"HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes)\",\"HandleVault.depositFor(bytes32,address,uint256)\",\"HandleVault.depositNativeFor(bytes32)\",\"HandleVault.grantRole(bytes32,address)\",\"HandleVault.pause()\",\"HandleVault.refund(uint256)\",\"HandleVault.renounceRole(bytes32,address)\",\"HandleVault.revokeRole(bytes32,address)\",\"HandleVault.rollbackDefaultAdminDelay()\",\"HandleVault.setFee(uint256,address)\",\"HandleVault.setOutflowCap(address,uint256)\",\"HandleVault.setTokenConfig(address,bool,uint256)\",\"HandleVault.unpause()\",\"HandleVault.withdrawFees(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.renounceOwnership()\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":4,\"README.md\":189,\"docs/SECURITY_REVIEW.md\":30,\"docs/VERIFICATION.md\":18,\"foundry.toml\":23,\"launch.json\":23,\"remappings.txt\":2,\"script/DeployHandleVault.s.sol\":34,\"src/HandleVault.sol\":510,\"src/MockUSDC.sol\":19,\"src/interfaces/IFutureHandleVault.sol\":19,\"test/Adversarial.t.sol\":268,\"test/Deployment.t.sol\":125,\"test/HandleVault.invariant.t.sol\":367,\"test/HandleVault.t.sol\":635,\"test/Signatures.t.sol\":194,\"test/helpers/Adversaries.sol\":119,\"test/helpers/VaultFixture.sol\":117},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"0b2d371ed07c606f2feafe72812cb81152e297578dc2f170eae7da24a97f6a4a","verifiedTreeHash":"88ab6f5af435f5b04a31c9db0b29887898491787","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":35568,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 35.47s\nCompiler run successful!\n","passed":true},{"durationMs":7662,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_constructorDoesNotCallUndeployedToken() (gas: 70423)\n[PASS] test_constructorRejectsInvalidConfiguration() (gas: 6264)\n[PASS] test_deploymentConfigurationUsesExplicitOwnerNotCaller() (gas: 17917749)\n[PASS] test_mockMintRestrictedAndOwnershipTwoStep() (gas: 301451)\n[PASS] test_scriptRunIsSepoliaOnlyAndNeedsNoEnvironment() (gas: 8524695)\nLogs:\n  Simulated MockUSDC (not a live deployment): 0x104fBc016F4bb334D775a19E8A6510109AC63E00\n  Simulated HandleVault (not a live deployment): 0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3\n\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 24.53ms (25.54ms CPU time)\n\nRan 16 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test_depositTokenCallbackCannotReenter() (gas: 687473)\n[PASS] test_failedNativeTransferRollsBackEarlierTokenPayoutAndFees() (gas: 2707311)\n[PASS] test_failedRefundKeepsOriginalRecordAndCanRetry() (gas: 711438)\n[PASS] test_failedTokenClaimRollsBackEveryEffectThenCanRetry() (gas: 1183983)\n[PASS] test_failedTokenRefundKeepsRecord() (gas: 768193)\n[PASS] test_feeOnTransferRejectedWithoutCreatingLiabilities() (gas: 241853)\n[PASS] test_feeWithdrawalCannotReenter() (gas: 933019)\n[PASS] test_missingAllowanceRevertsWithoutDepositRecord() (gas: 79109)\n[PASS] test_nativePayoutCannotReenterClaimAndSeesSettledAccounting() (gas: 1028014)\n[PASS] test_nativeRefundCannotReenterRefund() (gas: 639405)\n[PASS] test_rejectingFeeRecipientDoesNotBlockClaimsAndWithdrawalCanRecover() (gas: 940957)\n[PASS] test_safeERC20RejectsFalseReturnOnDeposit() (gas: 173528)\n[PASS] test_safeERC20SupportsNoReturnTokens() (gas: 1586750)\n[PASS] test_tokenEnablingTaxAfterDepositCannotUnderpayClaimOrRefund() (gas: 1168593)\n[PASS] test_tokenPayoutCallbackCannotReenterRefund() (gas: 965670)\n[PASS] test_unsolicitedTransfersAreUnaccountedSurplusNotWithdrawableFees() (gas: 1477273)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 79.05ms (12.45ms CPU time)\n\nRan 14 tests for test/CapSharing.t.sol:CapSharingTest\n[PASS] testFuzz_recipientNeverExceedsShareOrSharedCapInOneWindow(uint16,uint96[8]) (runs: 512, μ: 3660822, ~: 3644056)\nLogs:\n  Bound result 41\n  Bound result 2905984\n  Bound result 3242658\n  Bound result 2076548\n  Bound result 1000001\n  Bound result 1792548\n  Bound result 1045638\n  Bound result 3100008\n  Bound result 2607407\n\n[PASS] test_defaultShareIsWholeCapAndViewsAgree() (gas: 66710)\n[PASS] test_depositsAreBoundedByRecipientCap() (gas: 915082)\n[PASS] test_loweredCapSkipsOversizedDepositUntilRaised() (gas: 1423483)\n[PASS] test_loweringShareBelowExistingDepositDelaysItUntilRaised() (gas: 2023038)\n[PASS] test_nothingFitsStillRevertsWithCapExceeded() (gas: 1648954)\n[PASS] test_oneRecipientCannotConsumeTheWholeWindowWhenShared() (gas: 2315024)\n[PASS] test_oversizedHeadIsSkippedAndLaterDepositsArePaid() (gas: 3175043)\n[PASS] test_recipientWindowIsExactlyTwentyFourHoursAndCountsFees() (gas: 1682302)\n[PASS] test_recipientWindowsAreIndependentPerToken() (gas: 1883136)\n[PASS] test_setRecipientShareValidationEventAndAccess() (gas: 151973)\n[PASS] test_skipKeepsFifoOrderAmongDepositsThatFit() (gas: 2091348)\n[PASS] test_visitBudgetBoundsSkippedDepositsAndContinues() (gas: 98165739)\n[PASS] test_visitBudgetIsSharedAcrossTokens() (gas: 98216868)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 79.45ms (158.72ms CPU time)\n\nRan 34 tests for test/HandleVault.t.sol:HandleVaultTest\n[PASS] testFuzz_amountsAndFeesConserveFunds(uint96,uint16) (runs: 512, μ: 963885, ~: 973744)\nLogs:\n  Bound result 5427479551\n  Bound result 0\n\n[PASS] testFuzz_nativeAmountRefundedInFull(uint96,uint32) (runs: 512, μ: 462801, ~: 462868)\nLogs:\n  Bound result 9999900053735228478\n\n[PASS] test_adminHandoverCancellationAndNoBypassOrRenounce() (gas: 220168)\n[PASS] test_adminOnlySettersAndRoleManagement() (gas: 188402)\n[PASS] test_allowlistAndMinimumsAndMaxDeposit() (gas: 1752139)\n[PASS] test_capBlockedTokenDoesNotBlockAnotherToken() (gas: 1242387)\n[PASS] test_capCountsAllRecipientsAndFeesAndSameTimestampClaims() (gas: 1619996)\n[PASS] test_capIsRollingNotCalendarBucketAndCannotBeResetByConfig() (gas: 1705123)\n[PASS] test_capPartialSweepAndRollover() (gas: 1572222)\n[PASS] test_configurationValidationAndNoCodeToken() (gas: 232968)\n[PASS] test_depositRecordEventAndTotals() (gas: 1325548)\n[PASS] test_feeMaxRoundedPerDepositAndWithdrawOnlyFees() (gas: 1872538)\n[PASS] test_feeRejectsInvalidParameters() (gas: 91154)\n[PASS] test_globalSweepLimitAcrossTokens() (gas: 32390058)\n[PASS] test_initialConfiguration() (gas: 106178)\n[PASS] test_invalidPagination() (gas: 795855)\n[PASS] test_lifetimeOutflowWrapDoesNotDisableClaims() (gas: 2360513)\n[PASS] test_matureDepositsStillClaimableUntilRefunded() (gas: 734549)\n[PASS] test_maxUintAmountFeeDoesNotOverflow() (gas: 1034891)\n[PASS] test_multiTokenAndNativeClaim() (gas: 2414586)\n[PASS] test_nativeCapEnforcement() (gas: 1296968)\n[PASS] test_nativeDirectTransferRejected() (gas: 37000)\n[PASS] test_nativeFeesAndRecipientChange() (gas: 820664)\n[PASS] test_paginationAndConstantTimeMiddleRemoval() (gas: 3745571)\n[PASS] test_partialSweepContinuationIsFIFOAndBounded() (gas: 29711068)\n[PASS] test_pauseSemanticsAndUnpause() (gas: 1442074)\n[PASS] test_pauserRoleSeparatedFromAuthorizer() (gas: 213320)\n[PASS] test_refundClaimedOrUnknownRejected() (gas: 823986)\n[PASS] test_refundExactDelayToOriginalSenderByAnyone() (gas: 709392)\n[PASS] test_refundNativeNoFeeWhilePausedDisabledAndCapZero() (gas: 636654)\n[PASS] test_relayClaimEventsAndZeroFees() (gas: 846914)\n[PASS] test_rollingCapAtTimestampZero() (gas: 716653)\n[PASS] test_twoStepDelayedAdminTransfer() (gas: 315464)\n[PASS] test_unlistedTokensRemainOpen() (gas: 1205970)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 79.46ms (101.65ms CPU time)\n\nRan 14 tests for test/Signatures.t.sol:SignaturesTest\n[PASS] testFuzz_digestMatchesReference(bytes32,address,uint256,uint256) (runs: 512, μ: 10898, ~: 10898)\n[PASS] test_authorizerRotationRevokesOutstandingSignatures() (gas: 912692)\n[PASS] test_badDestinationAndTokenListsRejected() (gas: 205832)\n[PASS] test_chainIdSeparatesSignaturesAndDomainUpdates() (gas: 89791)\n[PASS] test_digestMatchesIndependentReferenceAndTypehash() (gas: 41645)\n[PASS] test_emptyClaimDoesNotBurnNonce() (gas: 378587)\n[PASS] test_expiredRejectedAndExactDeadlineAccepted() (gas: 320816)\n[PASS] test_malformedMalleableAndInvalidVSignaturesRejected() (gas: 173553)\n[PASS] test_nonAuthorizerCannotClaim() (gas: 77214)\n[PASS] test_nonceScopedToRecipientButSharedByAuthorizers() (gas: 1486091)\n[PASS] test_otherVaultSignatureRejected() (gas: 62486)\n[PASS] test_replayRejectedEvenIfNewDepositsArrive() (gas: 773223)\n[PASS] test_tokenSubsetIsDeliberatelyUnsigned() (gas: 761181)\n[PASS] test_wrongRecipientDestinationNonceOrDeadlineRejected() (gas: 606436)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 79.52ms (26.91ms CPU time)\n\nRan 1 test for test/HandleVault.invariant.t.sol:HandleVaultInvariantTest\n[PASS]\nHandleVaultInvariantTest invariants:\n[PASS] invariant_balancesEqualOpenDepositsPlusAccruedFeesAndModel\n[PASS] invariant_openPaginationAndPerRecipientTotalsMatchModel\n HandleVaultInvariantTest invariants (runs: 128, calls: 12288, reverts: 0)\n\n╭--------------+---------------+-------+---------+----------╮\n| Contract     | Selector      | Calls | Reverts | Discards |\n+===========================================================+\n| VaultHandler | advanceTime   | 1332  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | claim         | 1359  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureCap  | 1343  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureFee  | 1362  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | deposit       | 1335  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | refund        | 1382  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | toggleAllowed | 1415  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | togglePause   | 1406  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | withdrawFees  | 1354  | 0       | 0        |\n╰--------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 100\n  Bound result 1000000000000000000\n  Bound result 10000000\n  Bound result 10000000\n  Bound result 7\n  Bound result 1000000000000000000\n  Bound result 1000000\n  Bound result 1000000\n  Bound result 0\n  Bound result 11971\n  Bound result 84\n  Bound result 13\n  Bound result 1\n  Bound result 37143117673\n  Bound result 33\n  Bound result 0\n  Bound result 86400\n  Bound result 97\n  Bound result 2500\n  Bound result 7552\n  Bound result 14\n  Bound result 17\n  Bound result 757\n  Bound result 11905\n  Bound result 7711\n  Bound result 699\n  Bound result 3714247998\n  Bound result 7377\n  Bound result 6\n  Bound result 15617\n  Bound result 466\n  Bound result 2804\n  Bound result 678829032\n  Bound result 1129\n  Bound result 13258\n  Bound result 3\n  Bound result 3843\n  Bound result 2\n  Bound result 86740\n  Bound result 3379\n  Bound result 10406\n  Bound result 10458\n  Bound result 13315\n  Bound result 86707\n  Bound result 1276\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.55s (7.55s CPU time)\n\nRan 6 test suites in 7.56s (7.89s CPU time): 84 tests passed, 0 failed, 0 skipped (84 total tests)\n","passed":true},{"durationMs":65,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HandleVault.acceptDefaultAdminTransfer()\",\"HandleVault.beginDefaultAdminTransfer(address)\",\"HandleVault.cancelDefaultAdminTransfer()\",\"HandleVault.changeDefaultAdminDelay(uint48)\",\"HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes)\",\"HandleVault.depositFor(bytes32,address,uint256)\",\"HandleVault.depositNativeFor(bytes32)\",\"HandleVault.grantRole(bytes32,address)\",\"HandleVault.pause()\",\"HandleVault.refund(uint256)\",\"HandleVault.renounceRole(bytes32,address)\",\"HandleVault.revokeRole(bytes32,address)\",\"HandleVault.rollbackDefaultAdminDelay()\",\"HandleVault.setFee(uint256,address)\",\"HandleVault.setOutflowCap(address,uint256)\",\"HandleVault.setRecipientShareBps(uint256)\",\"HandleVault.setTokenConfig(address,bool,uint256)\",\"HandleVault.unpause()\",\"HandleVault.withdrawFees(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.renounceOwnership()\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":4,\"README.md\":201,\"docs/SECURITY_REVIEW.md\":31,\"docs/VERIFICATION.md\":20,\"foundry.toml\":23,\"launch.json\":23,\"remappings.txt\":2,\"script/DeployHandleVault.s.sol\":34,\"src/HandleVault.sol\":589,\"src/MockUSDC.sol\":19,\"src/interfaces/IFutureHandleVault.sol\":19,\"test/Adversarial.t.sol\":268,\"test/CapSharing.t.sol\":318,\"test/Deployment.t.sol\":125,\"test/HandleVault.invariant.t.sol\":371,\"test/HandleVault.t.sol\":635,\"test/Signatures.t.sol\":194,\"test/helpers/Adversaries.sol\":119,\"test/helpers/VaultFixture.sol\":117},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"1c889dee9a113bada9ec077e9323e3a244661a7136f3629eaae9a23d58a43b60","verifiedTreeHash":"aa72dcd9e2177b64ade77531de6a62ce99a34707","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":27068,"exitCode":0,"name":"build","output":"Compiling 61 files with Solc 0.8.26\nSolc 0.8.26 finished in 26.97s\nCompiler run successful!\n","passed":true},{"durationMs":7053,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 16 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test_depositTokenCallbackCannotReenter() (gas: 684768)\n[PASS] test_failedNativeTransferRollsBackEarlierTokenPayoutAndFees() (gas: 2353325)\n[PASS] test_failedRefundKeepsOriginalRecordAndCanRetry() (gas: 708533)\n[PASS] test_failedTokenClaimRollsBackEveryEffectThenCanRetry() (gas: 1039848)\n[PASS] test_failedTokenRefundKeepsRecord() (gas: 765563)\n[PASS] test_feeOnTransferRejectedWithoutCreatingLiabilities() (gas: 239104)\n[PASS] test_feeWithdrawalCannotReenter() (gas: 859647)\n[PASS] test_missingAllowanceRevertsWithoutDepositRecord() (gas: 76470)\n[PASS] test_nativePayoutCannotReenterClaimAndSeesSettledAccounting() (gas: 954752)\n[PASS] test_nativeRefundCannotReenterRefund() (gas: 636773)\n[PASS] test_rejectingFeeRecipientDoesNotBlockClaimsAndWithdrawalCanRecover() (gas: 867585)\n[PASS] test_safeERC20RejectsFalseReturnOnDeposit() (gas: 170889)\n[PASS] test_safeERC20SupportsNoReturnTokens() (gas: 1510930)\n[PASS] test_tokenEnablingTaxAfterDepositCannotUnderpayClaimOrRefund() (gas: 1095091)\n[PASS] test_tokenPayoutCallbackCannotReenterRefund() (gas: 892467)\n[PASS] test_unsolicitedTransfersAreUnaccountedSurplusNotWithdrawableFees() (gas: 1331006)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 12.92ms (6.38ms CPU time)\n\nRan 5 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_constructorDoesNotCallUndeployedToken() (gas: 67762)\n[PASS] test_constructorRejectsInvalidConfiguration() (gas: 6264)\n[PASS] test_deploymentConfigurationUsesExplicitOwnerNotCaller() (gas: 17305808)\n[PASS] test_mockMintRestrictedAndOwnershipTwoStep() (gas: 301451)\n[PASS] test_scriptRunIsSepoliaOnlyAndNeedsNoEnvironment() (gas: 8232329)\nLogs:\n  Simulated MockUSDC (not a live deployment): 0x104fBc016F4bb334D775a19E8A6510109AC63E00\n  Simulated HandleVault (not a live deployment): 0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3\n\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 33.62ms (18.71ms CPU time)\n\nRan 14 tests for test/Signatures.t.sol:SignaturesTest\n[PASS] testFuzz_digestMatchesReference(bytes32,address,uint256,uint256) (runs: 512, μ: 10832, ~: 10832)\n[PASS] test_authorizerRotationRevokesOutstandingSignatures() (gas: 839181)\n[PASS] test_badDestinationAndTokenListsRejected() (gas: 205502)\n[PASS] test_chainIdSeparatesSignaturesAndDomainUpdates() (gas: 89307)\n[PASS] test_digestMatchesIndependentReferenceAndTypehash() (gas: 41227)\n[PASS] test_emptyClaimDoesNotBurnNonce() (gas: 302622)\n[PASS] test_expiredRejectedAndExactDeadlineAccepted() (gas: 250142)\n[PASS] test_malformedMalleableAndInvalidVSignaturesRejected() (gas: 173289)\n[PASS] test_nonAuthorizerCannotClaim() (gas: 77016)\n[PASS] test_nonceScopedToRecipientButSharedByAuthorizers() (gas: 1351889)\n[PASS] test_otherVaultSignatureRejected() (gas: 62420)\n[PASS] test_replayRejectedEvenIfNewDepositsArrive() (gas: 699998)\n[PASS] test_tokenSubsetIsDeliberatelyUnsigned() (gas: 688022)\n[PASS] test_wrongRecipientDestinationNonceOrDeadlineRejected() (gas: 603753)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 33.65ms (10.97ms CPU time)\n\nRan 34 tests for test/HandleVault.t.sol:HandleVaultTest\n[PASS] testFuzz_amountsAndFeesConserveFunds(uint96,uint16) (runs: 512, μ: 891074, ~: 900299)\nLogs:\n  Bound result 99999000015\n  Bound result 50\n\n[PASS] testFuzz_nativeAmountRefundedInFull(uint96,uint32) (runs: 512, μ: 460309, ~: 460377)\nLogs:\n  Bound result 9999900053735228478\n\n[PASS] test_adminHandoverCancellationAndNoBypassOrRenounce() (gas: 219376)\n[PASS] test_adminOnlySettersAndRoleManagement() (gas: 187654)\n[PASS] test_allowlistAndMinimumsAndMaxDeposit() (gas: 1600767)\n[PASS] test_capBlockedTokenDoesNotBlockAnotherToken() (gas: 1159209)\n[PASS] test_capCountsAllRecipientsAndFeesAndSameTimestampClaims() (gas: 1480278)\n[PASS] test_capIsRollingNotCalendarBucketAndCannotBeResetByConfig() (gas: 1573011)\n[PASS] test_capPartialSweepAndRollover() (gas: 1421146)\n[PASS] test_configurationValidationAndNoCodeToken() (gas: 230087)\n[PASS] test_depositRecordEventAndTotals() (gas: 1317521)\n[PASS] test_feeMaxRoundedPerDepositAndWithdrawOnlyFees() (gas: 1793881)\n[PASS] test_feeRejectsInvalidParameters() (gas: 90758)\n[PASS] test_globalSweepLimitAcrossTokens() (gas: 32060168)\n[PASS] test_initialConfiguration() (gas: 105034)\n[PASS] test_invalidPagination() (gas: 722168)\n[PASS] test_lifetimeOutflowWrapDoesNotDisableClaims() (gas: 2206603)\n[PASS] test_matureDepositsStillClaimableUntilRefunded() (gas: 661522)\n[PASS] test_maxUintAmountFeeDoesNotOverflow() (gas: 961260)\n[PASS] test_multiTokenAndNativeClaim() (gas: 2196820)\n[PASS] test_nativeCapEnforcement() (gas: 1143476)\n[PASS] test_nativeDirectTransferRejected() (gas: 37000)\n[PASS] test_nativeFeesAndRecipientChange() (gas: 747314)\n[PASS] test_paginationAndConstantTimeMiddleRemoval() (gas: 3600445)\n[PASS] test_partialSweepContinuationIsFIFOAndBounded() (gas: 29392992)\n[PASS] test_pauseSemanticsAndUnpause() (gas: 1366174)\n[PASS] test_pauserRoleSeparatedFromAuthorizer() (gas: 212484)\n[PASS] test_refundClaimedOrUnknownRejected() (gas: 750827)\n[PASS] test_refundExactDelayToOriginalSenderByAnyone() (gas: 706570)\n[PASS] test_refundNativeNoFeeWhilePausedDisabledAndCapZero() (gas: 633723)\n[PASS] test_relayClaimEventsAndZeroFees() (gas: 773293)\n[PASS] test_rollingCapAtTimestampZero() (gas: 664978)\n[PASS] test_twoStepDelayedAdminTransfer() (gas: 313946)\n[PASS] test_unlistedTokensRemainOpen() (gas: 1130260)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 33.70ms (91.74ms CPU time)\n\nRan 1 test for test/HandleVault.invariant.t.sol:HandleVaultInvariantTest\n[PASS]\nHandleVaultInvariantTest invariants:\n[PASS] invariant_balancesEqualOpenDepositsPlusAccruedFeesAndModel\n[PASS] invariant_openPaginationAndPerRecipientTotalsMatchModel\n HandleVaultInvariantTest invariants (runs: 128, calls: 12288, reverts: 0)\n\n╭--------------+---------------+-------+---------+----------╮\n| Contract     | Selector      | Calls | Reverts | Discards |\n+===========================================================+\n| VaultHandler | advanceTime   | 1332  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | claim         | 1359  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureCap  | 1343  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureFee  | 1362  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | deposit       | 1335  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | refund        | 1382  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | toggleAllowed | 1415  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | togglePause   | 1406  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | withdrawFees  | 1354  | 0       | 0        |\n╰--------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 100\n  Bound result 1000000000000000000\n  Bound result 10000000\n  Bound result 10000000\n  Bound result 7\n  Bound result 1000000000000000000\n  Bound result 1000000\n  Bound result 1000000\n  Bound result 0\n  Bound result 11829\n  Bound result 84\n  Bound result 396\n  Bound result 1\n  Bound result 2630\n  Bound result 40\n  Bound result 69\n  Bound result 86400\n  Bound result 49\n  Bound result 172800\n  Bound result 6321\n  Bound result 14\n  Bound result 82\n  Bound result 2376452954\n  Bound result 10990\n  Bound result 10192\n  Bound result 748\n  Bound result 424\n  Bound result 6150\n  Bound result 4\n  Bound result 15617\n  Bound result 2905\n  Bound result 602\n  Bound result 6934\n  Bound result 82969\n  Bound result 3843\n  Bound result 1\n  Bound result 86740\n  Bound result 10910\n  Bound result 9968\n  Bound result 11742\n  Bound result 242\n  Bound result 1276\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.95s (6.94s CPU time)\n\nRan 5 test suites in 6.95s (7.06s CPU time): 70 tests passed, 0 failed, 0 skipped (70 total tests)\n","passed":true},{"durationMs":54,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HandleVault.acceptDefaultAdminTransfer()\",\"HandleVault.beginDefaultAdminTransfer(address)\",\"HandleVault.cancelDefaultAdminTransfer()\",\"HandleVault.changeDefaultAdminDelay(uint48)\",\"HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes)\",\"HandleVault.depositFor(bytes32,address,uint256)\",\"HandleVault.depositNativeFor(bytes32)\",\"HandleVault.grantRole(bytes32,address)\",\"HandleVault.pause()\",\"HandleVault.refund(uint256)\",\"HandleVault.renounceRole(bytes32,address)\",\"HandleVault.revokeRole(bytes32,address)\",\"HandleVault.rollbackDefaultAdminDelay()\",\"HandleVault.setFee(uint256,address)\",\"HandleVault.setOutflowCap(address,uint256)\",\"HandleVault.setTokenConfig(address,bool,uint256)\",\"HandleVault.unpause()\",\"HandleVault.withdrawFees(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.renounceOwnership()\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":4,\"README.md\":189,\"docs/SECURITY_REVIEW.md\":30,\"docs/VERIFICATION.md\":18,\"foundry.toml\":23,\"remappings.txt\":2,\"script/DeployHandleVault.s.sol\":34,\"src/HandleVault.sol\":510,\"src/MockUSDC.sol\":19,\"src/interfaces/IFutureHandleVault.sol\":19,\"test/Adversarial.t.sol\":268,\"test/Deployment.t.sol\":125,\"test/HandleVault.invariant.t.sol\":367,\"test/HandleVault.t.sol\":635,\"test/Signatures.t.sol\":194,\"test/helpers/Adversaries.sol\":119,\"test/helpers/VaultFixture.sol\":117},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4345,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/HandleVault.sol:494: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#494-509) sends eth to arbitrary user\n[high/high] uninitialized-state at src/HandleVault.sol:352: HandleVault._outflows (src/HandleVault.sol#97) is never initialized. It is used in:\n[medium/high] incorrect-equality at src/HandleVault.sol:479: HandleVault._recordOutflow(address,uint256) (src/HandleVault.sol#479-492) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/HandleVault.sol:224: HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes).capBlocked (src/HandleVault.sol#224) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HandleVault.sol:455: HandleVault._sweep(bytes32,address,address,uint256,uint256).accounting (src/HandleVault.sol#455) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HandleVault.sol:359: HandleVault.rollingOutflow(address).low (src/HandleVault.sol#359) is a local variable never initialized\n[medium/medium] unused-return at src/HandleVault.sol:206: HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes) (src/HandleVault.sol#206-241) ignores return value by (signer,err,None) = ECDSA.tryRecover(claimDigest(recipientId,dest,nonce,deadline),signature) (src/HandleVault.sol#218-219)\n[low/medium] missing-zero-check at src/HandleVault.sol:149: HandleVault.constructor(address,address,uint256,uint256,uint256,uint256).initialOwner (src/HandleVault.sol#149) lacks a zero-check on :\n[low/medium] calls-loop at src/HandleVault.sol:494: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#494-509) has external calls inside a loop: beforeDest = IERC20(token).balanceOf(dest) (src/HandleVault.sol#500)\n[low/medium] calls-loop at src/HandleVault.sol:494: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#494-509) has external calls inside a loop: afterVault = IERC20(token).balanceOf(address(this)) (src/HandleVault.sol#502)\n[low/medium] calls-loop at src/HandleVault.sol:494: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#494-509) has external calls inside a loop: afterDest = IERC20(token).balanceOf(dest) (src/HandleVault.sol#503)\n[low/medium] calls-loop at src/HandleVault.sol:494: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#494-509) has external calls inside a loop: (success,None) = address(dest).call{value: amount}() (src/HandleVault.sol#496)\n[low/medium] calls-loop at src/HandleVault.sol:494: HandleVault._pay(address,address,uint256) (src/HandleVault.sol#494-509) has external calls inside a loop: beforeVault = IERC20(token).balanceOf(address(this)) (src/HandleVault.sol#499)\n[low/medium] timestamp at src/HandleVault.sol:352: HandleVault.rollingOutflow(address) (src/HandleVault.sol#352-371) uses timestamp for comparisons\n[low/medium] timestamp at src/HandleVault.sol:479: HandleVault._recordOutflow(address,uint256) (src/HandleVault.sol#479-492) uses timestamp for comparisons\n[low/medium] timestamp at src/HandleVault.sol:326: HandleVault.getOpenDepositIds(bytes32,uint256,uint256) (src/HandleVault.sol#326-347) uses timestamp for comparisons\n[low/medium] timestamp at src/HandleVault.sol:244: HandleVault.refund(uint256) (src/HandleVault.sol#244-252) uses timestamp for comparisons\n[low/medium] timestamp at src/HandleVault.sol:206: HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes) (src/HandleVault.sol#206-241) uses timestamp for comparisons","passed":true},{"durationMs":475,"exitCode":0,"name":"aderyn","output":"[low] centralization-risk at src/HandleVault.sol:267: Centralization Risk (8 places)\n[low] costly-loop at src/HandleVault.sol:226: Costly operations inside loop (2 places)\n[low] large-numeric-literal at src/HandleVault.sol:464: Large Numeric Literal\n[low] require-revert-in-loop at src/HandleVault.sol:238: Loop Contains `require`/`revert` (3 places)\n[low] state-no-address-check at src/HandleVault.sol:161: Address State Variable Set Without Checks\n[low] unchecked-return at src/HandleVault.sol:162: Unchecked Return (2 places)\n[low] uninitialized-local-variable at src/HandleVault.sol:238: Uninitialized Local Variable (3 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"5a31bbc51aaeec9d5231da15178bcc82d7cc7a3409b856ada06b8c90e1ffd8d8","verifiedTreeHash":"ea6e62ac1560cb4dfd8af27a8b50fda7b1de9142","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":39195,"exitCode":0,"name":"build","output":"Compiling 63 files with Solc 0.8.26\nSolc 0.8.26 finished in 39.11s\nCompiler run successful!\n","passed":true},{"durationMs":6699,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_constructorDoesNotCallUndeployedToken() (gas: 67762)\n[PASS] test_constructorRejectsInvalidConfiguration() (gas: 6264)\n[PASS] test_deploymentConfigurationUsesExplicitOwnerNotCaller() (gas: 17305808)\n[PASS] test_mockMintRestrictedAndOwnershipTwoStep() (gas: 301451)\n[PASS] test_scriptRunIsSepoliaOnlyAndNeedsNoEnvironment() (gas: 8232329)\nLogs:\n  Simulated MockUSDC (not a live deployment): 0x104fBc016F4bb334D775a19E8A6510109AC63E00\n  Simulated HandleVault (not a live deployment): 0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3\n\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 300.96ms (33.17ms CPU time)\n\nRan 16 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test_depositTokenCallbackCannotReenter() (gas: 684768)\n[PASS] test_failedNativeTransferRollsBackEarlierTokenPayoutAndFees() (gas: 2353325)\n[PASS] test_failedRefundKeepsOriginalRecordAndCanRetry() (gas: 708533)\n[PASS] test_failedTokenClaimRollsBackEveryEffectThenCanRetry() (gas: 1039848)\n[PASS] test_failedTokenRefundKeepsRecord() (gas: 765563)\n[PASS] test_feeOnTransferRejectedWithoutCreatingLiabilities() (gas: 239104)\n[PASS] test_feeWithdrawalCannotReenter() (gas: 859647)\n[PASS] test_missingAllowanceRevertsWithoutDepositRecord() (gas: 76470)\n[PASS] test_nativePayoutCannotReenterClaimAndSeesSettledAccounting() (gas: 954752)\n[PASS] test_nativeRefundCannotReenterRefund() (gas: 636773)\n[PASS] test_rejectingFeeRecipientDoesNotBlockClaimsAndWithdrawalCanRecover() (gas: 867585)\n[PASS] test_safeERC20RejectsFalseReturnOnDeposit() (gas: 170889)\n[PASS] test_safeERC20SupportsNoReturnTokens() (gas: 1510930)\n[PASS] test_tokenEnablingTaxAfterDepositCannotUnderpayClaimOrRefund() (gas: 1095091)\n[PASS] test_tokenPayoutCallbackCannotReenterRefund() (gas: 892467)\n[PASS] test_unsolicitedTransfersAreUnaccountedSurplusNotWithdrawableFees() (gas: 1331006)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 301.09ms (10.12ms CPU time)\n\nRan 14 tests for test/Signatures.t.sol:SignaturesTest\n[PASS] testFuzz_digestMatchesReference(bytes32,address,uint256,uint256) (runs: 512, μ: 10832, ~: 10832)\n[PASS] test_authorizerRotationRevokesOutstandingSignatures() (gas: 839181)\n[PASS] test_badDestinationAndTokenListsRejected() (gas: 205502)\n[PASS] test_chainIdSeparatesSignaturesAndDomainUpdates() (gas: 89307)\n[PASS] test_digestMatchesIndependentReferenceAndTypehash() (gas: 41227)\n[PASS] test_emptyClaimDoesNotBurnNonce() (gas: 302622)\n[PASS] test_expiredRejectedAndExactDeadlineAccepted() (gas: 250142)\n[PASS] test_malformedMalleableAndInvalidVSignaturesRejected() (gas: 173289)\n[PASS] test_nonAuthorizerCannotClaim() (gas: 77016)\n[PASS] test_nonceScopedToRecipientButSharedByAuthorizers() (gas: 1351889)\n[PASS] test_otherVaultSignatureRejected() (gas: 62420)\n[PASS] test_replayRejectedEvenIfNewDepositsArrive() (gas: 699998)\n[PASS] test_tokenSubsetIsDeliberatelyUnsigned() (gas: 688022)\n[PASS] test_wrongRecipientDestinationNonceOrDeadlineRejected() (gas: 603753)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 301.26ms (46.95ms CPU time)\n\nRan 34 tests for test/HandleVault.t.sol:HandleVaultTest\n[PASS] testFuzz_amountsAndFeesConserveFunds(uint96,uint16) (runs: 512, μ: 891271, ~: 900189)\nLogs:\n  Bound result 55447500176\n  Bound result 2\n\n[PASS] testFuzz_nativeAmountRefundedInFull(uint96,uint32) (runs: 512, μ: 460309, ~: 460377)\nLogs:\n  Bound result 11654038906616717\n\n[PASS] test_adminHandoverCancellationAndNoBypassOrRenounce() (gas: 219376)\n[PASS] test_adminOnlySettersAndRoleManagement() (gas: 187654)\n[PASS] test_allowlistAndMinimumsAndMaxDeposit() (gas: 1600767)\n[PASS] test_capBlockedTokenDoesNotBlockAnotherToken() (gas: 1159209)\n[PASS] test_capCountsAllRecipientsAndFeesAndSameTimestampClaims() (gas: 1480278)\n[PASS] test_capIsRollingNotCalendarBucketAndCannotBeResetByConfig() (gas: 1573011)\n[PASS] test_capPartialSweepAndRollover() (gas: 1421146)\n[PASS] test_configurationValidationAndNoCodeToken() (gas: 230087)\n[PASS] test_depositRecordEventAndTotals() (gas: 1317521)\n[PASS] test_feeMaxRoundedPerDepositAndWithdrawOnlyFees() (gas: 1793881)\n[PASS] test_feeRejectsInvalidParameters() (gas: 90758)\n[PASS] test_globalSweepLimitAcrossTokens() (gas: 32060168)\n[PASS] test_initialConfiguration() (gas: 105034)\n[PASS] test_invalidPagination() (gas: 722168)\n[PASS] test_lifetimeOutflowWrapDoesNotDisableClaims() (gas: 2206603)\n[PASS] test_matureDepositsStillClaimableUntilRefunded() (gas: 661522)\n[PASS] test_maxUintAmountFeeDoesNotOverflow() (gas: 961260)\n[PASS] test_multiTokenAndNativeClaim() (gas: 2196820)\n[PASS] test_nativeCapEnforcement() (gas: 1143476)\n[PASS] test_nativeDirectTransferRejected() (gas: 37000)\n[PASS] test_nativeFeesAndRecipientChange() (gas: 747314)\n[PASS] test_paginationAndConstantTimeMiddleRemoval() (gas: 3600445)\n[PASS] test_partialSweepContinuationIsFIFOAndBounded() (gas: 29392992)\n[PASS] test_pauseSemanticsAndUnpause() (gas: 1366174)\n[PASS] test_pauserRoleSeparatedFromAuthorizer() (gas: 212484)\n[PASS] test_refundClaimedOrUnknownRejected() (gas: 750827)\n[PASS] test_refundExactDelayToOriginalSenderByAnyone() (gas: 706570)\n[PASS] test_refundNativeNoFeeWhilePausedDisabledAndCapZero() (gas: 633723)\n[PASS] test_relayClaimEventsAndZeroFees() (gas: 773293)\n[PASS] test_rollingCapAtTimestampZero() (gas: 664978)\n[PASS] test_twoStepDelayedAdminTransfer() (gas: 313946)\n[PASS] test_unlistedTokensRemainOpen() (gas: 1130260)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 301.41ms (562.50ms CPU time)\n\nRan 28 tests for test/HandleVaultEdges.t.sol:HandleVaultEdgesTest\n[PASS] testFuzz_anyKeyWithoutRoleIsRejected(uint256,uint256) (runs: 512, μ: 524256, ~: 524188)\nLogs:\n  Bound result 7496889281518579784636616634013717667430978935389503493\n\n[PASS] testFuzz_claimFollowsFifoUntilFirstDepositThatBreaksCap(uint96[4],uint96) (runs: 512, μ: 2036854, ~: 2027354)\nLogs:\n  Bound result 99999023725\n  Bound result 99998023732\n  Bound result 99998032248\n  Bound result 99998030991\n  Bound result 99998036706\n\n[PASS] testFuzz_deadlineBoundary(uint64,uint64) (runs: 512, μ: 586695, ~: 667259)\nLogs:\n  Bound result 14582845\n\n[PASS] testFuzz_feeSumOverManyDepositsMatchesPerDepositRounding(uint16,uint96[8]) (runs: 512, μ: 3667690, ~: 3671067)\nLogs:\n  Bound result 2\n  Bound result 9999005149\n  Bound result 9999014322\n  Bound result 9999023683\n  Bound result 9999015724\n  Bound result 9999004767\n  Bound result 9999011439\n  Bound result 9999009676\n  Bound result 9999010194\n\n[PASS] testFuzz_minimumAndAllowlistBoundaries(uint96,uint96) (runs: 512, μ: 330525, ~: 507043)\nLogs:\n  Bound result 22345029374\n  Bound result 7\n\n[PASS] testFuzz_paginationVisitsEveryOpenDepositOnceInOrder(uint8,uint8,uint64) (runs: 512, μ: 5754633, ~: 4242445)\nLogs:\n  Bound result 29\n  Bound result 20\n\n[PASS] testFuzz_refundTimingBoundary(uint96,uint32,uint32) (runs: 512, μ: 662048, ~: 662034)\nLogs:\n  Bound result 30953026558\n  Bound result 18548\n\n[PASS] testFuzz_rollingWindowDropsClaimExactlyAfter24Hours(uint96,uint32) (runs: 512, μ: 695688, ~: 695475)\nLogs:\n  Bound result 55447500176\n  Bound result 2\n\n[PASS] testFuzz_rollingWindowMatchesNaiveSumOverManyCheckpoints(uint8,uint256) (runs: 512, μ: 5774626, ~: 4612918)\nLogs:\n  Bound result 13\n\n[PASS] testFuzz_signatureBindsEveryField(bytes32,address,uint256,uint64) (runs: 512, μ: 1251477, ~: 1251482)\n[PASS] test_adminCannotExtractPrincipalThroughFeeSettings() (gas: 1422881)\n[PASS] test_adminLoweringCapBelowOpenDepositBlocksClaimButNotRefund() (gas: 1477650)\n[PASS] test_adminSettersEmitOldAndNewValues() (gas: 184417)\n[PASS] test_authorizerAndPauserHaveNoAdminPowers() (gas: 501384)\n[PASS] test_authorizerRenouncingItselfInvalidatesItsSignatures() (gas: 799007)\n[PASS] test_capBlockingIsHeadOfLineFifo() (gas: 1872745)\n[PASS] test_claimEmitsClaimedPerDepositInFifoOrderThenSummary() (gas: 1742281)\n[PASS] test_claimSkipsRefundedDepositsAndKeepsFifoOrder() (gas: 1575683)\n[PASS] test_claimSweepsDepositEqualToRemainingCap() (gas: 1502351)\n[PASS] test_claimantMayRelayForItselfAndDestMayBeTheRelayer() (gas: 664358)\n[PASS] test_compactSixtyFourByteSignatureRejected() (gas: 541413)\n[PASS] test_constructorEmitsInitialConfiguration() (gas: 22200)\n[PASS] test_depositEqualToCapAccepted() (gas: 1238663)\n[PASS] test_refundTokenIgnoresPauseDelistAndCapZero() (gas: 778831)\n[PASS] test_sixteenTokensAndSixtyFourDepositsFitOneClaimWellUnderBlockGas() (gas: 31577783)\n[PASS] test_sweepBudgetStopsExactlyAtSixtyFourAcrossTokenBoundary() (gas: 26179786)\n[PASS] test_unpausedStateRequiredToPauseAgainAndViceVersa() (gas: 144635)\n[PASS] test_zeroRecipientIdAndMaxNonceAreOrdinaryValues() (gas: 1162873)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 301.49ms (2.37s CPU time)\n\nRan 1 test for test/HandleVaultSecurity.invariant.t.sol:HandleVaultSecurityInvariantTest\n[PASS]\nHandleVaultSecurityInvariantTest invariants:\n[PASS] invariant_adversaryNeverProfits\n[PASS] invariant_balanceEqualsOpenDepositsPlusAccruedFees\n[PASS] invariant_privilegesAndConfigurationUntouchedByAdversary\n[PASS] invariant_rollingOutflowNeverExceedsCap\n[PASS] invariant_usedNoncesStayUsedAndUnusedStayUnused\n HandleVaultSecurityInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭------------------+-------------------------+-------+---------+----------╮\n| Contract         | Selector                | Calls | Reverts | Discards |\n+=========================================================================+\n| AdversaryHandler | adminCalls              | 519   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | advanceTime             | 496   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | assertStatusesMonotonic | 515   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | attackerDeposit         | 512   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | forgedSignature         | 520   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | honestClaim             | 524   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | honestDeposit           | 499   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | pauseToggleByPauser     | 509   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | redirectIssuedSignature | 503   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | refundAny               | 520   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | replayIssuedSignature   | 508   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | withdrawFeesAsAttacker  | 519   | 0       | 0        |\n╰------------------+-------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 500000000\n  Bound result 500000000000000000\n  Bound result 5000000\n  Bound result 7000000\n  Bound result 3\n  Bound result 12345\n  Bound result 3\n  Bound result 3\n  Bound result 3\n  Bound result 5\n  Bound result 0\n  Bound result 3\n  Bound result 68\n  Bound result 3\n  Bound result 2677\n  Bound result 1\n  Bound result 1\n  Bound result 999900000000017086\n  Bound result 1\n  Bound result 6321\n  Bound result 3\n  Bound result 999900000000006004\n  Bound result 18848\n  Bound result 3\n  Bound result 1026300537\n  Bound result 2\n  Bound result 9861035870873\n  Bound result 3\n  Bound result 13661\n  Bound result 999900000000007109\n  Bound result 3\n  Bound result 3\n  Bound result 9999005941\n  Bound result 95\n  Bound result 3\n  Bound result 2\n  Bound result 85448\n  Bound result 5874\n  Bound result 2\n  Bound result 4356\n  Bound result 3\n  Bound result 3\n  Bound result 61861\n  Bound result 273\n  Bound result 27\n  Bound result 1\n  Bound result 315850468\n  Bound result 2\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.12s (2.11s CPU time)\n\nRan 1 test for test/HandleVault.invariant.t.sol:HandleVaultInvariantTest\n[PASS]\nHandleVaultInvariantTest invariants:\n[PASS] invariant_balancesEqualOpenDepositsPlusAccruedFeesAndModel\n[PASS] invariant_openPaginationAndPerRecipientTotalsMatchModel\n HandleVaultInvariantTest invariants (runs: 128, calls: 12288, reverts: 0)\n\n╭--------------+---------------+-------+---------+----------╮\n| Contract     | Selector      | Calls | Reverts | Discards |\n+===========================================================+\n| VaultHandler | advanceTime   | 1332  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | claim         | 1359  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureCap  | 1343  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureFee  | 1362  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | deposit       | 1335  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | refund        | 1382  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | toggleAllowed | 1415  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | togglePause   | 1406  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | withdrawFees  | 1354  | 0       | 0        |\n╰--------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 100\n  Bound result 1000000000000000000\n  Bound result 10000000\n  Bound result 10000000\n  Bound result 7\n  Bound result 1000000000000000000\n  Bound result 1000000\n  Bound result 1000000\n  Bound result 0\n  Bound result 11827\n  Bound result 84\n  Bound result 48\n  Bound result 1\n  Bound result 2296\n  Bound result 9\n  Bound result 69\n  Bound result 1\n  Bound result 84\n  Bound result 40\n  Bound result 6321\n  Bound result 14\n  Bound result 84\n  Bound result 2376452954\n  Bound result 10979\n  Bound result 10191\n  Bound result 748\n  Bound result 424\n  Bound result 6150\n  Bound result 4\n  Bound result 15617\n  Bound result 1450\n  Bound result 2906\n  Bound result 6664\n  Bound result 9754181936946112596\n  Bound result 199\n  Bound result 3843\n  Bound result 5\n  Bound result 86740\n  Bound result 1745094842\n  Bound result 1144\n  Bound result 10106\n  Bound result 12439\n  Bound result 5\n  Bound result 1276\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.60s (6.60s CPU time)\n\nRan 7 test suites in 6.61s (10.23s CPU time): 99 tests passed, 0 failed, 0 skipped (99 total tests)\n","passed":true},{"durationMs":49,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HandleVault.acceptDefaultAdminTransfer()\",\"HandleVault.beginDefaultAdminTransfer(address)\",\"HandleVault.cancelDefaultAdminTransfer()\",\"HandleVault.changeDefaultAdminDelay(uint48)\",\"HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes)\",\"HandleVault.depositFor(bytes32,address,uint256)\",\"HandleVault.depositNativeFor(bytes32)\",\"HandleVault.grantRole(bytes32,address)\",\"HandleVault.pause()\",\"HandleVault.refund(uint256)\",\"HandleVault.renounceRole(bytes32,address)\",\"HandleVault.revokeRole(bytes32,address)\",\"HandleVault.rollbackDefaultAdminDelay()\",\"HandleVault.setFee(uint256,address)\",\"HandleVault.setOutflowCap(address,uint256)\",\"HandleVault.setTokenConfig(address,bool,uint256)\",\"HandleVault.unpause()\",\"HandleVault.withdrawFees(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.renounceOwnership()\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":4,\"README.md\":189,\"docs/SECURITY_REVIEW.md\":30,\"docs/VERIFICATION.md\":18,\"foundry.toml\":23,\"remappings.txt\":2,\"script/DeployHandleVault.s.sol\":34,\"src/HandleVault.sol\":510,\"src/MockUSDC.sol\":19,\"src/interfaces/IFutureHandleVault.sol\":19,\"test/Adversarial.t.sol\":268,\"test/Deployment.t.sol\":125,\"test/HandleVault.invariant.t.sol\":367,\"test/HandleVault.t.sol\":635,\"test/HandleVaultEdges.t.sol\":609,\"test/HandleVaultSecurity.invariant.t.sol\":500,\"test/Signatures.t.sol\":194,\"test/helpers/Adversaries.sol\":119,\"test/helpers/VaultFixture.sol\":117},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"c6e91ef775d84446f75dc8d241f2c09a9153a4562d914337686ff83686d9296b","verifiedTreeHash":"5702e4ef51708dbb9a95345e82dc79394db6b924","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":50022,"exitCode":0,"name":"build","output":"Compiling 64 files with Solc 0.8.26\nSolc 0.8.26 finished in 49.92s\nCompiler run successful!\n","passed":true},{"durationMs":7596,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_constructorDoesNotCallUndeployedToken() (gas: 70423)\n[PASS] test_constructorRejectsInvalidConfiguration() (gas: 6264)\n[PASS] test_deploymentConfigurationUsesExplicitOwnerNotCaller() (gas: 17917749)\n[PASS] test_mockMintRestrictedAndOwnershipTwoStep() (gas: 301451)\n[PASS] test_scriptRunIsSepoliaOnlyAndNeedsNoEnvironment() (gas: 8524695)\nLogs:\n  Simulated MockUSDC (not a live deployment): 0x104fBc016F4bb334D775a19E8A6510109AC63E00\n  Simulated HandleVault (not a live deployment): 0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3\n\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 36.26ms (35.04ms CPU time)\n\nRan 14 tests for test/CapSharing.t.sol:CapSharingTest\n[PASS] testFuzz_recipientNeverExceedsShareOrSharedCapInOneWindow(uint16,uint96[8]) (runs: 512, μ: 3660798, ~: 3644087)\nLogs:\n  Bound result 9683\n  Bound result 967313380\n  Bound result 967303810\n  Bound result 967314525\n  Bound result 922267279\n  Bound result 967310357\n  Bound result 967324177\n  Bound result 967300327\n  Bound result 52695967\n\n[PASS] test_defaultShareIsWholeCapAndViewsAgree() (gas: 66710)\n[PASS] test_depositsAreBoundedByRecipientCap() (gas: 915082)\n[PASS] test_loweredCapSkipsOversizedDepositUntilRaised() (gas: 1423483)\n[PASS] test_loweringShareBelowExistingDepositDelaysItUntilRaised() (gas: 2023038)\n[PASS] test_nothingFitsStillRevertsWithCapExceeded() (gas: 1648954)\n[PASS] test_oneRecipientCannotConsumeTheWholeWindowWhenShared() (gas: 2315024)\n[PASS] test_oversizedHeadIsSkippedAndLaterDepositsArePaid() (gas: 3175043)\n[PASS] test_recipientWindowIsExactlyTwentyFourHoursAndCountsFees() (gas: 1682302)\n[PASS] test_recipientWindowsAreIndependentPerToken() (gas: 1883136)\n[PASS] test_setRecipientShareValidationEventAndAccess() (gas: 151973)\n[PASS] test_skipKeepsFifoOrderAmongDepositsThatFit() (gas: 2091348)\n[PASS] test_visitBudgetBoundsSkippedDepositsAndContinues() (gas: 98165739)\n[PASS] test_visitBudgetIsSharedAcrossTokens() (gas: 98216868)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 339.74ms (406.52ms CPU time)\n\nRan 16 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test_depositTokenCallbackCannotReenter() (gas: 687473)\n[PASS] test_failedNativeTransferRollsBackEarlierTokenPayoutAndFees() (gas: 2707311)\n[PASS] test_failedRefundKeepsOriginalRecordAndCanRetry() (gas: 711438)\n[PASS] test_failedTokenClaimRollsBackEveryEffectThenCanRetry() (gas: 1183983)\n[PASS] test_failedTokenRefundKeepsRecord() (gas: 768193)\n[PASS] test_feeOnTransferRejectedWithoutCreatingLiabilities() (gas: 241853)\n[PASS] test_feeWithdrawalCannotReenter() (gas: 933019)\n[PASS] test_missingAllowanceRevertsWithoutDepositRecord() (gas: 79109)\n[PASS] test_nativePayoutCannotReenterClaimAndSeesSettledAccounting() (gas: 1028014)\n[PASS] test_nativeRefundCannotReenterRefund() (gas: 639405)\n[PASS] test_rejectingFeeRecipientDoesNotBlockClaimsAndWithdrawalCanRecover() (gas: 940957)\n[PASS] test_safeERC20RejectsFalseReturnOnDeposit() (gas: 173528)\n[PASS] test_safeERC20SupportsNoReturnTokens() (gas: 1586750)\n[PASS] test_tokenEnablingTaxAfterDepositCannotUnderpayClaimOrRefund() (gas: 1168593)\n[PASS] test_tokenPayoutCallbackCannotReenterRefund() (gas: 965670)\n[PASS] test_unsolicitedTransfersAreUnaccountedSurplusNotWithdrawableFees() (gas: 1477273)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 339.94ms (17.67ms CPU time)\n\nRan 30 tests for test/HandleVaultEdges.t.sol:HandleVaultEdgesTest\n[PASS] testFuzz_anyKeyWithoutRoleIsRejected(uint256,uint256) (runs: 512, μ: 526915, ~: 526849)\nLogs:\n  Bound result 788535806852647744617345925175865310728305549\n\n[PASS] testFuzz_claimPaysEveryFifoDepositThatFitsAndSkipsTheRest(uint96[6],uint96) (runs: 512, μ: 2976575, ~: 2958921)\nLogs:\n  Bound result 78856036157\n  Bound result 78855036175\n  Bound result 27948314130\n  Bound result 33110160516\n  Bound result 78855038552\n  Bound result 57659592432\n  Bound result 10049375285\n\n[PASS] testFuzz_deadlineBoundary(uint64,uint64) (runs: 512, μ: 630953, ~: 740298)\nLogs:\n  Bound result 40285911949\n\n[PASS] testFuzz_feeSumOverManyDepositsMatchesPerDepositRounding(uint16,uint96[8]) (runs: 512, μ: 3744388, ~: 3747837)\nLogs:\n  Bound result 0\n  Bound result 9999003187\n  Bound result 3101217954\n  Bound result 9999024285\n  Bound result 9999017078\n  Bound result 9999005953\n  Bound result 9999070411\n  Bound result 9999010454\n  Bound result 9999007732\n\n[PASS] testFuzz_minimumAndAllowlistBoundaries(uint96,uint96) (runs: 512, μ: 336296, ~: 509777)\nLogs:\n  Bound result 13917296645\n  Bound result 51776059052\n\n[PASS] testFuzz_paginationVisitsEveryOpenDepositOnceInOrder(uint8,uint8,uint64) (runs: 512, μ: 6121003, ~: 4251390)\nLogs:\n  Bound result 12\n  Bound result 7\n\n[PASS] testFuzz_refundTimingBoundary(uint96,uint32,uint32) (runs: 512, μ: 664718, ~: 664709)\nLogs:\n  Bound result 46295749550\n  Bound result 7767\n\n[PASS] testFuzz_rollingWindowDropsClaimExactlyAfter24Hours(uint96,uint32) (runs: 512, μ: 768807, ~: 768577)\nLogs:\n  Bound result 55447500176\n  Bound result 2\n\n[PASS] testFuzz_rollingWindowMatchesNaiveSumOverManyCheckpoints(uint8,uint256) (runs: 512, μ: 6323924, ~: 5068820)\nLogs:\n  Bound result 5\n\n[PASS] testFuzz_signatureBindsEveryField(bytes32,address,uint256,uint64) (runs: 512, μ: 1327134, ~: 1327162)\n[PASS] testFuzz_skipModelHonoursRecipientShareAndSharedWindow(uint16,uint96,uint96[5]) (runs: 512, μ: 3284579, ~: 3287314)\nLogs:\n  Bound result 370\n  Bound result 3699110847\n  Bound result 1662536602\n  Bound result 3699026571\n  Bound result 1557055541\n  Bound result 103586301\n  Bound result 263580742\n\n[PASS] test_adminCannotExtractPrincipalThroughFeeSettings() (gas: 1566976)\n[PASS] test_adminLoweringCapBelowOpenDepositBlocksClaimButNotRefund() (gas: 1565519)\n[PASS] test_adminSettersEmitOldAndNewValues() (gas: 185011)\n[PASS] test_authorizerAndPauserHaveNoAdminPowers() (gas: 564080)\n[PASS] test_authorizerRenouncingItselfInvalidatesItsSignatures() (gas: 872386)\n[PASS] test_claimEmitsClaimedPerDepositInFifoOrderThenSummary() (gas: 1889168)\n[PASS] test_claimSkipsRefundedDepositsAndKeepsFifoOrder() (gas: 1644978)\n[PASS] test_claimSweepsDepositEqualToRemainingCap() (gas: 1584883)\n[PASS] test_claimantMayRelayForItselfAndDestMayBeTheRelayer() (gas: 737385)\n[PASS] test_compactSixtyFourByteSignatureRejected() (gas: 543964)\n[PASS] test_constructorEmitsInitialConfiguration() (gas: 30638)\n[PASS] test_depositEqualToCapAccepted() (gas: 1328908)\n[PASS] test_oversizedHeadIsSkippedAndAnnouncedNotBlocking() (gas: 2284533)\n[PASS] test_refundTokenIgnoresPauseDelistAndCapZero() (gas: 781873)\n[PASS] test_sixteenTokensAndSixtyFourDepositsFitOneClaimWellUnderBlockGas() (gas: 32621278)\n[PASS] test_sweepBudgetStopsExactlyAtSixtyFourAcrossTokenBoundary() (gas: 26472630)\n[PASS] test_unpausedStateRequiredToPauseAgainAndViceVersa() (gas: 145053)\n[PASS] test_visitBudgetReachesFittingDepositInLastSlot() (gas: 98260514)\n[PASS] test_zeroRecipientIdAndMaxNonceAreOrdinaryValues() (gas: 1238759)\nSuite result: ok. 30 passed; 0 failed; 0 skipped; finished in 339.79ms (3.39s CPU time)\n\nRan 34 tests for test/HandleVault.t.sol:HandleVaultTest\n[PASS] testFuzz_amountsAndFeesConserveFunds(uint96,uint16) (runs: 512, μ: 964730, ~: 973744)\nLogs:\n  Bound result 17310692968\n  Bound result 75\n\n[PASS] testFuzz_nativeAmountRefundedInFull(uint96,uint32) (runs: 512, μ: 462801, ~: 462868)\nLogs:\n  Bound result 9999900053735228478\n\n[PASS] test_adminHandoverCancellationAndNoBypassOrRenounce() (gas: 220168)\n[PASS] test_adminOnlySettersAndRoleManagement() (gas: 188402)\n[PASS] test_allowlistAndMinimumsAndMaxDeposit() (gas: 1752139)\n[PASS] test_capBlockedTokenDoesNotBlockAnotherToken() (gas: 1242387)\n[PASS] test_capCountsAllRecipientsAndFeesAndSameTimestampClaims() (gas: 1619996)\n[PASS] test_capIsRollingNotCalendarBucketAndCannotBeResetByConfig() (gas: 1705123)\n[PASS] test_capPartialSweepAndRollover() (gas: 1572222)\n[PASS] test_configurationValidationAndNoCodeToken() (gas: 232968)\n[PASS] test_depositRecordEventAndTotals() (gas: 1325548)\n[PASS] test_feeMaxRoundedPerDepositAndWithdrawOnlyFees() (gas: 1872538)\n[PASS] test_feeRejectsInvalidParameters() (gas: 91154)\n[PASS] test_globalSweepLimitAcrossTokens() (gas: 32390058)\n[PASS] test_initialConfiguration() (gas: 106178)\n[PASS] test_invalidPagination() (gas: 795855)\n[PASS] test_lifetimeOutflowWrapDoesNotDisableClaims() (gas: 2360513)\n[PASS] test_matureDepositsStillClaimableUntilRefunded() (gas: 734549)\n[PASS] test_maxUintAmountFeeDoesNotOverflow() (gas: 1034891)\n[PASS] test_multiTokenAndNativeClaim() (gas: 2414586)\n[PASS] test_nativeCapEnforcement() (gas: 1296968)\n[PASS] test_nativeDirectTransferRejected() (gas: 37000)\n[PASS] test_nativeFeesAndRecipientChange() (gas: 820664)\n[PASS] test_paginationAndConstantTimeMiddleRemoval() (gas: 3745571)\n[PASS] test_partialSweepContinuationIsFIFOAndBounded() (gas: 29711068)\n[PASS] test_pauseSemanticsAndUnpause() (gas: 1442074)\n[PASS] test_pauserRoleSeparatedFromAuthorizer() (gas: 213320)\n[PASS] test_refundClaimedOrUnknownRejected() (gas: 823986)\n[PASS] test_refundExactDelayToOriginalSenderByAnyone() (gas: 709392)\n[PASS] test_refundNativeNoFeeWhilePausedDisabledAndCapZero() (gas: 636654)\n[PASS] test_relayClaimEventsAndZeroFees() (gas: 846914)\n[PASS] test_rollingCapAtTimestampZero() (gas: 716653)\n[PASS] test_twoStepDelayedAdminTransfer() (gas: 315464)\n[PASS] test_unlistedTokensRemainOpen() (gas: 1205970)\nSuite result: ok. 34 passed; 0 failed; 0 skipped; finished in 339.92ms (699.22ms CPU time)\n\nRan 14 tests for test/Signatures.t.sol:SignaturesTest\n[PASS] testFuzz_digestMatchesReference(bytes32,address,uint256,uint256) (runs: 512, μ: 10898, ~: 10898)\n[PASS] test_authorizerRotationRevokesOutstandingSignatures() (gas: 912692)\n[PASS] test_badDestinationAndTokenListsRejected() (gas: 205832)\n[PASS] test_chainIdSeparatesSignaturesAndDomainUpdates() (gas: 89791)\n[PASS] test_digestMatchesIndependentReferenceAndTypehash() (gas: 41645)\n[PASS] test_emptyClaimDoesNotBurnNonce() (gas: 378587)\n[PASS] test_expiredRejectedAndExactDeadlineAccepted() (gas: 320816)\n[PASS] test_malformedMalleableAndInvalidVSignaturesRejected() (gas: 173553)\n[PASS] test_nonAuthorizerCannotClaim() (gas: 77214)\n[PASS] test_nonceScopedToRecipientButSharedByAuthorizers() (gas: 1486091)\n[PASS] test_otherVaultSignatureRejected() (gas: 62486)\n[PASS] test_replayRejectedEvenIfNewDepositsArrive() (gas: 773223)\n[PASS] test_tokenSubsetIsDeliberatelyUnsigned() (gas: 761181)\n[PASS] test_wrongRecipientDestinationNonceOrDeadlineRejected() (gas: 606436)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 339.94ms (318.07ms CPU time)\n\nRan 1 test for test/HandleVaultSecurity.invariant.t.sol:HandleVaultSecurityInvariantTest\n[PASS]\nHandleVaultSecurityInvariantTest invariants:\n[PASS] invariant_adversaryNeverProfits\n[PASS] invariant_balanceEqualsOpenDepositsPlusAccruedFees\n[PASS] invariant_privilegesAndConfigurationUntouchedByAdversary\n[PASS] invariant_rollingOutflowNeverExceedsCap\n[PASS] invariant_usedNoncesStayUsedAndUnusedStayUnused\n HandleVaultSecurityInvariantTest invariants (runs: 96, calls: 6144, reverts: 0)\n\n╭------------------+-------------------------+-------+---------+----------╮\n| Contract         | Selector                | Calls | Reverts | Discards |\n+=========================================================================+\n| AdversaryHandler | adminCalls              | 519   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | advanceTime             | 496   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | assertStatusesMonotonic | 515   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | attackerDeposit         | 512   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | forgedSignature         | 520   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | honestClaim             | 524   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | honestDeposit           | 499   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | pauseToggleByPauser     | 509   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | redirectIssuedSignature | 503   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | refundAny               | 520   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | replayIssuedSignature   | 508   | 0       | 0        |\n|------------------+-------------------------+-------+---------+----------|\n| AdversaryHandler | withdrawFeesAsAttacker  | 519   | 0       | 0        |\n╰------------------+-------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 500000000\n  Bound result 500000000000000000\n  Bound result 5000000\n  Bound result 7000000\n  Bound result 3\n  Bound result 12345\n  Bound result 3\n  Bound result 3\n  Bound result 3\n  Bound result 5\n  Bound result 0\n  Bound result 3\n  Bound result 3\n  Bound result 2387\n  Bound result 3\n  Bound result 3\n  Bound result 9999017148\n  Bound result 3\n  Bound result 6765\n  Bound result 3\n  Bound result 835286525857613560\n  Bound result 18360\n  Bound result 3\n  Bound result 1026300537\n  Bound result 3\n  Bound result 9861035870873\n  Bound result 3\n  Bound result 36384\n  Bound result 999900000000007340\n  Bound result 2\n  Bound result 2\n  Bound result 9999004811\n  Bound result 900000000\n  Bound result 3\n  Bound result 2\n  Bound result 12485\n  Bound result 4901\n  Bound result 3\n  Bound result 4585\n  Bound result 1\n  Bound result 1\n  Bound result 3207\n  Bound result 89402\n  Bound result 24012\n  Bound result 1\n  Bound result 11404\n  Bound result 2\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.85s (2.85s CPU time)\n\nRan 1 test for test/HandleVault.invariant.t.sol:HandleVaultInvariantTest\n[PASS]\nHandleVaultInvariantTest invariants:\n[PASS] invariant_balancesEqualOpenDepositsPlusAccruedFeesAndModel\n[PASS] invariant_openPaginationAndPerRecipientTotalsMatchModel\n HandleVaultInvariantTest invariants (runs: 128, calls: 12288, reverts: 0)\n\n╭--------------+---------------+-------+---------+----------╮\n| Contract     | Selector      | Calls | Reverts | Discards |\n+===========================================================+\n| VaultHandler | advanceTime   | 1332  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | claim         | 1359  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureCap  | 1343  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | configureFee  | 1362  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | deposit       | 1335  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | refund        | 1382  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | toggleAllowed | 1415  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | togglePause   | 1406  | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| VaultHandler | withdrawFees  | 1354  | 0       | 0        |\n╰--------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 100\n  Bound result 1000000000000000000\n  Bound result 10000000\n  Bound result 10000000\n  Bound result 7\n  Bound result 1000000000000000000\n  Bound result 1000000\n  Bound result 1000000\n  Bound result 0\n  Bound result 12051\n  Bound result 84\n  Bound result 14400\n  Bound result 1\n  Bound result 4437731113145184131\n  Bound result 42\n  Bound result 66\n  Bound result 100\n  Bound result 96\n  Bound result 95\n  Bound result 7540\n  Bound result 14\n  Bound result 15\n  Bound result 757\n  Bound result 11828\n  Bound result 7736\n  Bound result 699\n  Bound result 3714247998\n  Bound result 7314\n  Bound result 4\n  Bound result 15617\n  Bound result 531\n  Bound result 2804\n  Bound result 29\n  Bound result 1202\n  Bound result 6592\n  Bound result 172701\n  Bound result 3843\n  Bound result 1\n  Bound result 86740\n  Bound result 4551\n  Bound result 124351\n  Bound result 11195\n  Bound result 13337\n  Bound result 2452\n  Bound result 1276\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.49s (7.49s CPU time)\n\nRan 8 test suites in 7.49s (12.08s CPU time): 115 tests passed, 0 failed, 0 skipped (115 total tests)\n","passed":true},{"durationMs":77,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HandleVault.acceptDefaultAdminTransfer()\",\"HandleVault.beginDefaultAdminTransfer(address)\",\"HandleVault.cancelDefaultAdminTransfer()\",\"HandleVault.changeDefaultAdminDelay(uint48)\",\"HandleVault.claim(bytes32,address,address[],uint256,uint256,bytes)\",\"HandleVault.depositFor(bytes32,address,uint256)\",\"HandleVault.depositNativeFor(bytes32)\",\"HandleVault.grantRole(bytes32,address)\",\"HandleVault.pause()\",\"HandleVault.refund(uint256)\",\"HandleVault.renounceRole(bytes32,address)\",\"HandleVault.revokeRole(bytes32,address)\",\"HandleVault.rollbackDefaultAdminDelay()\",\"HandleVault.setFee(uint256,address)\",\"HandleVault.setOutflowCap(address,uint256)\",\"HandleVault.setRecipientShareBps(uint256)\",\"HandleVault.setTokenConfig(address,bool,uint256)\",\"HandleVault.unpause()\",\"HandleVault.withdrawFees(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.renounceOwnership()\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":4,\"README.md\":201,\"docs/SECURITY_REVIEW.md\":31,\"docs/VERIFICATION.md\":20,\"foundry.toml\":23,\"remappings.txt\":2,\"script/DeployHandleVault.s.sol\":34,\"src/HandleVault.sol\":589,\"src/MockUSDC.sol\":19,\"src/interfaces/IFutureHandleVault.sol\":19,\"test/Adversarial.t.sol\":268,\"test/CapSharing.t.sol\":318,\"test/Deployment.t.sol\":125,\"test/HandleVault.invariant.t.sol\":371,\"test/HandleVault.t.sol\":635,\"test/HandleVaultEdges.t.sol\":731,\"test/HandleVaultSecurity.invariant.t.sol\":511,\"test/Signatures.t.sol\":194,\"test/helpers/Adversaries.sol\":119,\"test/helpers/VaultFixture.sol\":117},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f435ce5e1619011e57945f2c66a9b8d38fa33611a4cc721b92b2b3632ac89802","verifiedTreeHash":"87f8912b872bf3c219a30e41606c60a4446dfa50","verifierVersion":"0.1.0+8fbf00a5"}]}