{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"b384d442-601d-4fbb-9b90-3681b2aa1868","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"f7257978f20c9ee2406682158ac6573bc26ddd5cfcc3776f8aa26ef82e317783","dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"2d7d84a1a9972f1497e5bc68a704cbf42294d08c62d2868f102ffe3b909b9b51","skillId":"adapt-contract-project","tools":[]},"key":"adapt_contract_project","kind":"code","role":"implement","skillHash":"2d7d84a1a9972f1497e5bc68a704cbf42294d08c62d2868f102ffe3b909b9b51","skillId":"adapt-contract-project","state":"accepted"},{"acceptedSubmissionHash":"d25e52fe3c14e4f7c26525844c1aeaea716d9037c2cad61e57fc47c3dba23ca5","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"7ffa75066fec5807d1eae6113821d1787963c52ee9db65bafb8aa31d8bf3d994","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"16a6d214f7e8e580d2aa0b88f19f611d780cb49dd0543514fe488d1a41b0138a","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","state":"accepted"},{"acceptedSubmissionHash":"d78fb27340a8ec9af544a9f20bb5c9c9c99b8e422f3f45fff1d151a8c88aba78","dependsOn":["adapt_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":"ea0497fc6986d9f88658e44e85724a89ce8d3d1e3e117e5127f685b2e89167fd","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"45492542bb2eb5bc91a912e2ac22fbe71c444c9159580733d9114e73fb442363","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"df054469ae9ace20ed1cd293c33ddd8aa06f325f4a3815d432b72e0179fc6a4d","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"2720833a198d41c0f4b42095980f27695fc2a73dfe9888c396fb3d54cba684be","dependsOn":["adapt_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":"Audit and deploy IMD Vault: ownerless vesting and treasury vaults for identity.md projects. Deploy exactly src/VaultFactory.sol with constructor arguments: imd_=0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7, feeRecipient_=$owner. The factory deploys VestingVault and TreasuryVault itself and charges each project 5 IMD once, paid to feeRecipient_ (IMD RADAR's public wallet); payFor(chainId, token) lets a project on another chain pay on Ethereum. It also creates open vaults before a token exists (createLaunchVesting, or createTreasury with projectToken zero) that a project names as its launch remainderTo; an open vesting vault vests every token it receives on the same schedule, and its fee is keyed to the vault address. Keep the rules exactly as written in the source and README: no owner, admin, pause, upgrade or emergency withdrawal; vesting unlocks at most 25% a week (duration of at least 4 weeks after the cliff); treasury withdrawals wait at least 24h and at most 25% of a balance can leave per week; every rule can only get stricter. Fix only real findings, and add a Foundry test for each fix.","parentJobId":null,"planHash":"17993afb57099eca034f3787cbd2e5d3b888193491463f893dc2943bc91abb7d","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"b384d442-601d-4fbb-9b90-3681b2aa1868","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2244-audit-deploy-imd-vault"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51168","feedbackHash":"2f1f14e51b14ce7de1fe55b74451540d2cd57f74532714bc640524f69b5f034e","nodeKey":"adapt_contract_project","submissionHash":"f7257978f20c9ee2406682158ac6573bc26ddd5cfcc3776f8aa26ef82e317783","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"54154","feedbackHash":"f3502e839b808fae29bb1ea47f428c042d5539328836c9f1bd3cca13421bd748","nodeKey":"adapt_contract_project","submissionHash":"9ec423e3664e3b85c77ead03663711d214aa5104b2ec22c168dc1bb01ec79c1b","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52263","feedbackHash":"05f0ef0478defb97968c916b73ad0fdd2368bcf8b449a2c9f7d9dba640eb5330","nodeKey":"audit_economics","submissionHash":"d25e52fe3c14e4f7c26525844c1aeaea716d9037c2cad61e57fc47c3dba23ca5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52290","feedbackHash":"4172c4317c4bb53c519488426c48b0d101a7c06903384f2bad65f227039b4c71","nodeKey":"audit_flow","submissionHash":"7ffa75066fec5807d1eae6113821d1787963c52ee9db65bafb8aa31d8bf3d994","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51225","feedbackHash":"5f2ad329524a7cb4f9d82d60888b7c69b1dac94ece97756edf11717a8639bb74","nodeKey":"audit_imported_code","submissionHash":"16a6d214f7e8e580d2aa0b88f19f611d780cb49dd0543514fe488d1a41b0138a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52082","feedbackHash":"bc2cc590d905f0fba8187a101cb5ee3819f069da69f12c3d9cfc76c6529548e6","nodeKey":"audit_judge","submissionHash":"d78fb27340a8ec9af544a9f20bb5c9c9c99b8e422f3f45fff1d151a8c88aba78","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51039","feedbackHash":"09e7b6d93d9b164dba6ba2d6683bb066dab8571e1aaef7b91d218705deb4a6b8","nodeKey":"audit_judge","submissionHash":"a70ec7d04b5c76fc9ccaae48a0e9f9f4b73439ae8106e07b66fc988669dac157","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51891","feedbackHash":"03a866b4bfac7e0365d976c68ca3e9addc2a054fa3c0bc66d0de135e66964df5","nodeKey":"audit_math","submissionHash":"ea0497fc6986d9f88658e44e85724a89ce8d3d1e3e117e5127f685b2e89167fd","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52587","feedbackHash":"f07b54c3014a4b8fc3e28a154dd8faf9e07112097678addf052f9d8f5dabff9b","nodeKey":"audit_permissions","submissionHash":"45492542bb2eb5bc91a912e2ac22fbe71c444c9159580733d9114e73fb442363","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"53906","feedbackHash":"eca403ea2bafe03b60f38b1e582c04ff35fa5fffc640d7785191de83ab15e13b","nodeKey":"manifest","submissionHash":"8ea29e8dcb82875e8b5a644e01687463f8a4e77c1d0996573934df58e9137244","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51487","feedbackHash":"a8932808c3f39ff5faeb0528f7b32f63c87a62decdc80b2779ceda717aafff6a","nodeKey":"manifest","submissionHash":"df054469ae9ace20ed1cd293c33ddd8aa06f325f4a3815d432b72e0179fc6a4d","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51043","feedbackHash":"885657d305be1e11b2927645e8c50c240b1cbdfe21e6ded52765fe89259a9995","nodeKey":"write_foundry_tests","submissionHash":"14b104f812e56683335f02b9fe264b6ea93561b2069f8a4b43f742a0b8b212af","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52252","feedbackHash":"bc5ec4e571041d01c3e5ec2a45a34d7b6643816d631e0f483ff1163649fb2dc0","nodeKey":"write_foundry_tests","submissionHash":"2720833a198d41c0f4b42095980f27695fc2a73dfe9888c396fb3d54cba684be","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"2629ad4e2f04f69f859b79c0bbb3777000b2397fd6da8a83d3dca18511fb85b8","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"0c32bdbd1f7158d431e1ddb2a8b9177aa2365ce16aceecb1228d9539677141b0","device":"2565f234b0a569e9","findings":[{"description":"execute() keys the weekly window by w.asset (windows[w.asset]). A token reachable through two contract addresses that share one balance (e.g. a legacy/proxy entry forwarding to the main token, the same pattern ADAPTATION.md finding 5f526bd2 fixed in VestingVault.recover) has two independent windows. The manager can queue one withdrawal through each address and drain rate + rate*(1-rate) of the balance inside a single epoch, in one block (43.75% at 25%), and up to four windows across an epoch boundary (~68%). The rule 'at most 25% of a balance can leave per week' is broken. Only reachable for such double-entry tokens, hence low.","line":159,"path":"src/TreasuryVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {TreasuryVault} from \"src/TreasuryVault.sol\";\n\ncontract Dbl is ERC20 {\n    address public legacy;\n\n    constructor() ERC20(\"DBL\", \"DBL\") {}\n\n    function mint(address to, uint256 a) external {\n        _mint(to, a);\n    }\n\n    function setLegacy(address l) external {\n        legacy = l;\n    }\n\n    function transferFor(address from, address to, uint256 a) external {\n        require(msg.sender == legacy);\n        _transfer(from, to, a);\n    }\n}\n\ncontract Legacy {\n    Dbl immutable main;\n\n    constructor(Dbl m) {\n        main = m;\n    }\n\n    function balanceOf(address a) external view returns (uint256) {\n        return main.balanceOf(a);\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        main.transferFor(msg.sender, to, a);\n        return true;\n    }\n}\n\n/// A treasury holding a token with a second entry point lets 43.75% of its balance leave in one epoch,\n/// in one block, though at most 25% may leave per week.\ncontract DoubleEntryCapTest is Test {\n    function test_WeeklyCapHoldsAcrossEntryPointsOfOneBalance() public {\n        vm.warp(1_760_000_000);\n        address manager = makeAddr(\"manager\");\n        address dest = makeAddr(\"dest\");\n        Dbl dbl = new Dbl();\n        Legacy legacy = new Legacy(dbl);\n        dbl.setLegacy(address(legacy));\n        VaultFactory f = new VaultFactory(IERC20(address(0)), address(0));\n        TreasuryVault t = f.createTreasury(address(dbl), manager, 1 days, 2500, new address[](0));\n        dbl.mint(address(t), 1000e18);\n\n        vm.startPrank(manager);\n        uint256 a = t.queue(address(dbl), dest, 250e18);\n        uint256 b = t.queue(address(legacy), dest, 187.5e18);\n        vm.stopPrank();\n        vm.warp(block.timestamp + 1 days);\n        t.execute(a);\n        // Same epoch, same block: the cap for this balance is spent, so the second entry point must be refused.\n        try t.execute(b) {} catch {}\n        assertLe(dbl.balanceOf(dest), 250e18, \"more than 25% of the balance left in one week\");\n    }\n}","reproduction":"Treasury (rate 2500, delay 1 day) holding 1000e18 of DoubleEntryToken DBL with LegacyEntry L. Manager queues (DBL, dest, 250e18) and (L, dest, 187.5e18); after 1 day anyone executes both in the same block. Expected: the second reverts OverWeeklyCap(0), dest holds 250e18. Actual: both succeed, dest holds 437.5e18 (43.75%). Fix idea: measure the asset's balance before/after the transfer and charge the cap to the asset the cap was computed for, or refuse an execution whose transfer lowers another tracked asset's balance (as recover() does).","severity":"low","title":"Treasury weekly cap is per asset address, so a token with two entry points to one balance gets two caps"},{"description":"createVesting emits `amount` while a fee-on-transfer token delivers less to the vault. The vault's own accounting is by balance, so nothing is lost, but the radar reads this event to show what is locked and would overstate it.","line":81,"path":"src/VaultFactory.sol","reproduction":"createVesting with a 1%-fee token and amount 1000e18: the event says 1000e18, token.balanceOf(vault) is 990e18 (see FeeToken in test/Vault.t.sol). Expected the event to carry the received amount (balance after minus before).","severity":"info","title":"VestingCreated logs the requested amount, not what the vault received"},{"description":"createTreasury(projectToken, ...) and createVesting(token, ...) register the vault in vaultsByToken[token] for any caller; a treasury's projectToken is not checked at all. After the project pays its fee, further vaults are free, so a third party can file treasuries under another project's token with itself as manager. Nothing is stolen, but the radar must not treat vaultsByToken membership as the project's own vaults; it has to check manager/beneficiary and balances itself.","line":145,"path":"src/VaultFactory.sol","reproduction":"After the project's first vault, a stranger calls createTreasury(projectToken=PROJ, manager=stranger, 1 days, 2500, []): it succeeds without a fee and vaultsByToken(PROJ) lists the stranger's treasury.","severity":"info","title":"Anyone can file vaults under any project token in the registry"}],"hash":"14b104f812e56683335f02b9fe264b6ea93561b2069f8a4b43f742a0b8b212af","nodeId":"b3965e6a-8d53-46eb-bf71-9288fcc06e28","outcome":"completed","summary":"I added two test files. With the verifier's seed, `forge build` and `forge test` pass: 90 tests, 36 of them new, and the run also passes with a second seed. I found one real defect and two minor issues; they are reported in `.imd-findings.json` instead of being asserted as correct. No source file was changed.\n\n**`test/Invariants.t.sol`**: random call sequences (256 runs × 64 calls) over everything that holds value:\n- **Vesting:** one bound vault and one open vault holding two tokens. The handler mixes time jumps, top-ups, `sync`, `release` by anyone, duration extensions, beneficiary handovers, attempts by strangers, and `recover` of the vested token. The checks:\n  - what the vault holds plus what it released equals what it received, and only beneficiaries ever get paid;\n  - nothing is released during the cliff or beyond what has vested;\n  - at most 25% vests in any 7 days;\n  - the duration only grows and never drops below 4 weeks;\n  - an open vault records each token's arrival once and never moves it.\n- **Treasury:** a restricted treasury holding a token and ETH. Withdrawals can't run before the delay (never under 24h) or after the 14-day grace period, never run twice, and only go to listed destinations. Per epoch, no more than the rate × the balance at the first withdrawal leaves. Delay only rises, rate only falls, the destination list only shrinks and never reaches zero, and the treasury never reopens.\n- **Factory:** the radar wallet holds exactly 5 IMD per paid project or vault, never a second charge. The factory holds no tokens and the registry lists every vault it made.\n- Two fixed-sequence tests show the random runs really do reach paid withdrawals and releases, so the checks aren't passing on empty runs.\n\n**`test/EdgeCases.t.sol`**: 25 failure-path and boundary tests. They cover every error the contracts declare that the earlier tests never triggered, the exact second either side of each delay, cliff, grace period and handover, the weekly cap exactly reached and then exceeded by 1 wei, and a failed fee payment leaving nothing behind. They also cover an ETH payment to a destination that refuses it: the call rolls back, spends no cap, and can be retried. A fuzz test checks that nothing is ever paid out beyond what was deposited.\n\n**Reported findings:**\n- **Low: the treasury's weekly cap can be doubled** (`src/TreasuryVault.sol:159`). The cap is tracked per token address. Some tokens have two contract addresses sharing one balance, the same kind of token an earlier audit fix guarded against in `recover()`. With such a token, the manager can withdraw through each address and get two caps. Reproduced: from a treasury holding 1000, 437.5 left in one block where 250 is the limit. The failing test is attached as proof.\n- **Info: the vesting creation event can overstate what's locked** (`src/VaultFactory.sol:81`). For tokens that take a fee on transfer, the event records the amount requested, not what the vault received. The radar reads this event to show locked amounts.\n- **Info: anyone can file vaults under any project's token** (`src/VaultFactory.sol:145`). Once the project has paid, further vaults are free, and a stranger can register a treasury under another project's token with themselves as manager. Nothing can be stolen, but the radar must not treat that list as the project's own vaults.\n\nNothing is committed.","treeHash":"eba8e1f890207bcf68b8b1f5dc590c533457047c","usage":{"cachedInputTokens":1507563,"inputTokens":40,"model":"claude-opus-5-5","outputTokens":41436,"runtime":"claude","turns":22,"wallClockMs":514295}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4dd74fd7c315da80","findings":[{"citation":"resolved","description":"The README, the brief and the factory NatSpec promise that a vesting vault 'unlocks at most 25% a week (duration of at least 4 weeks after the cliff)'. The constructor only enforces duration_ >= 28 days; neither VestingVault nor VaultFactory.createVesting/createLaunchVesting bounds start_, so start may be any past timestamp. vestedAmount() computes elapsed = timestamp - (start + cliff) against totalAllocation(), so a vault created with start 28 days in the past, cliff 0 and duration 28 days is fully vested in the block that creates it. The IMD RADAR seal ('Locked with IMD Vault', >=3% of supply in vaults) reads the registry and feePaid, not the schedule, so a team can pay 5 IMD, lock 3% of supply in such a vault, obtain the seal and release everything in the next transaction. The existing fuzz test testFuzz_VestingNeverMoreThan25PercentAWeek always uses start = block.timestamp and samples 'at' from block.timestamp onward, so this edge is untested. Minimal fix preserving the design: in the VestingVault constructor (or in both factory entry points) revert BadSchedule when start_ < block.timestamp (or clamp start to max(start_, block.timestamp)). Add a Foundry test that creates a vault with a past start and asserts either a revert or <=25% releasable a week after creation.","line":65,"path":"src/VestingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {VestingVault} from \"src/VestingVault.sol\";\n\ncontract PastStartToken is ERC20 {\n    constructor() ERC20(\"T\", \"T\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\n/// @notice The README and brief promise that a vesting vault \"unlocks at most 25% a week\". Nothing bounds `start`,\n///         so a vault created with `start` 28 days in the past (cliff 0, duration 28 days) is 100% releasable in the\n///         very transaction that created it. This test passes once the factory (or the vault constructor) refuses a\n///         `start` before `block.timestamp`, or once the schedule is anchored no earlier than creation.\ncontract VestingPastStartTest is Test {\n    VaultFactory factory;\n    PastStartToken token;\n    address team = makeAddr(\"team\");\n    address project = makeAddr(\"project\");\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        // Robinhood-style factory: no fee, so the test needs no IMD.\n        factory = new VaultFactory(IERC20(address(0)), address(0));\n        token = new PastStartToken();\n        token.mint(project, 1_000_000e18);\n        vm.prank(project);\n        token.approve(address(factory), type(uint256).max);\n    }\n\n    function test_PastStartUnlocksEverythingAtOnce() public {\n        uint64 pastStart = uint64(block.timestamp - 28 days);\n        vm.prank(project);\n        try factory.createVesting(IERC20(address(token)), team, 1_000_000e18, pastStart, 0, 28 days) returns (VestingVault v) {\n            // Created: then the 25%-a-week rule must hold from creation onwards.\n            vm.warp(block.timestamp + 7 days);\n            uint256 releasable = v.releasable(address(token));\n            assertLe(releasable, 250_000e18 + 1, \"more than 25% releasable within a week of creation\");\n            if (releasable > 0) v.release(address(token));\n            assertLe(token.balanceOf(team), 250_000e18 + 1, \"more than 25% left within a week of creation\");\n        } catch {\n            // Refusing a start in the past is an acceptable fix.\n        }\n    }\n}","reproduction":"Factory on any chain (fee irrelevant). project approves 1,000,000 tokens and calls factory.createVesting(token, team, 1_000_000e18, uint64(block.timestamp - 28 days), 0, 28 days). Expected: the call is refused, or at most 250,000 tokens are releasable within the first 7 days. Actual: the call succeeds and v.releasable(token) == 1_000_000e18 immediately; release(token) pays the team 100% in the same block. Proof: test/scratch/VestingPastStart.t.sol fails with 'more than 25% releasable within a week of creation: 1e24 > 2.5e23+1'.","severity":"medium","snippet":"        if (duration_ < MIN_DURATION) revert BadSchedule();","title":"Vesting schedule is anchored to a creator-chosen start with no floor: a past start makes 100% releasable at creation"},{"citation":"resolved","description":"README 'Locking a launch's supply before the token exists' says the remainder 'arrives in the vault when the token launches, and vests ... from then on'. The code does not do that: vestedAmount(asset, t) multiplies the live totalAllocation(asset) (balance + released) by elapsed/duration, where elapsed is measured from the fixed start + cliff chosen at creation. Any token that lands after unlock has begun is immediately vested in the same proportion as the elapsed time, and anything landing after start + cliff + duration is 100% releasable at once. For the intended use (createLaunchVesting before a launch, vault named as remainderTo) the launch date is not under the vault's control: if the launch slips past start + cliff, part or all of the project's remainder is withdrawable the moment it arrives, which breaks the '25% a week' guarantee the seal advertises. The same applies to top-ups of a bound vault after unlock begins (the existing test test_VestingTopUpFollowsSchedule only tops up at a point where the proportional unlock is what the author expects). This is a distinct mechanism from the past-start finding: a start floor does not fix it. Possible fix preserving the design: track, per asset, the first time the vault observed a nonzero balance (set lazily in release()/a public sync(asset)) and measure elapsed from max(start + cliff, firstSeen[asset]) for the tranche that arrived then; or, simpler, document that the schedule is calendar-based and have the radar refuse the seal when start + cliff is in the past relative to the launch. The adapter must decide which; both need a test where tokens arrive after start + cliff.","line":90,"path":"src/VestingVault.sol","reproduction":"vm.warp(1_760_000_000). project calls factory.createLaunchVesting(team, uint64(block.timestamp), 0, 28 days). Launch slips: vm.warp(+28 days). The launch mints 1_000_000e18 of the new token to the vault. Expected per README: vesting starts from arrival, 0 releasable now and at most 25% after a week. Actual: v.releasable(token) == 1_000_000e18 immediately and release(token) pays the team the whole remainder. Reproduced by test_LateArrivalUnlocksOnArrival in test/scratch/Leads.t.sol (passes on current code, demonstrating the behaviour).","severity":"medium","snippet":"        uint256 total = totalAllocation(asset);","title":"Tokens that reach an open (launch) vault after start+cliff are unlocked on arrival instead of vesting from then on"},{"citation":"resolved","description":"The brief states 'at most 25% of a balance can leave per week' and the README 'no more than rateBps (at most 25%) of the balance can leave per asset per 7-day epoch'. The cap is keyed to block.timestamp / 7 days, so two withdrawals straddling an epoch boundary share no window: 25% of the balance one second before the boundary and 25% of the remaining 75% (18.75%) one second after, i.e. 43.75% of the original balance in any rolling week, and the manager controls timing via queue()/delay. The dev comment on line 13-14 documents the fixed-epoch design, so this is a weaker guarantee than the objective's wording rather than an unintended bug; it is reported so the adapter and the radar text state the real bound (at most rate + rate*(1-rate) per rolling week) or the window is made rolling (e.g. a 7-day ring of spent amounts). Rate decreases via decreaseRate() also only take effect at the next epoch because win.cap is frozen at the first execute of the epoch.","line":149,"path":"src/TreasuryVault.sol","reproduction":"createTreasury(token, project, 1 days, 2500, []) holding 10,000 tokens. Manager queues A = 2,500 and B = 1,875 to team. vm.warp(boundary - 1) where boundary = (block.timestamp / 7 days + 1) * 7 days (both past the 1-day delay): execute(A) succeeds (cap 2,500). vm.warp(boundary): execute(B) succeeds (new cap 7,500 * 25% = 1,875). Expected under 'at most 25% per week': the second execute reverts OverWeeklyCap. Actual: team holds 4,375 tokens (43.75%) after two seconds. Reproduced by test_TreasuryEpochBoundary in test/scratch/Leads.t.sol.","severity":"low","snippet":"        uint64 epoch = uint64(block.timestamp / EPOCH);","title":"Treasury weekly cap is per fixed calendar epoch, so 43.75% of a balance can leave in two consecutive seconds"},{"citation":"resolved","description":"Neither the constructor nor the factory checks that start_ + cliff_ fits in uint64. createVesting pulls the tokens into the vault first, and every later releasable()/release()/vestedAmount() call reverts with a checked-arithmetic panic (0x11) on this line, so the tokens can never leave: there is no emergency withdrawal by design, recover() refuses the vested token, and extendDuration() cannot help. The loss is self-inflicted by whoever created the vault, but the launch flow has a project create the vault from a UI/body and the vault cannot be undone, so a typo in start or cliff is unrecoverable. Fix: in the constructor compute unlockStart = start_ + cliff_ (reverting BadSchedule on overflow, and also bounding it to a sane horizon such as start_ + cliff_ + duration_ fitting in uint64), and add a test that createVesting(token, team, 1000e18, type(uint64).max, 1, 28 days) reverts.","line":88,"path":"src/VestingVault.sol","reproduction":"project calls factory.createVesting(token, team, 1000e18, type(uint64).max, 1, 28 days). Expected: BadSchedule revert, no tokens moved. Actual: the call succeeds, the vault holds 1000 tokens, and v.releasable(token) and v.release(token) revert with an arithmetic overflow panic forever. Reproduced by test_StartPlusCliffOverflowLocksForever in test/scratch/Leads.t.sol.","severity":"low","snippet":"        uint64 unlockStart = start + cliff;","title":"start + cliff is not validated, so an overflowing schedule locks the deposited tokens forever"},{"citation":"resolved","description":"recover() decides 'other token' by address equality only. Tokens with two entry points to one balance (TrueUSD's legacy address, Synthetix-style proxy/target pairs, any token with a forwarding legacy contract) pass the check when called with the secondary address, and the secondary's transfer() moves the vested balance to the beneficiary during the cliff. The beneficiary is the only recipient, so this is not theft from a third party, but it breaks the vesting guarantee and the seal for such tokens, and anyone can call recover(). Minimal fix: in a bound vault record balanceOf(token) before the transfer and revert if it decreased afterwards (the standard double-entry guard); add a test with a forwarding mock token.","line":157,"path":"src/VestingVault.sol","reproduction":"Deploy a token T and a forwarder L whose balanceOf/transfer call T on behalf of msg.sender (mock in test/scratch/Leads.t.sol). createVesting(T, team, 1000e18, now, 365 days, 28 days) so nothing vests for a year. Anyone calls v.recover(IERC20(L)). Expected: revert VestingToken, vault still holds 1000 T. Actual: team receives 1000 T during the cliff; vault balance 0. Reproduced by test_RecoverViaSecondEntryPoint.","severity":"low","snippet":"        if (vests(address(otherToken))) revert VestingToken();","title":"recover() on a bound vault lets a double-entry-point token leave before it vests"},{"citation":"resolved","description":"The objective says the factory 'charges each project 5 IMD once'. Open vaults are keyed to their own address (feePaid[chainId][vault]) and registered under token address(0), and nothing later links the vault to the token that lands in it. So a project that follows the README's launch flow (createLaunchVesting, then after launch createVesting or createTreasury for its real token) is charged again, because feePaid[chainId][token] is still false. The radar also cannot find the open vault via vaultsByToken(token). No funds are at risk and the contracts cannot know the token in advance, so this is informational: either document that the launch-remainder vault is a separate 5 IMD, or let a project claim the token for a paid open vault off-chain (radar) rather than in the contracts.","line":93,"path":"src/VaultFactory.sol","reproduction":"On the Ethereum factory: project calls createLaunchVesting(team, now, 0, 28 days) -> radar balance 5 IMD. Launch sends the token to that vault. project calls createVesting(token, team, 1e18, now, 0, 28 days) -> radar balance 10 IMD. Expected per objective: 5 IMD once per project. Reproduced by test_FeeTwiceForOneProject in test/scratch/Leads.t.sol.","severity":"info","snippet":"        _chargeOnce(address(vault));","title":"A project that makes a pre-launch vault and later a bound vault pays the 5 IMD fee twice"},{"citation":"resolved","description":"The rule 'treasury withdrawals wait at least 24h' is checked in VaultFactory.createTreasury (MIN_DELAY) but not in TreasuryVault, so a TreasuryVault deployed directly (not through the factory) can have delay 0 while presenting the same ABI and source. Such a vault is not in the registry (isVault is false), so the radar should not show it, but a reader verifying 'the rules are in the vault code' finds the floor missing. Defence in depth: move the MIN_DELAY check into the TreasuryVault constructor (keep it in the factory too) and test that new TreasuryVault(token, manager, 0, 2500, []) reverts.","line":96,"path":"src/TreasuryVault.sol","reproduction":"new TreasuryVault(token, manager, 0, 2500, new address[](0)) deploys and t.delay() == 0; a queued withdrawal is executable in the same block. Expected: BadDelay. Reproduced by test_DirectTreasuryHasNoMinDelay in test/scratch/Leads.t.sol.","severity":"info","snippet":"        if (delay_ > MAX_DELAY) revert BadDelay();","title":"TreasuryVault's own constructor does not enforce the 24h minimum delay; only the factory does"},{"citation":"resolved","description":"Read in full: src/VaultFactory.sol, src/VestingVault.sol, src/TreasuryVault.sol, test/Vault.t.sol, test/Fork.t.sol, README.md, SWARM_BRIEF.md, foundry.toml; dependencies lib/openzeppelin-contracts 5.1.0 (IERC20, SafeERC20, ReentrancyGuard) and forge-std. Existing suite: 38 tests pass. Not reached: the live IMD token on Ethereum (Fork.t.sol needs FORK_RPC and was not run), the swarm launch factory that pays remainderTo, and the radar's off-chain seal logic. Checked and found no defect: no owner/admin/pause/upgrade/emergency path in any contract; no DELEGATECALL, CALLCODE, SELFDESTRUCT, proxy or initializer; constructors make no external calls (safe on the empty-chain rehearsal); VaultFactory constructor arguments imd_=0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7, feeRecipient_=$owner give $owner only the fee stream; fee accounting sets feePaid before the IMD pull and reverts atomically; release()/execute()/deposit() follow checks-effects-interactions under nonReentrant; weekly cap and delay/grace arithmetic cannot underflow; rules only tighten (duration up, delay up to 30 days, rate down but never zero, destinations only removed with one kept); fee-on-transfer and rebasing tokens are measured by balance. Residual trust assumptions: anyone may register vaults for any token (registry spam is an off-chain filtering concern); the beneficiary/manager timelocks have no expiry on acceptance; Foundry's default timestamp 1 puts the treasury in epoch 0 where the Window zero-value equals the current epoch (first execute sees cap 0) - a test-environment artefact only, the mainnet epoch is ~2960.","line":8,"path":"README.md","reproduction":"Not a defect. Commands run: forge test --no-match-contract Fork (38 passed); forge test --match-path 'test/scratch/*' (proof fails as stated, 6 lead reproductions pass).","severity":"info","snippet":"## Contracts (`src`)","title":"Coverage: contracts read, checks passed, and what could not be reached"}],"hash":"16a6d214f7e8e580d2aa0b88f19f611d780cb49dd0543514fe488d1a41b0138a","nodeId":"54285071-4340-43e8-8999-3b323fa53ae4","outcome":"completed","summary":"The findings file is in place, every snippet matches its line, and the only new files are the findings JSON and the scratch tests, which the harness itself excludes from the tree. No source, config or library file was changed.\n\n**Outcome: 2 medium, 3 low, 3 info findings in `.imd-findings.json`.** The proof test for the top finding fails on the current code and is attached.\n\n**Contracts read:** VaultFactory, VestingVault, TreasuryVault, both test files, README, brief, foundry.toml, and the OpenZeppelin 5.1.0 pieces they import. The existing suite passes (38 tests). **Not reached:** the live IMD token on Ethereum (the fork test needs an RPC and was not run), the swarm launch factory that pays `remainderTo`, and the radar's off-chain seal logic.\n\nFindings, most severe first:\n\n- **Medium, with proof.** Nothing bounds `start`, so a vesting vault created with a start 28 days in the past, cliff 0 and duration 28 days is 100% releasable in the block that creates it. The seal reads the registry, not the schedule. Minimal fix: reject or clamp a start before `block.timestamp` in the VestingVault constructor.\n- **Medium.** Tokens that land in an open launch vault after `start + cliff` are unlocked on arrival in proportion to elapsed time, contrary to the README's \"vests from then on\". A slipped launch can make the whole remainder withdrawable at once. A start floor does not fix this. The adapter must choose between per-asset arrival anchoring and documenting the calendar schedule.\n- **Low.** The treasury cap is per fixed calendar epoch, so 43.75% of a balance can leave across one epoch boundary. This is documented in a dev comment but weaker than the objective's wording.\n- **Low.** `start + cliff` is unvalidated. An overflowing schedule pulls the tokens in and then every release reverts forever.\n- **Low.** `recover()` on a bound vault lets a double-entry-point token leave during the cliff. The standard balance-before-and-after guard fixes it.\n- **Info.** A project that makes a pre-launch vault and later a bound vault pays the 5 IMD fee twice, since nothing links the open vault to its token.\n- **Info.** TreasuryVault's own constructor does not enforce the 24h minimum delay. Only the factory does.\n- **Info.** Coverage note listing what was read, what passed the checklist (no owner, pause, upgrade, delegatecall, selfdestruct, proxy, or constructor external calls; reentrancy and accounting hold), and what could not be reached.\n\nThe remaining leads each have a reproducing test in `test/scratch/Leads.t.sol` that passes on the current code to demonstrate the behaviour.","treeHash":null,"usage":{"cachedInputTokens":798169,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":29123,"runtime":"claude","turns":22,"wallClockMs":410875}},{"artifacts":[],"attempt":1,"bundleHash":"c2c8e3f7616c444031f235958b5d1ebc4d31ee537f930228b5f7cd50d70a564c","device":"c0fc4ea4f50e3380","findings":[],"hash":"2720833a198d41c0f4b42095980f27695fc2a73dfe9888c396fb3d54cba684be","nodeId":"b3965e6a-8d53-46eb-bf71-9288fcc06e28","outcome":"completed","summary":"The latest implementation round had broken my earlier tests' build. They compile again, I added tests for what that round changed, and `forge test` now passes: 109 tests, both with the verifier's seed and with a second seed. I didn't change any source files and didn't write `.imd-findings.json`, because nothing I probed showed a defect I could reproduce.\n\n**What broke:** treasury withdrawals gained an `expiresAt` field, so my tests' reads of them no longer compiled. I fixed those reads in `test/EdgeCases.t.sol` and `test/Invariants.t.sol`. In the random-sequence treasury tests, I replaced the old expiry check (`queuedAt + current delay + 14 days`) with a stricter one: the expiry is recorded when a withdrawal is queued, and the tests check it never moves and that nothing executes after it. The factory random-sequence test now also checks that every vault is recorded under the wallet that created it.\n\n**New file `test/SharedCap.t.sol`:**\n- **Random-sequence tests over the new shared weekly cap.** A test token has two addresses that share one balance, and a second unrelated token sits beside it. A test contract deposits, queues through either address, cancels, executes, tightens the rate and delay, and moves time forward. After every step two things are checked:\n  - Within each 7-day period, the total leaving the shared balance stays within one 25% cap, whichever address is used.\n  - Every token held is accounted for.\n\n  My first version passed without exercising anything: almost no withdrawal ever executed, because the test also called the helper's read-only functions and tightened the rules in big jumps. I fixed that and confirmed that runs now reach real withdrawals and real over-cap refusals.\n- **Boundary tests:**\n  - A withdrawal can execute exactly at its expiry and is refused one second later.\n  - When a delay increase moves the due time onto the expiry, there is exactly one second left to execute.\n  - A fuzz test checks the expiry never moves when the delay is raised.\n  - The same asset is tracked once, ETH is never tracked, and only the manager can add assets.\n  - When the rate is lowered, cap periods already open keep their limit and new ones use the lower rate.\n  - The documented 43.75% maximum across two weeks' cap periods holds.\n  - A tracked token whose balance can't be read doesn't block other withdrawals, but can't itself leave until it can be read.\n  - A manager handover restarts its waiting period when proposed again and clears when cancelled.\n  - Each open treasury costs its own fee.\n\n**What I tried and couldn't break** in the new treasury code:\n- **Order of use:** withdrawing through one address and then the other, in either order.\n- **Late tracking:** the second address is only queued for the first time later in the same week.\n- **Week boundaries:** withdrawals on either side of the switch to a new 7-day period.\n- **Unreadable balances:** a tracked token whose balance read fails, including deliberately starving that read of gas. That would need a balance read costing millions of gas, so it isn't practical.\n\nThe changes are in the working tree and not committed.","treeHash":"5188399bc053ede4d9e1a4d79f524613b9bc207f","usage":{"cachedInputTokens":1366018,"inputTokens":40,"model":"claude-opus-5-5","outputTokens":27261,"runtime":"claude","turns":21,"wallClockMs":344421}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fb1af3f11ddf6470","findings":[{"citation":"resolved","description":"In an open vault (token == 0) `vests(asset)` is true for every address, and `totalAllocation`, `released`, `arrivedAt` and `releasable` are all keyed by the token *address*. A token with two entry points to one balance (TrueUSD/SNX-style legacy or proxy forwarder, which the team itself chooses when it launches into this vault) therefore gets two independent schedules over the same balance: `release(main)` pays the vested share, and `release(legacy)` then pays the vested share of what is left again. Each extra entry point adds another 25%-of-remainder per week, so the beneficiary breaks the brief's headline rule 'vesting unlocks at most 25% a week'. The project already treats this token class as in scope: `recover()` was fixed for exactly this mechanism (ADAPTATION change 4, balance read before/after), but the parallel `release()` path of the open vault has no such guard (asymmetry between the two exits of the same balance). Bound vaults are not affected (`vests` only accepts the bound address and `recover` checks the balance). Minimal fix keeping the design: when an open vault releases `asset`, refuse (revert) if the balance of any *other* already-recorded asset (arrivedAt != 0) fell during the transfer, or equivalently snapshot the balance of each recorded asset before the transfer, mirroring `recover`'s check.","line":102,"path":"src/VestingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {VestingVault} from \"src/VestingVault.sol\";\n\ncontract MintableToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Two entry points to one balance (TrueUSD-style legacy forwarder).\ncontract DoubleEntryToken is MintableToken {\n    address public legacy;\n\n    constructor() MintableToken(\"DBL\") {}\n\n    function setLegacy(address legacy_) external {\n        legacy = legacy_;\n    }\n\n    function transferFor(address from, address to, uint256 amount) external {\n        require(msg.sender == legacy, \"not legacy\");\n        _transfer(from, to, amount);\n    }\n}\n\ncontract LegacyEntry {\n    DoubleEntryToken immutable main;\n\n    constructor(DoubleEntryToken main_) {\n        main = main_;\n    }\n\n    function balanceOf(address a) external view returns (uint256) {\n        return main.balanceOf(a);\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        main.transferFor(msg.sender, to, amount);\n        return true;\n    }\n}\n\n/// An open (launch) vesting vault counts each token address separately, so a token with a second entry point\n/// to the same balance vests twice: more than 25% of the remainder leaves in the first week.\ncontract OpenVaultDoubleEntryTest is Test {\n    address radar = makeAddr(\"radar\");\n    address project = makeAddr(\"project\");\n    address team = makeAddr(\"team\");\n\n    function test_OpenVaultNeverReleasesMoreThan25PercentInFirstWeekThroughTwoEntryPoints() public {\n        vm.warp(1_760_000_000);\n        MintableToken imd = new MintableToken(\"IMD\");\n        VaultFactory factory = new VaultFactory(IERC20(address(imd)), radar);\n        imd.mint(project, 5e18);\n        vm.startPrank(project);\n        imd.approve(address(factory), type(uint256).max);\n        VestingVault v = factory.createLaunchVesting(team, uint64(block.timestamp), 0, 28 days);\n        vm.stopPrank();\n\n        // The launch pays the remainder into the open vault.\n        DoubleEntryToken dbl = new DoubleEntryToken();\n        LegacyEntry legacy = new LegacyEntry(dbl);\n        dbl.setLegacy(address(legacy));\n        dbl.mint(address(v), 1000e18);\n        v.sync(address(dbl));\n        v.sync(address(legacy)); // anyone can: the legacy address also reports a balance\n\n        vm.warp(block.timestamp + 7 days); // one week of a four-week schedule: 25% may be out\n        try v.release(address(dbl)) {} catch {}\n        try v.release(address(legacy)) {} catch {}\n\n        // 250e18 is the most that may have left; the vault pays 250 + 187.5 = 437.5e18.\n        assertLe(dbl.balanceOf(team), 250e18, \"more than 25% of the vested supply left in one week\");\n    }\n}","reproduction":"1) factory.createLaunchVesting(team, now, 0, 28 days). 2) Launch mints 1000e18 of DoubleEntryToken DBL (with LegacyEntry L forwarding to the same balance) to the vault. 3) Anyone calls sync(DBL) and sync(L) (both report balance 1000e18, both pass `vests`). 4) warp +7 days. 5) release(DBL) pays 250e18; release(L) then computes totalAllocation(L) = 750e18 + 0 and pays 25% of it = 187.5e18 out of the same balance. Expected: team holds at most 250e18 after one week of a 4-week schedule. Actual: 437.5e18 (43.75%); with N entry points 1-(0.75)^N in week one. Run: forge test --match-path test/scratch/OpenVaultDoubleEntry.t.sol -> FAIL 437500000000000000000 > 250000000000000000000.","severity":"medium","snippet":"    function totalAllocation(address asset) public view returns (uint256) {\n        return IERC20(asset).balanceOf(address(this)) + released[asset];\n    }","title":"Open vesting vault vests a double-entry token once per entry point: more than 25% of the remainder unlocks in the first week"},{"citation":"resolved","description":"`execute` keys the weekly window by `w.asset` and computes the cap from `_balance(w.asset)`. The manager may `queue` any asset address. For a token with two entry points to one balance (the project's own token is chosen by the team; TrueUSD-style legacy forwarders exist), queuing withdrawals against both addresses gives two independent windows over one balance: 25% via the main address, then 25% of the remainder via the legacy address, in the same epoch and even the same block. This breaks the brief's rule 'at most 25% of a balance can leave per week' and the README's bound of 43.75% in any 7 days (with the epoch straddle it rises further, and with N entry points to 1-(0.75)^N per epoch). Access x asymmetry: `recover()` in VestingVault got a balance-before/after guard for this exact token class, the treasury's capped exit did not. Minimal fix keeping the design: in `execute`, for an ERC-20, read the balances of the other assets that have a window in the current epoch (or of every asset ever queued) before and after the transfer and revert if one fell, so a second entry point can never spend a balance through a fresh window.","line":159,"path":"src/TreasuryVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {TreasuryVault} from \"src/TreasuryVault.sol\";\n\ncontract MintableToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract DoubleEntryToken is MintableToken {\n    address public legacy;\n\n    constructor() MintableToken(\"DBL\") {}\n\n    function setLegacy(address legacy_) external {\n        legacy = legacy_;\n    }\n\n    function transferFor(address from, address to, uint256 amount) external {\n        require(msg.sender == legacy, \"not legacy\");\n        _transfer(from, to, amount);\n    }\n}\n\ncontract LegacyEntry {\n    DoubleEntryToken immutable main;\n\n    constructor(DoubleEntryToken main_) {\n        main = main_;\n    }\n\n    function balanceOf(address a) external view returns (uint256) {\n        return main.balanceOf(a);\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        main.transferFor(msg.sender, to, amount);\n        return true;\n    }\n}\n\n/// The treasury's weekly cap is kept per asset address, so a token with a second entry point to the same\n/// balance gets two caps in one epoch: 43.75% of the balance leaves in one transaction block, not 25%.\ncontract TreasuryDoubleEntryTest is Test {\n    address radar = makeAddr(\"radar\");\n    address project = makeAddr(\"project\");\n    address manager = makeAddr(\"manager\");\n    address dest = makeAddr(\"dest\");\n\n    function test_TreasuryWeeklyCapHoldsAcrossTwoEntryPointsToOneBalance() public {\n        vm.warp(1_760_000_000);\n        MintableToken imd = new MintableToken(\"IMD\");\n        VaultFactory factory = new VaultFactory(IERC20(address(imd)), radar);\n        imd.mint(project, 5e18);\n        DoubleEntryToken dbl = new DoubleEntryToken();\n        LegacyEntry legacy = new LegacyEntry(dbl);\n        dbl.setLegacy(address(legacy));\n\n        vm.startPrank(project);\n        imd.approve(address(factory), type(uint256).max);\n        TreasuryVault t = factory.createTreasury(address(dbl), manager, 1 days, 2500, new address[](0));\n        vm.stopPrank();\n        dbl.mint(address(t), 1000e18);\n\n        vm.startPrank(manager);\n        uint256 a = t.queue(address(dbl), dest, 250e18);\n        uint256 b = t.queue(address(legacy), dest, 187.5e18);\n        vm.stopPrank();\n\n        vm.warp(block.timestamp + 1 days);\n        t.execute(a);\n        try t.execute(b) {} catch {}\n\n        assertLe(dbl.balanceOf(dest), 250e18, \"more than 25% of the balance left in one epoch\");\n    }\n}","reproduction":"1) factory.createTreasury(DBL, manager, 1 days, 2500, []) ; 1000e18 DBL minted to the treasury; L = LegacyEntry forwarding to DBL. 2) manager: queue(DBL, dest, 250e18) -> id a; queue(L, dest, 187.5e18) -> id b. 3) warp +1 day; execute(a) (window[DBL] cap 250e18) ; execute(b) (window[L] is fresh, cap = 750e18*25% = 187.5e18). Expected: total out in the epoch <= 250e18 (25%). Actual: dest receives 437.5e18 in one block. Run: forge test --match-path test/scratch/TreasuryDoubleEntry.t.sol -> FAIL 437500000000000000000 > 250000000000000000000.","severity":"medium","snippet":"        Window storage win = windows[w.asset];","title":"Treasury weekly cap is per asset address: a double-entry token gets one 25% cap per entry point in the same epoch"},{"citation":"resolved","description":"Expiry is computed from the *current* delay, not the delay the withdrawal was announced with. `increaseDelay` is meant to only tighten rules, but it moves every queued withdrawal's expiry window forward, so a withdrawal that already expired (announced eta + 14 days passed, which the radar and holders read as dead, and which the manager may have deliberately let lapse instead of cancelling) becomes executable again. By choosing newDelay = now - queuedAt (any value up to 30 days) the manager makes it executable immediately, and since `execute` is permissionless anyone can then pay it out, with no new `Queued` event. This violates 'executes after the delay ... within 14 days' (README) and 'every rule can only get stricter': the stricter delay loosens the expiry. Minimal fix: store the expiry at queue time (e.g. `expiresAt = queuedAt + delay + GRACE` in the Withdrawal struct) and check `block.timestamp > w.expiresAt` in execute, while keeping `due = queuedAt + delay` with the current delay so tightening still holds queued withdrawals back.","line":154,"path":"src/TreasuryVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {TreasuryVault} from \"src/TreasuryVault.sol\";\n\ncontract MintableToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// A withdrawal that expired (announced eta + 14 days passed) comes back to life when the manager makes the\n/// delay *stricter*, and executes in the same block, with no new announcement.\ncontract ExpiredRevivalTest is Test {\n    address radar = makeAddr(\"radar\");\n    address project = makeAddr(\"project\");\n    address manager = makeAddr(\"manager\");\n    address dest = makeAddr(\"dest\");\n    address stranger = makeAddr(\"stranger\");\n\n    function test_IncreasingTheDelayDoesNotReviveAnExpiredWithdrawal() public {\n        vm.warp(1_760_000_000);\n        MintableToken imd = new MintableToken(\"IMD\");\n        MintableToken token = new MintableToken(\"PROJ\");\n        VaultFactory factory = new VaultFactory(IERC20(address(imd)), radar);\n        imd.mint(project, 5e18);\n        vm.startPrank(project);\n        imd.approve(address(factory), type(uint256).max);\n        TreasuryVault t = factory.createTreasury(address(token), manager, 1 days, 2500, new address[](0));\n        vm.stopPrank();\n        token.mint(address(t), 1000e18);\n\n        uint256 queuedAt = block.timestamp;\n        vm.prank(manager);\n        uint256 id = t.queue(address(token), dest, 250e18); // eta queuedAt + 1 day, expires at + 15 days\n\n        vm.warp(queuedAt + 20 days);\n        vm.expectRevert(TreasuryVault.Expired.selector);\n        t.execute(id);\n\n        // A \"stricter\" rule: the delay goes from 1 day to 20 days.\n        vm.prank(manager);\n        t.increaseDelay(20 days);\n\n        // The expired withdrawal must stay dead.\n        vm.prank(stranger);\n        try t.execute(id) {} catch {}\n        assertEq(token.balanceOf(dest), 0, \"expired withdrawal executed after the delay was increased\");\n    }\n}","reproduction":"1) createTreasury(token, manager, 1 days, 2500, []); 1000e18 token in the vault. 2) T0: manager queue(token, dest, 250e18) -> eta T0+1d, expiry T0+15d. 3) warp T0+20d: execute(id) reverts Expired (as intended). 4) manager increaseDelay(20 days). 5) Same block, stranger execute(id). Expected: still Expired, dest receives 0. Actual: due = T0+20d, executes, dest receives 250e18. Run: forge test --match-path test/scratch/ExpiredRevival.t.sol -> FAIL 250000000000000000000 != 0.","severity":"medium","snippet":"        uint64 due = w.queuedAt + delay;\n        if (block.timestamp < due) revert TooEarly(due);\n        if (block.timestamp > due + GRACE) revert Expired();","title":"Making the delay stricter revives expired withdrawals, executable by anyone in the same block"},{"citation":"resolved","description":"The manager-change eta is frozen at proposal time, while queued withdrawals are re-evaluated against the current delay ('Tightening a rule (longer delay, removed destination) also holds back withdrawals already queued', README). The two delayed actions are treated asymmetrically: after `increaseDelay`, an earlier proposal still completes on the old schedule, so the radar's displayed delay (e.g. 30 days) does not hold for a manager change that was already pending. Funds are still protected by the withdrawal delay, so impact is limited to the advertised notice period for control changes. Fix: in `acceptManager`, require `block.timestamp >= proposedAt + delay` using the current delay (store the proposal time instead of the eta), consistent with `execute`.","line":223,"path":"src/TreasuryVault.sol","reproduction":"createTreasury(token, manager, 1 days, 2500, []). T0: manager proposeManager(M2) (eta T0+1d); manager increaseDelay(30 days). warp T0+1d: M2 acceptManager() succeeds. Expected per README ('A new manager takes over after the delay', tightening holds back pending actions): revert TooEarly until T0+30d. Actual: manager() == M2 after 1 day.","severity":"low","snippet":"        pendingManagerEta = uint64(block.timestamp) + delay;","title":"A pending manager change keeps the old (shorter) delay after the delay is increased"}],"hash":"45492542bb2eb5bc91a912e2ac22fbe71c444c9159580733d9114e73fb442363","nodeId":"cbe79cfc-518f-4176-bccf-2dfe42dd8002","outcome":"completed","summary":"I found four defects in the permission area and wrote them to `.imd-findings.json`, with a coverage row for all 23 entry points plus two invariant rows. Each of the three medium findings comes with a Foundry proof, and I ran each proof to confirm it fails on the current code for the stated reason. I changed no source files; the scratch tests have been deleted.\n\n**Findings**\n1. **Medium – `src/VestingVault.sol:102`:** an open (launch) vesting vault tracks each token by its address. A token with two addresses sharing one balance therefore vests twice. On a 4-week schedule, `sync` both addresses and call `release` on each after one week: the beneficiary gets 437.5e18 of 1000e18 (43.75%) instead of at most 25%. `recover()` was already fixed against this same kind of token, but `release()` was not.\n2. **Medium – `src/TreasuryVault.sol:159`:** the treasury's weekly cap is also kept per token address. The manager queues 250e18 against the main address and 187.5e18 against the second address, and both execute in the same block. That is 43.75% leaving in one week instead of 25%.\n3. **Medium – `src/TreasuryVault.sol:154`:** the 14-day expiry is measured from the current delay, so raising the delay brings expired withdrawals back. A withdrawal queued at T0 with a 1-day delay is expired at T0+20d. The manager then calls `increaseDelay(20 days)`, and in the same block anyone can `execute` it and pay out 250e18. So a change meant to make the rules stricter loosens the expiry. The fix is to store each withdrawal's expiry when it is queued.\n4. **Low – `src/TreasuryVault.sol:223`:** a manager change already proposed keeps the old, shorter delay. Propose a new manager with a 1-day delay, then `increaseDelay(30 days)`: the new manager can still accept after 1 day. I confirmed this with a quick test, but it has no proof attached.\n\n**Coverage:** five entry points are marked `finding` (`acceptManager`, `execute`, `increaseDelay`, `release`, `sync`). The other 18 are `holds`, none are `unreached`. I found no owner, admin, pause, upgrade or emergency path. The fee recipient's only role is receiving fees, and the factory keeps no funds or privileged state.\n\nI treated the static-analysis lines as leads. The strict-equality and timestamp warnings didn't lead to a defect in this area. I didn't report anything about third parties using `execute` to fix the epoch cap early, because that only makes the cap stricter.","treeHash":null,"usage":{"cachedInputTokens":718560,"inputTokens":22,"model":"claude-opus-5-5","outputTokens":23744,"runtime":"claude","turns":14,"wallClockMs":257030}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"60c045240bb09c33","findings":[{"citation":"resolved","description":"execute() keeps one Window per `w.asset` address and sizes its cap from `_balance(w.asset)`. A token with two entry points to one balance (TrueUSD's legacy contract forwarding to the main token, Synthetix ProxyERC20 and its target) passes as two assets: each address gets its own 25% window over the same underlying balance. The manager queues one withdrawal through each entry and executes both in the same 7-day epoch; the second is sized on the balance left after the first, so 25% + 25% x 75% = 43.75% of the treasury leaves in one epoch (about 68% across an epoch boundary, adding the documented straddle). The README rule 'no more than rateBps (at most 25%) of the balance can leave per asset per 7-day epoch' is a rule on the balance, not on the address. The same token class was accepted as finding 5f526bd2 for VestingVault.recover(), which now reads the vested balance before and after; the treasury has no equivalent guard. Trigger needs a double-entry asset and the manager (the role the cap exists to bound), so severity is low. Minimal fix that keeps the design: for a treasury with a nonzero projectToken, guard the project token the way recover() does: read `IERC20(projectToken).balanceOf(this)` before the transfer in execute() and, when `w.asset != projectToken`, revert if it fell. That closes the alias path for the asset the vault is filed under; for other assets, state in the README that the cap is per address and that double-entry tokens are not supported.","line":159,"path":"src/TreasuryVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {VestingVault} from \"src/VestingVault.sol\";\nimport {TreasuryVault} from \"src/TreasuryVault.sol\";\n\n/// @dev A token with two entry points to one balance (TrueUSD legacy contract, Synthetix proxy/target).\ncontract MainToken is ERC20 {\n    address public legacy;\n\n    constructor() ERC20(\"DBL\", \"DBL\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n\n    function setLegacy(address legacy_) external {\n        legacy = legacy_;\n    }\n\n    function transferFor(address from, address to, uint256 amount) external {\n        require(msg.sender == legacy, \"not legacy\");\n        _transfer(from, to, amount);\n    }\n}\n\ncontract LegacyEntry {\n    MainToken immutable main;\n\n    constructor(MainToken main_) {\n        main = main_;\n    }\n\n    function balanceOf(address a) external view returns (uint256) {\n        return main.balanceOf(a);\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        main.transferFor(msg.sender, to, amount);\n        return true;\n    }\n}\n\ncontract DoubleEntryTest is Test {\n    address radar = makeAddr(\"radar\");\n    address project = makeAddr(\"project\");\n    address team = makeAddr(\"team\");\n    address multisig = makeAddr(\"multisig\");\n\n    VaultFactory factory;\n    MainToken main;\n    LegacyEntry legacy;\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        factory = new VaultFactory(IERC20(address(0)), address(0)); // free factory: the fee is not the point\n        main = new MainToken();\n        legacy = new LegacyEntry(main);\n        main.setLegacy(address(legacy));\n    }\n\n    /// @notice The treasury's weekly cap is keyed by asset address, but both entries share one balance:\n    ///         43.75% of the balance leaves in one epoch, against the 25% rule.\n    function test_TreasuryCapIsPerAddressNotPerBalance() public {\n        vm.prank(project);\n        TreasuryVault t = factory.createTreasury(address(main), project, 1 days, 2500, new address[](0));\n        main.mint(address(t), 10_000e18);\n\n        vm.startPrank(project);\n        uint256 a = t.queue(address(main), multisig, 2_500e18);\n        uint256 b = t.queue(address(legacy), multisig, 1_875e18);\n        vm.stopPrank();\n\n        // Pick a moment with both executions well inside one epoch.\n        uint256 due = block.timestamp + 1 days;\n        uint256 epochStart = (due / t.EPOCH() + 1) * t.EPOCH();\n        vm.warp(epochStart + 1 hours);\n        t.execute(a);\n        t.execute(b); // the rule says this must be OverWeeklyCap; it is not\n        assertEq(main.balanceOf(multisig), 4_375e18);\n        assertLe(main.balanceOf(multisig), 2_500e18, \"more than 25% of the balance left in one epoch\");\n    }\n\n    /// @notice The open vesting vault vests \"every token it receives\" per address: releasing through both entries\n    ///         pays 43.75% in the first week, against the 25% ceiling.\n    function test_OpenVestingReleasesThroughBothEntries() public {\n        vm.prank(project);\n        VestingVault v = factory.createLaunchVesting(team, uint64(block.timestamp), 0, 28 days);\n        main.mint(address(v), 10_000e18);\n        v.sync(address(main));\n        v.sync(address(legacy));\n        vm.warp(block.timestamp + 7 days);\n        v.release(address(main)); // 25% of 10,000\n        v.release(address(legacy)); // 25% of the remaining 7,500 counted as a separate allocation\n        assertEq(main.balanceOf(team), 4_375e18);\n        assertLe(main.balanceOf(team), 2_500e18, \"more than 25% vested in the first week\");\n    }\n}","reproduction":"Treasury created with projectToken=main, delay=1 days, rateBps=2500, no destinations; vault holds 10,000e18 of `main`; `legacy` is a contract whose balanceOf/transfer forward to `main` (test/scratch/DoubleEntry.t.sol, MainToken + LegacyEntry). Manager calls queue(main, multisig, 2_500e18) then queue(legacy, multisig, 1_875e18). Warp to 1 hour into the next epoch after both are due. execute(a) then execute(b): both succeed. Expected: execute(b) reverts OverWeeklyCap(0) because 25% of the balance already left this epoch. Actual: multisig holds 4,375e18 = 43.75% of the balance after one epoch. Run: forge test --match-path test/scratch/DoubleEntry.t.sol --match-test test_TreasuryCapIsPerAddressNotPerBalance","severity":"low","snippet":"        Window storage win = windows[w.asset];\n        uint64 epoch = uint64(block.timestamp / EPOCH);\n        if (win.epoch != epoch) {\n            win.epoch = epoch;\n            win.cap = (_balance(w.asset) * rateBps) / BPS;","title":"TreasuryVault weekly cap is keyed per asset address, so a double-entry token moves 43.75% of one balance in a single epoch"},{"citation":"resolved","description":"In an open vault vests(asset) is true for every nonzero address, and the schedule runs per address on `balanceOf(this) + released[asset]`. A token reachable through two addresses that share one balance is therefore vested twice: release(main) pays 25% of the balance after one week, then release(legacy) treats the remaining 75% as a separate allocation that is also 25% vested and pays another 18.75%. 43.75% leaves in week one against the 25%-a-week ceiling the brief and README state, and the same again on the remainder each week. The bound vault is protected (recover() reads the vested balance before and after, finding 5f526bd2); the open vault has no check at all. The beneficiary triggers it, and only with a double-entry asset, so severity is low; the launch token itself is a plain ERC20, but an open vault accepts any token. Minimal fix that keeps the design: in release(), snapshot `IERC20(a).balanceOf(this)` for the assets already recorded in `arrivedAt` is not bounded; instead record, per asset, the balance observed at the last release and refuse a release of asset X whose transfer lowers the balance of a different recorded asset (check the one other recorded asset with the largest allocation, or keep a small enumerable list of recorded assets, which an open vault only grows by sync). Alternatively document the per-address accounting as a limitation and have the radar refuse double-entry tokens.","line":102,"path":"src/VestingVault.sol","reproduction":"createLaunchVesting(team, now, cliff 0, duration 28 days) on a free factory; mint 10,000e18 of `main` to the vault; sync(main); sync(legacy) where legacy forwards balanceOf/transfer to main. Warp +7 days. release(main) pays 2,500e18. release(legacy): expected NothingToRelease or at most 0 more (25% of the one balance is already out); actual it pays 1,875e18, team holds 4,375e18 after one week. Run: forge test --match-path test/scratch/DoubleEntry.t.sol --match-test test_OpenVestingReleasesThroughBothEntries","severity":"low","snippet":"    function totalAllocation(address asset) public view returns (uint256) {\n        return IERC20(asset).balanceOf(address(this)) + released[asset];\n    }","title":"Open VestingVault counts each address as its own allocation, so a double-entry token vests 43.75% in the first week"},{"citation":"resolved","description":"Both the due time and the expiry are derived from the current `delay` at execution, so raising the delay moves the whole 14-day execution window later. A withdrawal announced on day 0 with a 1-day delay expires on day 15. On day 16 it reverts Expired. The manager calls increaseDelay(10 days): the window is now day 10 to day 24, so the same withdrawal is executable immediately, in the same block as the delay change. Watchers who rely on `Queued` and the 24h-to-30-day notice treat an expired announcement as dead; the revived one gives them no reaction window at all, while a fresh queue() would give them at least `delay`. It is a loosening produced by the one setter that is meant only to tighten ('every rule can only get stricter'), reachable for any delay raise that lands `now` inside `queuedAt + newDelay .. + 14 days`, which is any withdrawal expired less than 30 days ago. Manager-triggered, so low. Minimal fix: fix the expiry at queue time (store `expiresAt = queuedAt + delay + GRACE` in the Withdrawal and check `block.timestamp > w.expiresAt`), keeping `due = queuedAt + delay` on the current delay so a raise still holds queued withdrawals back and, if it pushes `due` past `expiresAt`, simply makes them unexecutable until requeued. Add a test: queue, warp past expiry, increaseDelay, execute must still revert Expired.","line":154,"path":"src/TreasuryVault.sol","reproduction":"Treasury: delay 1 day, rate 2500, open destinations, 1000e18 of token. Manager: queue(token, multisig, 100e18) at t0. Warp t0+16 days: execute(id) reverts Expired (due t0+1d, expiry t0+15d). Manager: increaseDelay(10 days). Same block: execute(id) succeeds, multisig receives 100e18. Expected: an announcement that expired stays expired; at minimum a revived withdrawal should wait the new delay from the moment it was revived. Actual: executes with zero wait after the manager's action. test/scratch/Edges.t.sol test_IncreaseDelayCanReviveAnExpiredWithdrawal passes on the current code (it asserts the revival).","severity":"low","snippet":"        uint64 due = w.queuedAt + delay;\n        if (block.timestamp < due) revert TooEarly(due);\n        if (block.timestamp > due + GRACE) revert Expired();","title":"increaseDelay() revives an expired withdrawal and lets it execute in the same block, with no fresh announcement window"},{"citation":"resolved","description":"Documented design (README accounting note: 'later top-ups vest on the same schedule'; ADAPTATION change 2 keeps it for bound vaults and for a second tranche in an open vault). Consequence worth stating for the radar: once `elapsed >= duration` every token later sent to the vault is 100% releasable in the same block, so a bound or already-synced open vault can hold 3% of the supply, earn the 'Locked with IMD Vault' seal on balance, and release it in the next block. Not a contract defect under the brief's 'keep the rules exactly as written'; the radar should count `balanceOf(vault) - releasable(token)` as locked, not the balance. No change to the contracts proposed.","line":120,"path":"src/VestingVault.sol","reproduction":"createVesting(token, team, 1000e18, start=now, cliff=0, duration=28 days). Warp +28 days, release(token). Project transfers 1,000,000e18 to the vault. releasable(token) == 1,000,000e18 immediately; release(token) pays it in the same block. Same with createLaunchVesting after sync and 28 days. test/scratch/TopUp.t.sol, both tests pass on the current code (they assert the documented behaviour).","severity":"info","snippet":"        uint256 total = totalAllocation(asset);\n        uint64 elapsed = timestamp - from;\n        if (elapsed >= duration) return total;\n        return (total * elapsed) / duration;","title":"A tranche that reaches a vault after its schedule has run is releasable at once, so a vault's balance is not a measure of locked supply"}],"hash":"7ffa75066fec5807d1eae6113821d1787963c52ee9db65bafb8aa31d8bf3d994","nodeId":"37cfcb50-bf0a-44c4-b2ef-05d5ac54f4dc","outcome":"completed","summary":"The review is complete. The findings file at `.imd-findings.json` holds four findings and 28 coverage rows, all snippets verified against the tree, and no tracked file was changed (scratch tests live only under `test/scratch/`).\n\n**Findings**\n\n1. **Low, TreasuryVault.execute:** the weekly cap is keyed by asset address, so a double-entry token (TrueUSD legacy entry, Synthetix proxy/target) gets two 25% windows over one balance. Reproduced: 43.75% of the treasury leaves in a single epoch. Proof test attached in `test/scratch/DoubleEntry.t.sol`, fails on the current code.\n2. **Low, VestingVault.release in an open vault:** the same token class is vested once per address, so the beneficiary takes 43.75% in week one instead of 25%. Reproduced in the same scratch file.\n3. **Low, TreasuryVault.increaseDelay:** because expiry is derived from the live delay, raising the delay revives an already expired withdrawal and lets it execute in the same block, with no fresh notice window. Reproduced in `test/scratch/Edges.t.sol`. Proposed fix stores the expiry at queue time.\n4. **Info, VestingVault.vestedAmount:** a tranche sent after the schedule ran is 100% releasable immediately. Documented design, but it means the radar should count balance minus releasable as locked, not balance.\n\n**What held**\n\n- All 23 listed entry points were traced. Checks, state changes, CEI ordering and failure paths hold for everything except the three functions above.\n- Reentrancy through a hooked token into execute and release is blocked. An ETH destination that rejects leaves the withdrawal open and consumes none of the window. The last destination cannot be removed.\n- The live IMD token on Ethereum is a LayerZero OFT ERC20 with no fee, pause or blacklist selectors. The project's fork test against it pays exactly 5 IMD to the recipient.\n- VaultFactory runtime is 17,180 bytes, under the deployment limit, with both vault creation codes embedded.\n\n**Not reached:** nothing in the assigned area. The straddle bound and the double fee for pre-launch vaults are documented design, not reported again.","treeHash":null,"usage":{"cachedInputTokens":1506642,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":43146,"runtime":"claude","turns":34,"wallClockMs":617835}},{"artifacts":[],"attempt":1,"bundleHash":"e82b4338073edfc93314a4e05d34d9f274f91543d79bf78df2c297c55e828b05","device":"6e1587ffa91ea90b","findings":[],"hash":"8ea29e8dcb82875e8b5a644e01687463f8a4e77c1d0996573934df58e9137244","nodeId":"60351ae0-9c80-4dba-a8ff-15c78b4ad949","outcome":"completed","summary":"Created `launch.json` naming only `VaultFactory`, with the specified IMD address and `$owner` constructor arguments.\n\nValidation passed: manifest and ABI checks, `forge build`, and all 54 tests using the supplied fuzz seed, including the Ethereum fork test.\n\nOnly `launch.json` changed for submission.","treeHash":"0c3ba9634f1812885e9a9f7c5da557007ed21cbe","usage":{"cachedInputTokens":236544,"inputTokens":33375,"model":"gpt-6-astra","outputTokens":3304,"runtime":"codex","turns":4,"wallClockMs":102035}},{"artifacts":[],"attempt":1,"bundleHash":"dff4e42e9ed8dd4f4fa6524ce6bd698d5aca36e5bf8946ae8851f43bbf7250b0","device":"f9a9196633a6ffa8","findings":[],"hash":"9ec423e3664e3b85c77ead03663711d214aa5104b2ec22c168dc1bb01ec79c1b","nodeId":"1964d3ae-5022-4336-b269-72574222b5ec","outcome":"completed","summary":"The work is complete and verified: `forge build` succeeds and `forge test` passes 54 tests with the verifier's fuzz seed and three others.\n\n**What I found.** All seven concrete audit findings reproduced on the code as it arrived (checked with a scratch test each before changing anything). The factory itself already fits the launch kind: a nonpayable two-argument constructor, no external calls, no owner, so `src/VaultFactory.sol` is untouched.\n\n**What I changed, each with a test in the new `test/Audit.t.sol`:**\n- **Past start (medium).** The vault constructor now refuses a `start` before the current block, so nothing can be unlocked in the block that locks it.\n- **Late arrival to an open vault (medium).** Open vaults record each token's arrival once, via the new public `sync` or the first `release`. That token's linear unlock begins at the later of `start + cliff` and its recorded arrival, so a launch that slips past the schedule still vests from the day it pays the vault. Bound vaults and top-ups keep their documented calendar behaviour. Two existing launch tests now call `sync` after the stand-in launch, with a comment saying why.\n- **Schedule overflow (low).** The constructor and `extendDuration` require `start + cliff + duration` to fit in 64 bits, so tokens can no longer be trapped by a typo.\n- **Double-entry token through `recover` (low).** A bound vault reads the vested token's balance before and after the transfer and reverts if it fell.\n- **Treasury 24h floor (info).** The treasury constructor enforces its own `MIN_DELAY` as well as the factory.\n- **Epoch boundary cap (low).** Left as designed, since the source and README already describe fixed epochs and the brief says to keep the rules as written. The NatSpec and README now state the real bound of 43.75% across a boundary at the 25% ceiling, and a test pins it.\n\n**Left alone and explained in ADAPTATION.md:** the double fee for a pre-launch vault plus a later bound vault matches the brief's \"fee keyed to the vault address\" and is now documented in the README. The coverage note needed nothing. The fork test still skips without an RPC.\n\nThe README gained a fourth launch step telling projects to call `sync(token)` right after their launch, which the deployer's radar may also do on their behalf. Nothing was committed, since the git directory is off limits for this task. Nothing needs to be called after deployment.","treeHash":"40d4e255daaab445386081c0a2899ef65ce77a8d","usage":{"cachedInputTokens":1009171,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":40752,"runtime":"claude","turns":37,"wallClockMs":526264}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9ab27edcfd62be02","findings":[{"citation":"resolved","description":"Merged from audit_flow, audit_permissions and write_foundry_tests (same root cause). execute() keeps one Window per `w.asset` address and sizes its cap from `_balance(w.asset)`. queue() accepts any asset address, so for a token reachable through two addresses that share one balance (TrueUSD's legacy contract forwarding to the main token, Synthetix ProxyERC20 and its target; treasuries hold third-party assets like these) the manager queues one withdrawal through each address and both get a fresh window over the same underlying balance. 25% leaves through the first address, then 25% of the remaining 75% through the second, in the same epoch and even the same block: 43.75% of the balance, 68% with the documented epoch straddle, 1-(0.75)^N with N entry points. The README rule 'no more than rateBps (at most 25%) of the balance can leave per asset per 7-day epoch' and the brief's 'at most 25% of a balance can leave per week' are rules on the balance, not on the address. The project already treats this token class as in scope: VestingVault.recover() was fixed for exactly this mechanism (ADAPTATION change 4) and reads the vested balance before and after; the treasury's capped exit has no equivalent guard. Trigger needs the manager (the role the cap exists to bound) and such an asset, hence medium rather than high. Minimal fix that keeps the design: in execute(), for an ERC-20 withdrawal, snapshot the balances of the other assets this treasury tracks before the transfer and revert if any fell. The cheapest version that passes the attached proof is to snapshot `projectToken` when it is nonzero and `w.asset != projectToken`; a fuller version snapshots every distinct asset ever queued (that set grows only through the manager, so nobody else can bloat the loop) and refuses an execution whose transfer lowers any of them. State in the README that the cap is per address for assets outside that set.","line":159,"path":"src/TreasuryVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {TreasuryVault} from \"src/TreasuryVault.sol\";\n\ncontract MintableToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract DoubleEntryToken is MintableToken {\n    address public legacy;\n\n    constructor() MintableToken(\"DBL\") {}\n\n    function setLegacy(address legacy_) external {\n        legacy = legacy_;\n    }\n\n    function transferFor(address from, address to, uint256 amount) external {\n        require(msg.sender == legacy, \"not legacy\");\n        _transfer(from, to, amount);\n    }\n}\n\ncontract LegacyEntry {\n    DoubleEntryToken immutable main;\n\n    constructor(DoubleEntryToken main_) {\n        main = main_;\n    }\n\n    function balanceOf(address a) external view returns (uint256) {\n        return main.balanceOf(a);\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        main.transferFor(msg.sender, to, amount);\n        return true;\n    }\n}\n\n/// The treasury's weekly cap is kept per asset address, so a token with a second entry point to the same\n/// balance gets two caps in one epoch: 43.75% of the balance leaves in one transaction block, not 25%.\ncontract TreasuryDoubleEntryTest is Test {\n    address radar = makeAddr(\"radar\");\n    address project = makeAddr(\"project\");\n    address manager = makeAddr(\"manager\");\n    address dest = makeAddr(\"dest\");\n\n    function test_TreasuryWeeklyCapHoldsAcrossTwoEntryPointsToOneBalance() public {\n        vm.warp(1_760_000_000);\n        MintableToken imd = new MintableToken(\"IMD\");\n        VaultFactory factory = new VaultFactory(IERC20(address(imd)), radar);\n        imd.mint(project, 5e18);\n        DoubleEntryToken dbl = new DoubleEntryToken();\n        LegacyEntry legacy = new LegacyEntry(dbl);\n        dbl.setLegacy(address(legacy));\n\n        vm.startPrank(project);\n        imd.approve(address(factory), type(uint256).max);\n        TreasuryVault t = factory.createTreasury(address(dbl), manager, 1 days, 2500, new address[](0));\n        vm.stopPrank();\n        dbl.mint(address(t), 1000e18);\n\n        vm.startPrank(manager);\n        uint256 a = t.queue(address(dbl), dest, 250e18);\n        uint256 b = t.queue(address(legacy), dest, 187.5e18);\n        vm.stopPrank();\n\n        vm.warp(block.timestamp + 1 days);\n        t.execute(a);\n        try t.execute(b) {} catch {}\n\n        assertLe(dbl.balanceOf(dest), 250e18, \"more than 25% of the balance left in one epoch\");\n    }\n}","reproduction":"factory.createTreasury(DBL, manager, 1 days, 2500, []) where DBL is an ERC20 and L is a contract whose balanceOf/transfer forward to DBL's balance (LegacyEntry in the proof); 1000e18 DBL minted to the treasury. Manager: queue(DBL, dest, 250e18) -> id a; queue(L, dest, 187.5e18) -> id b. Warp +1 day. execute(a): windows[DBL] cap 250e18, pays 250e18. execute(b): windows[L] is fresh, cap = 750e18 * 25% = 187.5e18, pays 187.5e18. Expected: execute(b) reverts OverWeeklyCap(0), dest holds 250e18 (25%). Actual: both succeed, dest holds 437.5e18 (43.75%) in one block. Run: forge test --match-path test/scratch/Proof_964fb4ddf18e.t.sol -> FAIL 'more than 25% of the balance left in one epoch: 437500000000000000000 > 250000000000000000000' on the current code.","severity":"medium","snippet":"        Window storage win = windows[w.asset];\n        uint64 epoch = uint64(block.timestamp / EPOCH);\n        if (win.epoch != epoch) {\n            win.epoch = epoch;\n            win.cap = (_balance(w.asset) * rateBps) / BPS;","title":"Treasury weekly cap is keyed per asset address, so a token with two entry points to one balance gets a 25% cap per entry point in the same epoch (43.75% in one block)"},{"citation":"resolved","description":"Merged from audit_flow and audit_permissions. Both the due time and the expiry are derived from the current `delay` at execution time, so raising the delay moves a queued withdrawal's whole 14-day execution window later. A withdrawal announced at T0 with a 1-day delay expires at T0+15d; after that execute() reverts Expired and watchers (the radar, holders) read it as dead. The manager then calls increaseDelay(newDelay) with newDelay close to now - queuedAt (any value up to 30 days): due becomes T0+newDelay <= now, expiry T0+newDelay+14d > now, and the same withdrawal executes immediately, by anyone, with no new Queued event and no fresh notice window. The setter that is only meant to tighten ('every rule can only get stricter'; README: 'executes after the delay ..., within 14 days') loosens the expiry. Reachable for any withdrawal expired less than 30 days ago. Manager-triggered and the destination and amount were announced once, so low. Minimal fix: fix the expiry at queue time (store `expiresAt = queuedAt + delay + GRACE` in the Withdrawal and check `block.timestamp > w.expiresAt`), keeping `due = queuedAt + delay` on the current delay so a raise still holds queued withdrawals back and, if it pushes due past expiresAt, simply makes them unexecutable until requeued.","line":154,"path":"src/TreasuryVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {TreasuryVault} from \"src/TreasuryVault.sol\";\n\ncontract MintableToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// A withdrawal that expired (announced eta + 14 days passed) comes back to life when the manager makes the\n/// delay *stricter*, and executes in the same block, with no new announcement.\ncontract ExpiredRevivalTest is Test {\n    address radar = makeAddr(\"radar\");\n    address project = makeAddr(\"project\");\n    address manager = makeAddr(\"manager\");\n    address dest = makeAddr(\"dest\");\n    address stranger = makeAddr(\"stranger\");\n\n    function test_IncreasingTheDelayDoesNotReviveAnExpiredWithdrawal() public {\n        vm.warp(1_760_000_000);\n        MintableToken imd = new MintableToken(\"IMD\");\n        MintableToken token = new MintableToken(\"PROJ\");\n        VaultFactory factory = new VaultFactory(IERC20(address(imd)), radar);\n        imd.mint(project, 5e18);\n        vm.startPrank(project);\n        imd.approve(address(factory), type(uint256).max);\n        TreasuryVault t = factory.createTreasury(address(token), manager, 1 days, 2500, new address[](0));\n        vm.stopPrank();\n        token.mint(address(t), 1000e18);\n\n        uint256 queuedAt = block.timestamp;\n        vm.prank(manager);\n        uint256 id = t.queue(address(token), dest, 250e18); // eta queuedAt + 1 day, expires at + 15 days\n\n        vm.warp(queuedAt + 20 days);\n        vm.expectRevert(TreasuryVault.Expired.selector);\n        t.execute(id);\n\n        // A \"stricter\" rule: the delay goes from 1 day to 20 days.\n        vm.prank(manager);\n        t.increaseDelay(20 days);\n\n        // The expired withdrawal must stay dead.\n        vm.prank(stranger);\n        try t.execute(id) {} catch {}\n        assertEq(token.balanceOf(dest), 0, \"expired withdrawal executed after the delay was increased\");\n    }\n}","reproduction":"createTreasury(token, manager, 1 days, 2500, []); 1000e18 token in the vault. T0: manager queue(token, dest, 250e18) -> id (eta T0+1d, expiry T0+15d). Warp T0+20d: execute(id) reverts Expired (correct). Manager: increaseDelay(20 days). Same block, stranger: execute(id). Expected: still Expired, dest holds 0. Actual: due = T0+20d, expiry T0+34d, executes, dest receives 250e18. Run: forge test --match-path test/scratch/Proof_5b94d528a68f.t.sol -> FAIL 'expired withdrawal executed after the delay was increased: 250000000000000000000 != 0' on the current code.","severity":"low","snippet":"        uint64 due = w.queuedAt + delay;\n        if (block.timestamp < due) revert TooEarly(due);\n        if (block.timestamp > due + GRACE) revert Expired();","title":"increaseDelay() revives an expired withdrawal and makes it executable by anyone in the same block, with no new announcement"},{"citation":"resolved","description":"Merged from audit_flow, audit_permissions and audit_math (same root cause as finding 1, in the vesting vault). In an open vault (`token` zero) vests(asset) is true for every nonzero address and totalAllocation, released, arrivedAt and releasable are all keyed by the asset address. A token with two addresses to one balance therefore gets two independent schedules over one pool of tokens: release(main) pays the vested share, then totalAllocation(legacy) = remaining balance + 0 is treated as a fresh allocation and release(legacy) pays its vested share again. With cliff 0 and the minimum 28-day duration, 43.75% leaves in week one against the 'at most 25% a week' rule; a third release(main) then reverts NothingToRelease because vested(main) < released[main]. Bound vaults are protected (vests() accepts only the bound address; recover() reads the vested balance before and after). Only the beneficiary gains, only for a double-entry asset the project itself chose to lock, and an IMD LaunchToken has one entry point, so the primary launch flow is unaffected: low. Fix options: (a) document the per-address accounting as a limitation and have the radar refuse its seal to an open vault holding a double-entry token (it already reviews the token as legit); (b) a contract-side guard would have to snapshot the balances of the other recorded assets in release(), but sync() is public, so anyone could pad that set with dust of junk tokens until release() runs out of gas; a bounded variant (only assets with released > 0) is still paddable over 28 days. Binding the open vault to its first asset would contradict the brief's 'vests every token it receives'. Option (a) is the one that keeps the design; no proof is attached so the author is not held to a contract change the brief rules out.","line":103,"path":"src/VestingVault.sol","reproduction":"factory.createLaunchVesting(team, now, 0, 28 days). 1000e18 of DoubleEntryToken DBL minted to the vault, L a contract forwarding balanceOf/transfer to DBL. sync(DBL); sync(L) (both report 1000e18, both pass vests). Warp +7 days. release(DBL): vested = 1000e18 * 7/28 = 250e18, pays 250e18. release(L): totalAllocation(L) = 750e18 + 0, vested = 187.5e18, pays 187.5e18. Expected: team holds at most 250e18 after one week of a four-week schedule. Actual: 437.5e18. Reproduced with the audit_permissions proof: forge test --match-path test/scratch/Proof_48e24f27b239.t.sol -> FAIL 'more than 25% of the vested supply left in one week: 437500000000000000000 > 250000000000000000000'.","severity":"low","snippet":"        return IERC20(asset).balanceOf(address(this)) + released[asset];","title":"Open vesting vault vests a double-entry token once per entry point: 43.75% of the balance leaves in the first week"},{"citation":"resolved","description":"Merged from audit_economics (medium), audit_math (dust clock then top-up) and audit_flow (info). vestedAmount() applies the elapsed fraction of the schedule to totalAllocation() = balance + released, i.e. to everything the vault holds at the moment of the call, whatever its arrival time. Only the first arrival of a token in an open vault gets a clock (arrivedAt); a bound vault has none and a later tranche of an already-recorded token has none. So any token that reaches the vault after unlockStart is unlocked by the fraction already elapsed in the block it arrives, and once elapsed >= duration every later deposit is 100% releasable at once. In an open vault the clock for any token can be started by 1 wei (sync records arrival with no minimum), after which a deposit 28 days later is instantly releasable. This is the documented design (README accounting note: 'later top-ups vest on the same schedule'; ADAPTATION change 2 keeps it) and the brief says to keep the rules as written, so it is not a code-versus-spec defect, but it is a guarantee gap the radar must not rely on: the seal requires 'at least 3% of the supply is locked in its vaults' and reads the registry, and a project can satisfy that with a 1-wei bound vault whose 4-week schedule has ended and then park 3% of the supply there, releasable by anyone in the same block. The 25%-a-week rule holds for the first funding of each token, not for the vault. Resolution is a scope decision: either the radar counts locked supply as balance - releasable(token) and the README states that a top-up after the schedule is not locked, or the author accounts per tranche (record the allocation seen at the last sync/release and vest any excess linearly over `duration` from max(start + cliff, now), summing tranches). No proof attached, since a failing test would hold the author to the redesign the brief does not ask for.","line":120,"path":"src/VestingVault.sol","reproduction":"Bound: createVesting(PROJ, team, 1, now, 0, 28 days) (pays 5 IMD). Warp +28 days. Transfer 3_000_000e18 PROJ to the vault. Expected under 'at most 25% a week': releasable == 0 now, <= 750_000e18 after 7 days. Actual: releasable(PROJ) == 3_000_000e18 + 1 in the same block; release(PROJ) by anyone pays it all. Open: createLaunchVesting(team, now, 0, 28 days); mint 1 wei of T to the vault, sync(T); warp +28 days; mint 1_000_000e18 T; releasable(T) == 1_000_000e18 + 1 (test/scratch/JudgeRepro.t.sol test_OpenVaultDustClockThenTopUp passes on this code asserting that). Second tranche: open vault, 1_000_000e18 synced, after 21 days 750_000e18 released, a further 1_000_000e18 arrives: releasable == 750_000e18 on arrival. The audit_economics proof test/scratch/Proof_6fd4b45f5078.t.sol fails on this code with '3000000000000000000000001 != 0' and '750000000000000000000000 != 0'.","severity":"low","snippet":"        uint256 total = totalAllocation(asset);\n        uint64 elapsed = timestamp - from;\n        if (elapsed >= duration) return total;\n        return (total * elapsed) / duration;","title":"Tokens arriving after the unlock has advanced vest retroactively: a vault whose schedule has run is a pass-through, and the radar's 'locked' reading of a balance is not a lock"},{"citation":"resolved","description":"Merged from audit_economics and write_foundry_tests. The fee is keyed only to the token address and nothing ties a vault to the project that paid: once feePaid[chainid][X] is true, _chargeOnce returns at once, so any caller can createTreasury(X, anyManager, 1 days, 1, []) with no token and no IMD, or createVesting(X, ...) with 1 wei of X, and each vault is pushed into allVaults, isVault and _vaultsByToken[X]. createTreasury never checks projectToken at all. The brief says the radar reads allVaults, vaultsByToken and the events to show vaults, the seal and queued-withdrawal alerts, so a stranger can attach treasuries with its own manager and queued withdrawals to a legitimate project's file, and can grow vaultsByToken(X) (an unpaginated view) without bound: measured by the specialist at ~1.68M gas per free treasury and ~2.5k gas per entry to read back, so about 20k entries push the view past a 50M eth_call limit for roughly 34 ETH at 1 gwei. No funds at risk and the fee cannot be avoided for a new token: registry integrity and griefing, low. Minimal fix without an owner: record the creator in the registry entry and the Created events so the radar can filter to vaults made by the payer; or charge the fee per (token, creator) pair, which the brief's 'charges each project 5 IMD once' also permits; and have the radar read vaults from events rather than the unbounded view.","line":132,"path":"src/VaultFactory.sol","reproduction":"Project: createVesting(PROJ, team, 1000e18, now, 0, 28 days) -> radar holds 5e18 IMD. Stranger with zero IMD and zero PROJ: createTreasury(PROJ, stranger, 1 days, 1, []) five times. Expected: a vault for PROJ by a non-payer is refused or charged. Actual: all succeed, radar still holds exactly 5e18 IMD, vaultsByToken(PROJ).length == 6 and isVault is true for each (test/scratch/JudgeRepro.t.sol test_StrangersFileFreeVaultsUnderPaidToken passes on this code asserting that).","severity":"low","snippet":"        if (address(imd) == address(0) || feePaid[block.chainid][token]) return;","title":"Once a token has paid, anyone files unlimited free vaults under it, polluting and growing the registry the radar reads"},{"citation":"resolved","description":"From audit_permissions. proposeManager freezes pendingManagerEta = now + delay, while execute() re-evaluates queued withdrawals against the current delay (README: tightening 'also holds back withdrawals already queued'). After increaseDelay an earlier proposal still completes on the old schedule, so the delay the radar displays does not hold for a control change already pending. The same manager controls both calls and funds stay behind the withdrawal delay, so this is a consistency note. Fix if wanted: store the proposal time and require block.timestamp >= proposedAt + delay in acceptManager, as execute does.","line":239,"path":"src/TreasuryVault.sol","reproduction":"createTreasury(token, manager, 1 days, 2500, []). T0: manager proposeManager(M2) (eta T0+1d); manager increaseDelay(30 days). Warp T0+1d: M2 acceptManager(). Expected under 'a new manager takes over after the delay' with the current delay: TooEarly until T0+30d. Actual: manager() == M2 after 1 day (test/scratch/JudgeRepro.t.sol test_PendingManagerKeepsOldDelay passes asserting that).","severity":"info","snippet":"        if (block.timestamp < pendingManagerEta) revert TooEarly(pendingManagerEta);","title":"A pending manager change keeps the delay it was proposed with after increaseDelay(), unlike queued withdrawals"},{"citation":"resolved","description":"From write_foundry_tests. createVesting emits `amount` after the transfer, while a fee-on-transfer token delivers less to the vault. The vault's own accounting is by balance so nothing is lost, but the radar, which reads this event to show what is locked, would overstate it. Fix: emit the vault's balance after minus before.","line":81,"path":"src/VaultFactory.sol","reproduction":"createVesting with a 1%-fee token and amount 1000e18: the VestingCreated data word is 1000e18, token.balanceOf(vault) is 990e18 (test/scratch/JudgeRepro.t.sol test_VestingCreatedLogsRequestedAmount passes asserting that).","severity":"info","snippet":"        emit VestingCreated(address(vault), address(token), beneficiary, amount, start, cliff, duration);","title":"VestingCreated logs the requested amount, not what the vault received from a fee-on-transfer token"},{"citation":"resolved","description":"From audit_math. The epoch cap is balance * rateBps / 10_000 rounded down: when balance < 10_000 / rateBps the cap is 0 and execute reverts OverWeeklyCap(0) every epoch until someone deposits more (at 1 bps anything under 10_000 base units; at 2_500 bps, 3 units). More generally each epoch moves at most rateBps of the current balance, so an asset decays geometrically and can never be fully withdrawn or migrated. A direct consequence of the stated rule and the amounts are dust: informational, document it.","line":163,"path":"src/TreasuryVault.sol","reproduction":"createTreasury(T, manager, 1 days, rateBps=1, []); mint 9_999 T to the vault; manager queue(T, manager, 1); warp +1 day. availableThisWeek(T) returns 0 (9_999 * 1 / 10_000 = 0) and execute(id) reverts OverWeeklyCap(0); expected 1 unit withdrawable (test/scratch/JudgeRepro.t.sol test_CapRoundsToZero passes asserting the revert).","severity":"info","snippet":"            win.cap = (_balance(w.asset) * rateBps) / BPS;","title":"Treasury weekly cap rounds down to zero for small balances, so the last units of an asset can never leave"}],"hash":"a70ec7d04b5c76fc9ccaae48a0e9f9f4b73439ae8106e07b66fc988669dac157","nodeId":"ff552eb2-6bb2-4548-a9f6-c7d685963a83","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds 8 findings plus a 25-row coverage record. No tracked file was changed; only the ignored findings file and `test/scratch/` were written.\n\n**What I verified**\n\n- The build passes and all 90 existing tests pass. The factory's runtime and initcode sizes are well under the deployment limits, its constructor makes no external call, and `launch.json` matches the brief.\n- All four specialist proofs fail on the current code for the stated reasons. I wrote one more scratch suite that confirms the five lower-severity claims by asserting the current behaviour.\n- The live IMD token on Ethereum is a plain 18-decimal ERC20 with an owner, so the fee path is standard.\n\n**Findings kept, after merging duplicates across the five reports**\n\n1. **Medium, with proof.** The treasury's weekly cap is keyed per asset address. A double-entry token gets a fresh 25% cap per entry point, so 43.75% leaves in one block. Three specialists reported it; merged into one.\n2. **Low, with proof.** Raising the delay revives an expired withdrawal and lets anyone execute it in the same block, with no new announcement. Two reports merged.\n3. **Low.** An open vesting vault vests a double-entry token once per address, 43.75% in week one. Three reports merged. No proof attached, because every contract-side guard I could find is either a design change the brief rules out or a gas-griefable loop. The fix I propose is documentation plus the radar refusing such tokens.\n4. **Low.** Top-ups vest retroactively, so a finished vault is a pass-through and a 1-wei vault can earn the 3% seal. Three reports merged, downgraded from medium because the README documents it. The radar should count balance minus releasable, or the author accounts per tranche; that is a scope decision.\n5. **Low.** Once a token has paid, anyone files free vaults under it and can grow the unbounded registry view. Two reports merged.\n6. **Info.** A pending manager change keeps its original delay after `increaseDelay`.\n7. **Info.** The vesting creation event logs the requested amount, not what a fee-on-transfer token delivered.\n8. **Info.** The weekly cap rounds to zero for small balances, so an asset can never be fully withdrawn.\n\nMy own pass over every entry point found nothing beyond these. The static-analysis lines are all intended sentinel checks or timestamp reads, none reproduced as a defect.","treeHash":null,"usage":{"cachedInputTokens":938977,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":42398,"runtime":"claude","turns":25,"wallClockMs":592002}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6b16b4979d227241","findings":[{"citation":"resolved","description":"vestedAmount() applies the elapsed fraction of the schedule to totalAllocation() = balance + released, i.e. to everything the vault holds at the moment of the call, whatever its arrival time. Only the FIRST arrival of a token in an open vault gets a clock (arrivedAt, finding 5dbb3b32); a bound vault has none and a later tranche of an already-recorded token in an open vault has none. So any token that reaches the vault after unlockStart is unlocked by the fraction already elapsed in the block it arrives, and a vault whose schedule has run (elapsed >= duration) releases every later deposit immediately. This contradicts the rule the brief and README state as a property of the vault, 'vesting unlocks at most 25% a week (duration of at least 4 weeks after the cliff)', and the invariant the fuzz tests claim (testFuzz_VestingNeverUnlocksMoreThan25PercentInAWeek only ever funds once). Economic consequence: the radar's seal requires 'at least 3% of the supply is locked in its vaults' and reads the vault registry; a project can satisfy it with a 1-wei bound vault whose 4-week schedule has ended and then park 3% of the supply there, where it is releasable in the same block by anyone calling release(). The same holds for the primary launch flow: after the launch remainder has been recorded in an open vault, any further tranche of that token (a second allocation, a buy-back, a partner's allocation) is folded into the running schedule and unlocks by the elapsed fraction on arrival. The README's accounting note ('top-ups vest on the same schedule') documents the mechanism for bound vaults, but it is the same defect class the previous round fixed for the first arrival in open vaults, and it leaves the 25%-a-week rule holding only for the funding transaction, not for the vault. Minimal fix that keeps the design (no owner, rules only stricter): account per tranche. Keep, per asset, the allocation recorded at the last sync/release (recordedAllocation[asset]) and its own unlock start; when totalAllocation(asset) exceeds the recorded amount, treat the excess as a new tranche that unlocks linearly over `duration` from max(start + cliff, now) (the same rule 5dbb3b32 applies to the first arrival), and let vestedAmount sum the tranches. If the author keeps the current behaviour instead, the brief's rule needs to be restated as applying to each funding transaction only, and the radar must count locked supply as balance - releasable rather than balance; that is a scope decision for the judge.","line":120,"path":"src/VestingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {VestingVault} from \"src/VestingVault.sol\";\n\ncontract ScratchToken is ERC20 {\n    constructor(string memory n) ERC20(n, n) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice Tokens that reach a vesting vault after its linear unlock has advanced are unlocked retroactively:\n///         a vault whose schedule has run is a pass-through, and a later tranche of a token an open vault already\n///         recorded unlocks up to 100% in the week it arrives, not the 25% the rules promise.\ncontract TopUpPassThroughTest is Test {\n    address radar = makeAddr(\"radar\");\n    address project = makeAddr(\"project\");\n    address team = makeAddr(\"team\");\n\n    ScratchToken imd;\n    ScratchToken token;\n    VaultFactory factory;\n\n    function setUp() public {\n        vm.warp(1_760_000_000);\n        imd = new ScratchToken(\"IMD\");\n        token = new ScratchToken(\"PROJ\");\n        factory = new VaultFactory(IERC20(address(imd)), radar);\n        imd.mint(project, 100e18);\n        token.mint(project, 100_000_000e18); // the whole supply\n        vm.startPrank(project);\n        imd.approve(address(factory), type(uint256).max);\n        token.approve(address(factory), type(uint256).max);\n        vm.stopPrank();\n    }\n\n    /// @dev A bound vault made with 1 wei and a finished 4-week schedule: 3% of the supply sent to it afterwards\n    ///      (the radar's \"locked\" threshold) is releasable in the same block.\n    function test_FinishedBoundVaultIsAPassThrough() public {\n        vm.prank(project);\n        VestingVault v = factory.createVesting(IERC20(address(token)), team, 1, uint64(block.timestamp), 0, 28 days);\n        vm.warp(block.timestamp + 28 days);\n\n        uint256 threePercent = 3_000_000e18;\n        vm.prank(project);\n        token.transfer(address(v), threePercent);\n\n        // Expected under \"vesting unlocks at most 25% a week\": nothing now, at most 25% of the tranche in 7 days.\n        assertEq(v.releasable(address(token)), 0, \"a tranche is unlocked the block it arrives\");\n        vm.warp(block.timestamp + 7 days);\n        assertLe(v.releasable(address(token)), threePercent / 4 + 1, \"more than 25% of the tranche in one week\");\n    }\n\n    /// @dev An open (launch) vault: the launch remainder is recorded and vests; a second tranche of the same token\n    ///      21 days later unlocks 75% on arrival, since it is folded into the running schedule.\n    function test_SecondTrancheToAnOpenVaultUnlocksOnArrival() public {\n        vm.prank(project);\n        VestingVault v = factory.createLaunchVesting(team, uint64(block.timestamp), 0, 28 days);\n        vm.prank(project);\n        token.transfer(address(v), 1_000_000e18); // the launch remainder\n        v.sync(address(token));\n\n        vm.warp(block.timestamp + 21 days);\n        v.release(address(token)); // 750_000e18 released so far, as scheduled\n        assertEq(token.balanceOf(team), 750_000e18);\n\n        vm.prank(project);\n        token.transfer(address(v), 1_000_000e18); // a second tranche\n        assertEq(v.releasable(address(token)), 0, \"second tranche unlocked on arrival\");\n        vm.warp(block.timestamp + 7 days);\n        assertLe(v.releasable(address(token)), 250_000e18 + 250_000e18 + 1, \"more than 25% of each tranche in a week\");\n    }\n}","reproduction":"Bound vault: project calls factory.createVesting(PROJ, team, 1, block.timestamp, 0, 28 days) (pays 5 IMD). Warp 28 days. Project transfers 3_000_000e18 PROJ (3% of a 100M supply) to the vault. Expected under the stated rule: releasable == 0 now and <= 750_000e18 after 7 days. Actual: vault.releasable(PROJ) == 3_000_000e18 + 1 in the same block; release(PROJ) by anyone pays it all to team. Open (launch) vault: createLaunchVesting(team, now, 0, 28 days); 1_000_000e18 of the launched token arrives and sync(token) records it; after 21 days release() has paid 750_000e18 as scheduled; a second 1_000_000e18 tranche arrives. Expected: 0 releasable on arrival, at most 25% of the new tranche a week later. Actual: releasable == 750_000e18 on arrival (75% of the tranche, 21/28 elapsed). test/scratch/TopUpPassThrough.t.sol fails on both with these exact values.","severity":"medium","snippet":"        uint256 total = totalAllocation(asset);\n        uint64 elapsed = timestamp - from;\n        if (elapsed >= duration) return total;\n        return (total * elapsed) / duration;","title":"Tokens arriving after the unlock has advanced vest retroactively: a finished vault is a pass-through, and a later tranche can unlock 100% in its arrival week"},{"citation":"resolved","description":"The fee is keyed only to the token address, and nothing ties a vault to the project that paid: after one 5 IMD payment for token X, any caller can createTreasury(X, anyManager, 1 days, 1, []) (no token, no IMD needed) or createVesting(X, ...) with 1 wei of X, and each such vault is pushed into allVaults, isVault and _vaultsByToken[X] at no fee. The radar files vaults, the seal and queued-withdrawal alerts by vaultsByToken(X) and the Created events, so a stranger can attach arbitrary treasuries (with their own manager and queued withdrawals) to a legitimate project's file, and can grow vaultsByToken(X) until the unpaginated view no longer fits an RPC call. Measured: 1,678,860 gas per free treasury and about 2,560 gas per entry to read back, so ~20,000 entries (~34 G gas, ~34 ETH at 1 gwei) push the view past a 50M eth_call limit. No funds are at risk and the fee cannot be avoided for a new token, so this is a registry-integrity and griefing issue, not a loss. Minimal fix without adding an owner: have the radar read vaults from events (which carry the creator) or add a creator field to the registry and the events, so the off-chain filter can ignore vaults not created by the payer; alternatively, charge the fee per (token, creator) pair instead of per token, which the brief's wording ('charges each project 5 IMD once') also permits.","line":132,"path":"src/VaultFactory.sol","reproduction":"Project pays: createVesting(PROJ, team, 1000e18, now, 0, 28 days) -> radar holds 5e18 IMD. Stranger with zero IMD and zero PROJ then calls createTreasury(PROJ, stranger, 1 days, 1, []) 100 times. Expected: a vault for PROJ by a non-payer is refused or charged. Actual: all succeed, radar still holds exactly 5e18 IMD, isVault is true for each, and vaultsByToken(PROJ).length == 101 (test/scratch/RegistrySpam.t.sol).","severity":"low","snippet":"        if (address(imd) == address(0) || feePaid[block.chainid][token]) return;","title":"Once a token has paid, anyone can file unlimited free vaults under it, polluting the registry the radar reads"}],"hash":"d25e52fe3c14e4f7c26525844c1aeaea716d9037c2cad61e57fc47c3dba23ca5","nodeId":"a3cfa3b8-da25-457f-825b-9a023372166d","outcome":"completed","summary":"Review complete. No tracked file changed; the findings file and two scratch tests are the only new files, and both are ignored by git.\n\n**Outcome:** one medium and one low finding, written to `.imd-findings.json` with a Foundry proof for the medium and a coverage row for all 23 entry points plus 4 invariants.\n\n**Finding 1, medium: late tranches vest retroactively.** `vestedAmount` applies the elapsed fraction of the schedule to the whole current balance, so any token arriving after the unlock has started is unlocked by that fraction on arrival. A bound vault whose 4-week schedule has ended is a pass-through: a 1-wei vault plus 3% of supply sent 28 days later is fully releasable in the same block, which satisfies the radar's \"3% locked\" seal with liquid tokens. The launch flow has the same gap: after the remainder is recorded in an open vault, a second tranche 21 days in unlocks 75% on arrival. Both are shown by `test/scratch/TopUpPassThrough.t.sol`, which fails on the current code. The README documents top-ups as design, so I stated the fix (per-tranche accounting with each tranche vesting from its own arrival, the rule round 1 applied to first arrivals) and the alternative (restate the rule and count locked supply as balance minus releasable) as a judge's scope decision.\n\n**Finding 2, low: free registry entries under a paid token.** The fee is keyed only to the token address, so once a project has paid, any stranger with no IMD can file unlimited treasuries under that token at 1.68M gas each, all appearing in `vaultsByToken`. No funds at risk; it is registry pollution the radar should filter by creator event.\n\n**What holds in my area.** The treasury cap, delay, expiry, stricter-only setters and manager handover trace clean. Flash or donated deposits cannot inflate the cap profitably because the deposit stays locked. The 43.75% epoch-straddle bound was already accepted as design in round 1 and is not re-reported. I verified the live IMD token on-chain: an OpenZeppelin ERC20 under LayerZero OFT, 18 decimals, no tax, pause or blacklist, and the fork test passes against it, so the 5 IMD fee path is sound.\n\n**Unverified lead, not reported:** `VestingVault` has no `receive()`, so a launch that also forwards ETH to `remainderTo` would revert. The launch factory is not in the tree, so I could not confirm whether it does.","treeHash":null,"usage":{"cachedInputTokens":1374024,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":37639,"runtime":"claude","turns":34,"wallClockMs":572152}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"401c4383398af47c","findings":[],"hash":"d78fb27340a8ec9af544a9f20bb5c9c9c99b8e422f3f45fff1d151a8c88aba78","nodeId":"ff552eb2-6bb2-4548-a9f6-c7d685963a83","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with no open findings and coverage for all 23 entry points plus two invariants. The working tree is otherwise unchanged.\n\n**Settlement of my earlier findings**\n\n- **Treasury cap per address (medium, c693ed06): fixed.** The attached proof now passes against this code. I traced the new tracked-asset logic in execute() in every order: main first, legacy first, legacy queued after main already withdrew, and three addresses. Every outflow through a tracked address is charged to each other tracked address whose balance fell, so the window opened first in an epoch bounds the epoch total. The tester's shared-cap invariant suite also passes.\n- **increaseDelay revives an expired withdrawal (low, 960ccd81): fixed.** Expiry is stored at queue time and checked directly. The proof passes. A raise that pushes the due time past the expiry closes the withdrawal, as I proposed.\n- **Pending manager keeps the old delay (info, 5811a810): fixed.** The eta is now proposal time plus the current delay.\n- **Free vaults under a paid token (low, 8d4b11e3): fixed as proposed.** The registry and both creation events record the creator, and the README tells the radar to filter by it and read events, not the unbounded view.\n- **VestingCreated logs the requested amount (info, 28e462ee): fixed.** The factory emits the measured balance difference.\n- **Open-vault double-entry vesting (low, 93c43379) and retroactive top-up vesting (low, 7f9d9778): closed by documentation.** Both were resolved with the documentation option I offered, and I said then that no contract change would be required. The old proofs for them still fail, which is expected. README and NatSpec now state per-address accounting, the seal refusal for double-entry tokens, and that locked supply is counted as balance minus releasable.\n- **Cap rounds to zero (info, cc57dd0d): documented**, as requested.\n\nThe specialist reports this round describe the pre-fix code and all map onto the items above, so nothing new came out of them.\n\n**Verification**\n\n- All 109 project tests pass, including the 11 new audit tests and the tester's SharedCap invariant and edge tests.\n- VaultFactory runtime is 18,760 bytes and initcode 19,030 bytes, within the deployment limits.\n- I could not run the protected floor tests: they import the verifier's floor harness, which this tree does not carry.\n\n**Not reported as a finding.** A manager could queue a junk token whose balanceOf burns all gas, permanently blocking ERC-20 executions. I left it out because the manager can already freeze the treasury for good under the agreed design, for example by restricting destinations to a dead address, so it adds no new power. The staticcall in `_tryBalance` could be given a gas stipend if the author wants to close it anyway.","treeHash":null,"usage":{"cachedInputTokens":987677,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":15966,"runtime":"claude","turns":21,"wallClockMs":290113}},{"artifacts":[],"attempt":1,"bundleHash":"2ec0cc2bacea7bcd8e869bf5283d7d3464ca10adc657b3a8a32812da24709321","device":"9e51ef2afd7c2af8","findings":[],"hash":"df054469ae9ace20ed1cd293c33ddd8aa06f325f4a3815d432b72e0179fc6a4d","nodeId":"60351ae0-9c80-4dba-a8ff-15c78b4ad949","outcome":"completed","summary":"Revised launch.json is in place and verified. Only that file changed.\n\n**What I found.** The accepted manifest from my previous round still had the correct deployment entry: one contract, VaultFactory, with the IMD token address as the first argument and `$owner` as the fee recipient. The constructor in the accepted source is unchanged, takes exactly those two parameters in that order, and calls no other contract, so it deploys cleanly on the protected harness's empty chain. The notes, however, were written before the other contributor's second audit round changed the treasury and factory, and no longer described the accepted tree.\n\n**What I changed.** Only the notes string. It now describes the accepted implementation:\n\n- Treasury withdrawals have a fixed expiry set when queued, so raising the delay can never revive an expired one.\n- Every ERC-20 asset the manager ever queues is tracked, and a withdrawal that lowers another tracked balance is charged against that asset's window, so two addresses to one balance share one 25% cap.\n- A pending manager change waits the current delay.\n- The registry records the creator of each vault, and creation events carry the creator and the amount actually received.\n- The open-vault, fee, vesting floor and no-admin rules are stated as before.\n\n**Verification.**\n\n| Check | Result |\n|---|---|\n| Manifest shape (only kind, contracts, notes; string args; name length) | ok, notes 1780 chars |\n| `forge build` | successful |\n| `forge test` with the verifier's fuzz seed | 62 passed, 0 failed |\n| `git status` | only launch.json modified |\n\nNo `.imd-blocked.json` was needed, since every argument is fixed by the brief and the launch policy.","treeHash":"49f15a4f6c2473916522b720fc7650dba123a938","usage":{"cachedInputTokens":318803,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":3819,"runtime":"claude","turns":9,"wallClockMs":63706}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"934ef01306a3cc89","findings":[{"citation":"resolved","description":"In an open vault (`token` zero) every nonzero address `vests`, and `totalAllocation(asset)` is `balanceOf + released[asset]`, keyed per address. A token reachable through two addresses that share one balance (the legacy/proxy pattern the README already guards against in `recover()`) is therefore counted as two independent allocations on one pool of tokens. Releasing through address A reduces the shared balance, then `totalAllocation(B) = reducedBalance + released[B]` (zero) is treated as a fresh allocation and `rateOfSchedule` of it is released again. With cliff 0 and the minimum 28-day duration, 7 days in: release(A) pays 25% of T, release(B) pays 25% of the remaining 75% = 18.75%, so 43.75% of T leaves in the first week; the stated rule is at most 25% in any week. Seam: precision/accounting x invariant (per-address `released` versus a shared balance). The bound vault is not affected: `vests(B)` is false and `recover(B)` reverts when the vested balance falls. Preconditions: an open vesting vault holding such a token (only the beneficiary gains, so this is the team moving its own lock faster than the vault advertises; an IMD LaunchToken has one entry point, so launch remainders are not affected, but the README says an open vault vests every token it receives). Fix options (a before/after balance check on the released asset does not catch this, since each release moves exactly what it sent): (1) bind an open vault to the first asset it records: on the first successful `sync`, store it as the vault's token, after which `vests` is single-token and `recover()` applies as in a bound vault; this keeps the launch use case exactly (the launch remainder is the first arrival) and closes the second entry point, at the cost of the README line that an open vault vests every token it receives, and note that `sync` is public, so anyone could bind the vault to dust before the launch unless the binding call is limited to the beneficiary (a new, small privilege the brief would have to accept). (2) Keep the design and state the limitation in the README, with the radar refusing its seal to open vaults holding a double-entry token, as it already reviews the token as legit. Either way add a test like the proof.","line":103,"path":"src/VestingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {VaultFactory} from \"src/VaultFactory.sol\";\nimport {VestingVault} from \"src/VestingVault.sol\";\n\n/// @dev A second entry point to the same balance (legacy / proxy style token).\ncontract Proxy {\n    address public immutable base;\n\n    constructor(address b) {\n        base = b;\n    }\n\n    function balanceOf(address a) external view returns (uint256) {\n        return DoubleEntry(base).balanceOf(a);\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        return DoubleEntry(base).transferVia(msg.sender, to, amount);\n    }\n}\n\ncontract DoubleEntry is ERC20 {\n    Proxy public proxy;\n\n    constructor() ERC20(\"D\", \"D\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n\n    function setProxy(Proxy p) external {\n        proxy = p;\n    }\n\n    function transferVia(address from, address to, uint256 amount) external returns (bool) {\n        require(msg.sender == address(proxy), \"proxy\");\n        _transfer(from, to, amount);\n        return true;\n    }\n}\n\ncontract OpenVaultDoubleEntryTest is Test {\n    address team = makeAddr(\"team\");\n\n    /// Open vault + double-entry token: 43.75% of the allocation reaches the beneficiary in the first week\n    /// of a 4-week schedule, against the 25%-a-week ceiling.\n    function test_OpenVaultDoubleEntryBreaksWeeklyCeiling() public {\n        vm.warp(1_760_000_000);\n        VaultFactory factory = new VaultFactory(IERC20(address(0)), address(0)); // free chain, no fee\n        DoubleEntry t = new DoubleEntry();\n        Proxy p = new Proxy(address(t));\n        t.setProxy(p);\n        VestingVault v = factory.createLaunchVesting(team, uint64(block.timestamp), 0, 28 days);\n        t.mint(address(v), 1_000_000e18);\n        v.sync(address(t));\n        v.sync(address(p));\n        vm.warp(block.timestamp + 7 days);\n        v.release(address(t)); // 250_000e18: 25% of 1_000_000e18\n        v.release(address(p)); // 187_500e18: 25% of the 750_000e18 still held, counted as a fresh allocation\n        assertLe(t.balanceOf(team), 250_000e18, \"more than 25% left in the first week\");\n    }\n}","reproduction":"State: factory on a free chain (`new VaultFactory(IERC20(0), 0)`), `createLaunchVesting(team, now, cliff=0, duration=28 days)`. Token D (ERC20) with proxy P whose `balanceOf`/`transfer` forward to D's balance. Mint 1_000_000e18 D to the vault; `sync(D)`; `sync(P)`; warp +7 days. `release(D)` sends 250_000e18 (expected: 25%). `release(P)`: `totalAllocation(P) = 750_000e18 + 0`, vested = 750_000e18 * 7/28 = 187_500e18, sends 187_500e18. Team holds 437_500e18 = 43.75% after one week; expected at most 250_000e18. A third `release(D)` then reverts NothingToRelease because `vested(D) = (562_500e18 + 250_000e18) * 7/28 = 203_125e18 < released[D]`. Test test/scratch/MathReview.t.sol:test_OpenVaultDoubleEntryBreaksWeeklyCeiling fails on this code with `437500000000000000000000 > 250000000000000000000000`.","severity":"low","snippet":"        return IERC20(asset).balanceOf(address(this)) + released[asset];","title":"Open vault: a token with two entry points to one balance is counted twice, releasing 43.75% in the first week"},{"citation":"resolved","description":"`_sync` records `arrivedAt[asset]` the first time the vault holds any amount of `asset`, with no minimum. Combined with the balance-plus-released allocation (README: top-ups vest on the same schedule), an open vault's clock for a token can be started by 1 wei, and every later deposit of that token is vested as if it had been in the vault since that moment. Once `arrivedAt + duration` has passed, a deposit of any size is immediately 100% releasable, while the radar sees a vault 'locking' that balance. ADAPTATION.md states that nobody can set the clock earlier with dust because a launch's first tranche is the launch transaction; that holds for a launch remainder but not for the other tokens an open vault accepts, nor for a project that funds the vault itself after creation (the bound-vault equivalent is `createVesting` with amount 1 and a top-up after the schedule, also instantly releasable). This is the documented top-up design rather than an arithmetic error, so it is reported as a broken guarantee under specific conditions: 'at most 25% a week' only holds for the first tranche of each token. Minimal fix without changing the schedule rule: either make the radar count only `balance - releasable(token)` as locked, or (contract-side) start a separate linear clock per deposit, which the brief's 'keep the rules exactly as written' may rule out; at minimum the README should state that a top-up after the schedule is not locked at all.","line":217,"path":"src/VestingVault.sol","reproduction":"Free factory; `createLaunchVesting(team, now, 0, 28 days)`. Mint 1 wei of token T to the vault, `sync(T)` (arrivedAt = now). Warp +28 days. Mint 1_000_000e18 T to the vault. `releasable(T)` returns 1_000_000e18 + 1 immediately; `release(T)` pays the whole amount to the team in the same block (expected under 'at most 25% a week': at most 250_000e18 in the first 7 days after the deposit). Reproduced by test/scratch/MathReview.t.sol:test_OpenVaultDustClockThenTopUpIsInstantlyReleasable (asserts the current behaviour, so it passes on this code).","severity":"low","snippet":"        arrivedAt[asset] = uint64(block.timestamp);","title":"Open vault clock can be started by 1 wei; a later deposit after the schedule has run is 100% releasable at once"},{"citation":"resolved","description":"The epoch cap is `balance * rateBps / 10_000` rounded down. When `balance < 10_000 / rateBps` the cap is 0 and `execute` reverts `OverWeeklyCap(0)` for any amount, every epoch, until someone deposits more. At the allowed minimum rate of 1 bps any balance under 10_000 base units is stuck (9_999 units of an 8-decimal token); at 2_500 bps, 3 units. More generally, because each epoch can move at most `rateBps` of the current balance, an asset can never be fully withdrawn: the balance decays geometrically to this floor and the last units are unreachable. This is a direct consequence of the stated rule ('at most 25% of a balance can leave per week') and the amounts are dust, so it is informational: the rule as written implies a treasury can never be emptied or migrated in full. No contract change is proposed; documenting it in the README is enough.","line":163,"path":"src/TreasuryVault.sol","reproduction":"Free factory; `createTreasury(T, manager, 1 days, rateBps=1, [])`. Mint 9_999 T to the vault. Manager `queue(T, manager, 1)`; warp +1 day. `availableThisWeek(T)` returns 0 (9_999 * 1 / 10_000 = 0). `execute(id)` reverts `OverWeeklyCap(0)`; expected: 1 unit is withdrawable. Reproduced by test/scratch/MathReview.t.sol:test_TreasuryBalanceFrozenWhenCapRoundsToZero.","severity":"info","snippet":"            win.cap = (_balance(w.asset) * rateBps) / BPS;","title":"Treasury weekly cap rounds down to zero for small balances, freezing the remainder of an asset forever"}],"hash":"ea0497fc6986d9f88658e44e85724a89ce8d3d1e3e117e5127f685b2e89167fd","nodeId":"7bd0646d-69b4-42c0-9fb9-d2411ae7968f","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds three findings and a coverage row for all 23 entry points plus three invariants. The working tree is otherwise untouched apart from the scratch tests, which are not kept.\n\n**What I checked.** I traced every division, cast, boundary comparison and sentinel branch in the three contracts against the Math Precision, Boundary and Numerical Gap guides. The core arithmetic holds: `vestedAmount` rounds down and caps at the total, the schedule is bounded to fit `uint64`, the treasury epoch cap never lets `spent` exceed `cap`, all delay and eta sums are bounded by 30 days, and the straddle bound of 43.75% matches what the README states. The vendored SafeERC20 (OpenZeppelin 5.1.0) refuses targets without code, and IMD on mainnet has 18 decimals and symbol IMD, so the hardcoded 5e18 fee is right. The existing suite of 54 tests passes.\n\n**Findings, most severe first.**\n\n1. **Low, with a failing proof.** An open vault counts a double-entry token (one balance reachable through two addresses) as two allocations, because `released` is keyed per address while the balance is shared. On a 28-day schedule the team receives 43.75% in the first week instead of at most 25%. The proof test fails on this code with that exact number. Only a vault holding such a token is affected, and IMD launch tokens have one entry point.\n2. **Low.** The open-vault clock starts on any nonzero balance, including 1 wei. A deposit made after the schedule has run is 100% releasable in the same block. This is the documented top-up design, but it means the 25%-a-week rule only holds for each token's first tranche, and the radar would show the balance as locked.\n3. **Info.** The treasury cap floors to zero for small balances (any balance under 10,000 units at 1 bps, or 3 units at 25%), so an asset can never be fully withdrawn. A direct consequence of the rule as written.\n\n**Not reported.** Intermediate overflow in `total * elapsed` or `balance * rateBps` needs a balance above 2^192, which no real token reaches. The epoch-zero window default only matters on a chain younger than seven days.","treeHash":null,"usage":{"cachedInputTokens":1341702,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":30224,"runtime":"claude","turns":26,"wallClockMs":453747}},{"artifacts":[],"attempt":1,"bundleHash":"75ee2a62431b3f919622c38961cac85800633785348431055962fd6730ff154d","device":"6208734cdf5317a1","findings":[],"hash":"f7257978f20c9ee2406682158ac6573bc26ddd5cfcc3776f8aa26ef82e317783","nodeId":"1964d3ae-5022-4336-b269-72574222b5ec","outcome":"completed","summary":"The factory stays well under the 24,576-byte runtime limit, the diff is confined to the three contracts, the audit test file and the two docs, and the untracked `.imd-responses.json` is in place. The work is complete.\n\n**Outcome.** The reopened finding reproduced and is fixed. The reviewer's proof now passes, the full suite passes with the verifier's fuzz seed and two others, and every finding has an answer in `.imd-responses.json`.\n\n| Check | Result |\n|---|---|\n| Proof `c693ed06` on starting tree | failed, 437.5e18 left |\n| Proof `c693ed06` after fix | passes, 250e18 leaves and the second address reverts `OverWeeklyCap(0)` |\n| `forge test`, three fuzz seeds | 64 passed, 0 failed (62 in tree plus the two proofs in scratch) |\n\n**What changed in the contracts.**\n- **Treasury shared cap (medium, held to proof).** The treasury now tracks every ERC-20 asset the manager has ever queued. Each ERC-20 withdrawal reads the other tracked balances before and after the transfer, and any balance that fell is charged against that asset's weekly window too. Two addresses to one balance therefore share one 25% window, in whichever order they are used. Reads use a static call that treats failure as zero, so a mistakenly queued address never blocks other withdrawals, and ETH is not tracked since a token transfer cannot move it.\n- **Expiry fixed at queue time (low).** The withdrawal struct stores its expiry, so raising the delay can close a queued withdrawal but never revive an expired one.\n- **Pending manager follows the current delay (info).** The proposal time is stored and `pendingManagerEta()` becomes a view over it plus the current delay, with the same name and return type.\n- **Creator in the registry (low).** The factory records `creatorOf[vault]` and both creation events carry the creator, so the radar can filter vaults to the paying project. The fee rule itself is unchanged.\n- **Received amount in the event (info).** `VestingCreated` logs what the vault actually received from a fee-on-transfer token.\n\n**Documented rather than changed.** Three advisory findings reproduce but ask for documentation or an off-chain rule, and a code change would redesign behaviour the brief says to keep: the open vault's per-address accounting for double-entry tokens, calendar-based top-ups, and the cap rounding to zero for dust. The README now states each limitation, counts locked supply as balance minus releasable, and says the radar refuses its seal to an open vault holding a double-entry token.\n\n**Not run.** The fork test against the live IMD token still skips without an RPC variable, as in the previous round.","treeHash":"73420fe53444732445740ad64a038b86abf51885","usage":{"cachedInputTokens":1122985,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":36468,"runtime":"claude","turns":43,"wallClockMs":490060}}],"verification":[{"checks":[{"durationMs":3848,"exitCode":0,"name":"build","output":"Compiling 40 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.74s\nCompiler run successful!\n","passed":true},{"durationMs":6364,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Fork.t.sol:ForkTest\n[PASS] test_RealImdFeeAndVesting() (gas: 3689)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 236.72µs (69.25µs CPU time)\n\nRan 4 tests for test/Invariants.t.sol:TreasuryInvariantTest\n[PASS] invariant_conservation() (runs: 128, calls: 8192, reverts: 0)\n\n╭-----------------+-------------------+-------+---------+----------╮\n| Contract        | Selector          | Calls | Reverts | Discards |\n+==================================================================+\n| TreasuryHandler | cancel            | 738   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | decreaseRate      | 751   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | deposit           | 715   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | execute           | 752   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | increaseDelay     | 734   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | queue             | 730   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | removeDestination | 769   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | reopen            | 745   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | snapshot          | 747   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | strangerQueue     | 784   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | warp              | 727   | 0       | 0        |\n╰-----------------+-------------------+-------+---------+----------╯\n\n[PASS] invariant_rulesBoundedAndStricter() (runs: 256, calls: 16384, reverts: 0)\n\n╭-----------------+-------------------+-------+---------+----------╮\n| Contract        | Selector          | Calls | Reverts | Discards |\n+==================================================================+\n| TreasuryHandler | cancel            | 1473  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | decreaseRate      | 1475  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | deposit           | 1466  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | execute           | 1465  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | increaseDelay     | 1454  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | queue             | 1471  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | removeDestination | 1531  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | reopen            | 1478  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | snapshot          | 1515  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | strangerQueue     | 1561  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | warp              | 1495  | 0       | 0        |\n╰-----------------+-------------------+-------+---------+----------╯\n\n[PASS] invariant_windowNeverOverspent() (runs: 256, calls: 16384, reverts: 0)\n\n╭-----------------+-------------------+-------+---------+----------╮\n| Contract        | Selector          | Calls | Reverts | Discards |\n+==================================================================+\n| TreasuryHandler | cancel            | 1473  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | decreaseRate      | 1475  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | deposit           | 1466  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | execute           | 1465  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | increaseDelay     | 1454  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | queue             | 1471  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | removeDestination | 1531  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | reopen            | 1478  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | snapshot          | 1515  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | strangerQueue     | 1561  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | warp              | 1495  | 0       | 0        |\n╰-----------------+-------------------+-------+---------+----------╯\n\n[PASS] test_HandlerReachesExecution() (gas: 1156492)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.77s (3.73s CPU time)\n\nRan 25 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_ReleasedNeverExceedsDeposited(uint96,uint32[6],uint32) (runs: 512, μ: 1943224, ~: 1964655)\n[PASS] test_BeneficiaryHandoverAtTheExactSecondAndAfter() (gas: 1614044)\n[PASS] test_CancelledProposalCanNotBeAccepted() (gas: 1885945)\n[PASS] test_CreateTreasuryRejectsBadArguments() (gas: 2265161)\n[PASS] test_CreateVestingRejectsZeroAmountAndZeroBeneficiary() (gas: 286064)\n[PASS] test_DuplicateDestinationsAreListedOnce() (gas: 1983540)\n[PASS] test_DurationFloorIsExactlyFourWeeks() (gas: 1456354)\n[PASS] test_FeeFailureLeavesNothingBehind() (gas: 2878088)\n[PASS] test_FeeIsChargedToTheCallerOnlyOnceEvenAcrossKinds() (gas: 4130667)\n[PASS] test_NothingAtTheCliffSecondThenLinear() (gas: 1383881)\n[PASS] test_OnlyTheBeneficiaryChangesTheSchedule() (gas: 1364836)\n[PASS] test_OpenVaultReleaseBeforeAnythingArrivesReverts() (gas: 1276259)\n[PASS] test_PayForRejectsZeroTokenAndSecondPayment() (gas: 166086)\n[PASS] test_PayForThisChainCoversTheFirstVault() (gas: 1368363)\n[PASS] test_ReleaseTwiceInOneBlockPaysOnce() (gas: 1411705)\n[PASS] test_SyncIsANoOpOnABoundVaultAndRefusesOtherAssets() (gas: 1347877)\n[PASS] test_TreasuryCapExactlyAvailableThenOneWeiOver() (gas: 2416942)\n[PASS] test_TreasuryDelayAndGraceBoundaries() (gas: 2409187)\n[PASS] test_TreasuryDepositAndQueueRejectZero() (gas: 1861071)\n[PASS] test_TreasuryDestinationRulesFailurePaths() (gas: 2050973)\n[PASS] test_TreasuryEthToARefusingDestinationRollsBackAndCanRetry() (gas: 2461180)\n[PASS] test_TreasuryLastSecondOfGraceExecutes() (gas: 2121771)\n[PASS] test_TreasuryManagerHandoverFailurePaths() (gas: 2066517)\n[PASS] test_TreasuryRuleBoundsAtTheEdges() (gas: 2037096)\n[PASS] test_TreasuryUnknownIdsRevert() (gas: 1861973)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 2.92s (68.17ms CPU time)\n\nRan 6 tests for test/Invariants.t.sol:VestingInvariantTest\n[PASS] invariant_conservation() (runs: 128, calls: 8192, reverts: 2)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 932   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 936   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 955   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 822   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 902   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 913   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 905   | 2       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 909   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 918   | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] invariant_immutables() (runs: 256, calls: 16384, reverts: 3)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 1887  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 1775  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 1931  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 1684  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 1813  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 1817  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 1823  | 3       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 1843  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 1811  | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] invariant_noEarlyRelease() (runs: 256, calls: 16384, reverts: 2)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 1887  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 1775  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 1931  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 1684  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 1813  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 1817  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 1823  | 2       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 1843  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 1811  | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] invariant_openVaultClock() (runs: 256, calls: 16384, reverts: 2)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 1887  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 1775  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 1931  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 1684  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 1813  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 1817  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 1823  | 2       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 1843  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 1811  | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] invariant_schedule() (runs: 256, calls: 16384, reverts: 2)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 1887  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 1775  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 1931  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 1684  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 1813  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 1817  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 1823  | 2       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 1843  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 1811  | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] test_HandlerReachesRelease() (gas: 1070291)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 2.92s (7.38s CPU time)\n\nRan 38 tests for test/Vault.t.sol:VaultTest\n[PASS] testFuzz_TreasuryNeverExceedsWeeklyCap(uint256,uint16,uint256[5]) (runs: 512, μ: 2898039, ~: 2905362)\n[PASS] testFuzz_VestingNeverOverpays(uint256,uint64,uint64,uint32[4]) (runs: 512, μ: 1571203, ~: 1539020)\n[PASS] testFuzz_VestingNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64) (runs: 512, μ: 1290682, ~: 1294502)\n[PASS] test_BoundVestingRejectsOtherTokens() (gas: 1353104)\n[PASS] test_CreateVestingNeedsAToken() (gas: 36022)\n[PASS] test_EachLaunchVaultPaysItsOwnFee() (gas: 4152513)\n[PASS] test_FactoryBurnsFeeOncePerProjectAndRegisters() (gas: 3008188)\n[PASS] test_FactoryChargesEachNewProject() (gas: 3535292)\n[PASS] test_FactoryNeedsRadarWalletWhenCharging() (gas: 6067)\n[PASS] test_FactoryRejectsShortDelay() (gas: 36546)\n[PASS] test_FactoryRevertsWithoutImd() (gas: 1681822)\n[PASS] test_FeeGoesToRadarNotBurned() (gas: 1277275)\n[PASS] test_FreeFactoryOnChainsWithoutImd() (gas: 1329764)\n[PASS] test_LaunchTreasuryReceivesRemainder() (gas: 2743462)\n[PASS] test_LaunchVestingReceivesRemainderAndVests() (gas: 2083511)\n[PASS] test_LaunchVestingVestsEveryTokenAndHasNoRecover() (gas: 2232717)\n[PASS] test_PayForRobinhoodProject() (gas: 149482)\n[PASS] test_RobinhoodLaunchVaultPaidOnEthereum() (gas: 1253428)\n[PASS] test_TreasuryCancelAndExpiry() (gas: 2195171)\n[PASS] test_TreasuryCapIgnoresDepositsWithinEpoch() (gas: 2332044)\n[PASS] test_TreasuryDelayAndAnyoneExecutes() (gas: 2271099)\n[PASS] test_TreasuryDestinationAllowlist() (gas: 1880115)\n[PASS] test_TreasuryEth() (gas: 2152869)\n[PASS] test_TreasuryManagerChangeIsDelayed() (gas: 1923037)\n[PASS] test_TreasuryOnlyManagerQueues() (gas: 1804546)\n[PASS] test_TreasuryRateCappedAt25Percent() (gas: 1860429)\n[PASS] test_TreasuryRulesOnlyTighten() (gas: 2209413)\n[PASS] test_TreasuryTighteningHoldsQueued() (gas: 2445259)\n[PASS] test_TreasuryWeeklyCap() (gas: 2391975)\n[PASS] test_VestingBeneficiaryCancel() (gas: 1364219)\n[PASS] test_VestingBeneficiaryChangeIsDelayed() (gas: 1495314)\n[PASS] test_VestingCliffThenLinear() (gas: 1576216)\n[PASS] test_VestingExtendAfterReleaseNeverUnderflows() (gas: 1391586)\n[PASS] test_VestingFeeOnTransferToken() (gas: 2047840)\n[PASS] test_VestingOnlyExtends() (gas: 1357271)\n[PASS] test_VestingRecoverOtherTokensOnly() (gas: 1412024)\n[PASS] test_VestingRejectsFasterThan25PercentAWeek() (gas: 1405749)\n[PASS] test_VestingTopUpFollowsSchedule() (gas: 1310793)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 2.92s (1.89s CPU time)\n\nRan 15 tests for test/Audit.t.sol:AuditTest\n[PASS] testFuzz_LateArrivalNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64,uint64) (runs: 512, μ: 1383346, ~: 1387871)\n[PASS] testFuzz_VestingNeverMoreThan25PercentInFirstWeek(uint64,uint64,uint64) (runs: 512, μ: 164027, ~: 157381)\n[PASS] test_ArrivalInsideTheCliffKeepsTheSchedule() (gas: 1380294)\n[PASS] test_BoundVaultHasNoArrivalClock() (gas: 1353828)\n[PASS] test_EachTokenInAnOpenVaultHasItsOwnClock() (gas: 1512431)\n[PASS] test_ExtendDurationCannotOverflowTheSchedule() (gas: 1320607)\n[PASS] test_FirstReleaseOfANewTokenOnlyStartsItsClock() (gas: 1459684)\n[PASS] test_LateArrivalVestsFromArrival() (gas: 1614165)\n[PASS] test_OpenVaultRecoverAlwaysRefuses() (gas: 1281584)\n[PASS] test_RecoverRefusesASecondEntryPointToTheVestedToken() (gas: 2417595)\n[PASS] test_SyncIsIdempotentAndNeedsABalance() (gas: 1436917)\n[PASS] test_TreasuryConstructorEnforcesMinDelay() (gas: 8667)\n[PASS] test_TreasuryEpochBoundaryBoundIsRatePlusRateOfRemainder() (gas: 2509887)\n[PASS] test_VestingRejectsOverflowingSchedule() (gas: 1659284)\n[PASS] test_VestingRejectsStartInThePast() (gas: 2848529)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 2.92s (119.94ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:FactoryInvariantTest\n[PASS] invariant_feesAndRegistry() (runs: 256, calls: 16384, reverts: 0)\n\n╭----------------+---------------------+-------+---------+----------╮\n| Contract       | Selector            | Calls | Reverts | Discards |\n+===================================================================+\n| FactoryHandler | createLaunchVesting | 4059  | 0       | 0        |\n|----------------+---------------------+-------+---------+----------|\n| FactoryHandler | createTreasury      | 4149  | 0       | 0        |\n|----------------+---------------------+-------+---------+----------|\n| FactoryHandler | createVesting       | 4087  | 0       | 0        |\n|----------------+---------------------+-------+---------+----------|\n| FactoryHandler | payFor              | 4089  | 0       | 0        |\n╰----------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.26s (6.26s CPU time)\n\nRan 7 test suites in 6.26s (19.73s CPU time): 90 tests passed, 0 failed, 0 skipped (90 total tests)\n","passed":true},{"durationMs":54,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"TreasuryVault.acceptManager()\",\"TreasuryVault.cancel(uint256)\",\"TreasuryVault.cancelManager()\",\"TreasuryVault.decreaseRate(uint16)\",\"TreasuryVault.deposit(address,uint256)\",\"TreasuryVault.execute(uint256)\",\"TreasuryVault.increaseDelay(uint64)\",\"TreasuryVault.proposeManager(address)\",\"TreasuryVault.queue(address,address,uint256)\",\"TreasuryVault.receive()\",\"TreasuryVault.removeDestination(address)\",\"TreasuryVault.restrictDestinations(address[])\",\"VaultFactory.createLaunchVesting(address,uint64,uint64,uint64)\",\"VaultFactory.createTreasury(address,address,uint64,uint16,address[])\",\"VaultFactory.createVesting(address,address,uint256,uint64,uint64,uint64)\",\"VaultFactory.payFor(uint256,address)\",\"VestingVault.acceptBeneficiary()\",\"VestingVault.cancelBeneficiary()\",\"VestingVault.extendDuration(uint64)\",\"VestingVault.proposeBeneficiary(address)\",\"VestingVault.recover(address)\",\"VestingVault.release(address)\",\"VestingVault.sync(address)\"],\"files\":{\".gitignore\":6,\"ADAPTATION.md\":47,\"README.md\":88,\"SWARM_BRIEF.md\":131,\"foundry.toml\":23,\"src/TreasuryVault.sol\":285,\"src/VaultFactory.sol\":147,\"src/VestingVault.sol\":221,\"test/Audit.t.sol\":335,\"test/EdgeCases.t.sol\":514,\"test/Fork.t.sol\":37,\"test/Invariants.t.sol\":755,\"test/Vault.t.sol\":615},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"14b104f812e56683335f02b9fe264b6ea93561b2069f8a4b43f742a0b8b212af","verifiedTreeHash":"eba8e1f890207bcf68b8b1f5dc590c533457047c","verifierVersion":"0.1.0+a6ee6548"},{"checks":[{"durationMs":9923,"exitCode":0,"name":"build","output":"Compiling 41 files with Solc 0.8.26\nSolc 0.8.26 finished in 9.75s\nCompiler run successful!\n","passed":true},{"durationMs":14942,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Fork.t.sol:ForkTest\n[PASS] test_RealImdFeeAndVesting() (gas: 3689)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 468.28µs (126.84µs CPU time)\n\nRan 9 tests for test/SharedCap.t.sol:SharedCapEdgeTest\n[PASS] testFuzz_ExpiryNeverMovesWithTheDelay(uint64,uint64,uint256) (runs: 512, μ: 2309378, ~: 2298555)\n[PASS] test_CreatorIsRecordedForEveryPathAndFreeVaultsStillCarryIt() (gas: 4553862)\n[PASS] test_ExpiryIsInclusiveAndOneSecondLaterRefused() (gas: 618758)\n[PASS] test_IncreaseDelayShortensButKeepsAnOpenWindow() (gas: 440014)\n[PASS] test_PendingManagerEtaResetsOnReproposeAndClearsOnCancel() (gas: 222843)\n[PASS] test_QueueingTheSameAssetTwiceTracksItOnceAndEthNever() (gas: 689893)\n[PASS] test_SharedChargeOpensTheOtherWindowAtTheLowerRate() (gas: 981250)\n[PASS] test_SharedWindowSpansEpochsCorrectly() (gas: 812676)\n[PASS] test_UnreadableTrackedAssetIsSkippedEitherWay() (gas: 1485134)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 103.89ms (73.15ms CPU time)\n\nRan 38 tests for test/Vault.t.sol:VaultTest\n[PASS] testFuzz_TreasuryNeverExceedsWeeklyCap(uint256,uint16,uint256[5]) (runs: 512, μ: 3256135, ~: 3270449)\n[PASS] testFuzz_VestingNeverOverpays(uint256,uint64,uint64,uint32[4]) (runs: 512, μ: 1598233, ~: 1563771)\n[PASS] testFuzz_VestingNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64) (runs: 512, μ: 1315236, ~: 1318945)\n[PASS] test_BoundVestingRejectsOtherTokens() (gas: 1377547)\n[PASS] test_CreateVestingNeedsAToken() (gas: 36022)\n[PASS] test_EachLaunchVaultPaysItsOwnFee() (gas: 4454024)\n[PASS] test_FactoryBurnsFeeOncePerProjectAndRegisters() (gas: 3289130)\n[PASS] test_FactoryChargesEachNewProject() (gas: 3816278)\n[PASS] test_FactoryNeedsRadarWalletWhenCharging() (gas: 6067)\n[PASS] test_FactoryRejectsShortDelay() (gas: 36613)\n[PASS] test_FactoryRevertsWithoutImd() (gas: 1915886)\n[PASS] test_FeeGoesToRadarNotBurned() (gas: 1301718)\n[PASS] test_FreeFactoryOnChainsWithoutImd() (gas: 1354185)\n[PASS] test_LaunchTreasuryReceivesRemainder() (gas: 3072408)\n[PASS] test_LaunchVestingReceivesRemainderAndVests() (gas: 2106028)\n[PASS] test_LaunchVestingVestsEveryTokenAndHasNoRecover() (gas: 2255256)\n[PASS] test_PayForRobinhoodProject() (gas: 149438)\n[PASS] test_RobinhoodLaunchVaultPaidOnEthereum() (gas: 1275945)\n[PASS] test_TreasuryCancelAndExpiry() (gas: 2521705)\n[PASS] test_TreasuryCapIgnoresDepositsWithinEpoch() (gas: 2669294)\n[PASS] test_TreasuryDelayAndAnyoneExecutes() (gas: 2600089)\n[PASS] test_TreasuryDestinationAllowlist() (gas: 2136658)\n[PASS] test_TreasuryEth() (gas: 2409792)\n[PASS] test_TreasuryManagerChangeIsDelayed() (gas: 2182109)\n[PASS] test_TreasuryOnlyManagerQueues() (gas: 2061089)\n[PASS] test_TreasuryRateCappedAt25Percent() (gas: 2117378)\n[PASS] test_TreasuryRulesOnlyTighten() (gas: 2466036)\n[PASS] test_TreasuryTighteningHoldsQueued() (gas: 2777026)\n[PASS] test_TreasuryWeeklyCap() (gas: 2734436)\n[PASS] test_VestingBeneficiaryCancel() (gas: 1388662)\n[PASS] test_VestingBeneficiaryChangeIsDelayed() (gas: 1519757)\n[PASS] test_VestingCliffThenLinear() (gas: 1600659)\n[PASS] test_VestingExtendAfterReleaseNeverUnderflows() (gas: 1416029)\n[PASS] test_VestingFeeOnTransferToken() (gas: 2072283)\n[PASS] test_VestingOnlyExtends() (gas: 1381714)\n[PASS] test_VestingRecoverOtherTokensOnly() (gas: 1436467)\n[PASS] test_VestingRejectsFasterThan25PercentAWeek() (gas: 1430192)\n[PASS] test_VestingTopUpFollowsSchedule() (gas: 1335236)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 1.62s (1.31s CPU time)\n\nRan 2 tests for test/SharedCap.t.sol:SharedCapInvariantTest\n[PASS]\nSharedCapInvariantTest invariants:\n[PASS] invariant_conservation\n[PASS] invariant_sharedCap\n SharedCapInvariantTest invariants (runs: 256, calls: 16384, reverts: 0)\n\n╭------------------+---------------+-------+---------+----------╮\n| Contract         | Selector      | Calls | Reverts | Discards |\n+===============================================================+\n| SharedCapHandler | cancel        | 2337  | 0       | 0        |\n|------------------+---------------+-------+---------+----------|\n| SharedCapHandler | decreaseRate  | 2318  | 0       | 0        |\n|------------------+---------------+-------+---------+----------|\n| SharedCapHandler | deposit       | 2246  | 0       | 0        |\n|------------------+---------------+-------+---------+----------|\n| SharedCapHandler | execute       | 2436  | 0       | 0        |\n|------------------+---------------+-------+---------+----------|\n| SharedCapHandler | increaseDelay | 2379  | 0       | 0        |\n|------------------+---------------+-------+---------+----------|\n| SharedCapHandler | queue         | 2302  | 0       | 0        |\n|------------------+---------------+-------+---------+----------|\n| SharedCapHandler | warp          | 2366  | 0       | 0        |\n╰------------------+---------------+-------+---------+----------╯\n\n[PASS] test_HandlerReachesBothSidesOfTheSharedCap() (gas: 1451014)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.37s (2.37s CPU time)\n\nRan 4 tests for test/Invariants.t.sol:TreasuryInvariantTest\n[PASS] invariant_conservation() (runs: 128, calls: 8192, reverts: 0)\n\n╭-----------------+-------------------+-------+---------+----------╮\n| Contract        | Selector          | Calls | Reverts | Discards |\n+==================================================================+\n| TreasuryHandler | cancel            | 738   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | decreaseRate      | 751   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | deposit           | 715   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | execute           | 752   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | increaseDelay     | 734   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | queue             | 730   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | removeDestination | 769   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | reopen            | 745   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | snapshot          | 747   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | strangerQueue     | 784   | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | warp              | 727   | 0       | 0        |\n╰-----------------+-------------------+-------+---------+----------╯\n\n[PASS] invariant_rulesBoundedAndStricter() (runs: 256, calls: 16384, reverts: 0)\n\n╭-----------------+-------------------+-------+---------+----------╮\n| Contract        | Selector          | Calls | Reverts | Discards |\n+==================================================================+\n| TreasuryHandler | cancel            | 1473  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | decreaseRate      | 1475  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | deposit           | 1466  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | execute           | 1465  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | increaseDelay     | 1454  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | queue             | 1471  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | removeDestination | 1531  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | reopen            | 1478  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | snapshot          | 1515  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | strangerQueue     | 1561  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | warp              | 1495  | 0       | 0        |\n╰-----------------+-------------------+-------+---------+----------╯\n\n[PASS] invariant_windowNeverOverspent() (runs: 256, calls: 16384, reverts: 0)\n\n╭-----------------+-------------------+-------+---------+----------╮\n| Contract        | Selector          | Calls | Reverts | Discards |\n+==================================================================+\n| TreasuryHandler | cancel            | 1473  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | decreaseRate      | 1475  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | deposit           | 1466  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | execute           | 1465  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | increaseDelay     | 1454  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | queue             | 1471  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | removeDestination | 1531  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | reopen            | 1478  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | snapshot          | 1515  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | strangerQueue     | 1561  | 0       | 0        |\n|-----------------+-------------------+-------+---------+----------|\n| TreasuryHandler | warp              | 1495  | 0       | 0        |\n╰-----------------+-------------------+-------+---------+----------╯\n\n[PASS] test_HandlerReachesExecution() (gas: 1283085)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 2.70s (5.66s CPU time)\n\nRan 23 tests for test/Audit.t.sol:AuditTest\n[PASS] testFuzz_LateArrivalNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64,uint64) (runs: 512, μ: 1406444, ~: 1410517)\n[PASS] testFuzz_VestingNeverMoreThan25PercentInFirstWeek(uint64,uint64,uint64) (runs: 512, μ: 161947, ~: 157750)\n[PASS] test_ArrivalInsideTheCliffKeepsTheSchedule() (gas: 1402921)\n[PASS] test_BoundVaultHasNoArrivalClock() (gas: 1378328)\n[PASS] test_EachTokenInAnOpenVaultHasItsOwnClock() (gas: 1535004)\n[PASS] test_ExtendDurationCannotOverflowTheSchedule() (gas: 1345050)\n[PASS] test_FactoryRecordsTheCreatorOfEveryVault() (gas: 4478684)\n[PASS] test_FirstReleaseOfANewTokenOnlyStartsItsClock() (gas: 1482328)\n[PASS] test_IncreaseDelayNeverRevivesAnExpiredWithdrawal() (gas: 2429868)\n[PASS] test_IncreaseDelayPastTheExpiryClosesAQueuedWithdrawal() (gas: 2675690)\n[PASS] test_LateArrivalVestsFromArrival() (gas: 1636726)\n[PASS] test_OpenVaultRecoverAlwaysRefuses() (gas: 1304145)\n[PASS] test_PendingManagerFollowsTheCurrentDelay() (gas: 2205226)\n[PASS] test_RecoverRefusesASecondEntryPointToTheVestedToken() (gas: 2442231)\n[PASS] test_SyncIsIdempotentAndNeedsABalance() (gas: 1459456)\n[PASS] test_TreasuryCapIsSharedByTwoEntryPointsToOneBalance() (gas: 4153927)\n[PASS] test_TreasuryCapThroughLegacyAddressFirstIsSharedToo() (gas: 3602827)\n[PASS] test_TreasuryConstructorEnforcesMinDelay() (gas: 8689)\n[PASS] test_TreasuryEpochBoundaryBoundIsRatePlusRateOfRemainder() (gas: 2855323)\n[PASS] test_TreasuryTrackedAssetsNeverBlockOtherWithdrawals() (gas: 3379736)\n[PASS] test_VestingCreatedLogsWhatTheVaultReceived() (gas: 1971615)\n[PASS] test_VestingRejectsOverflowingSchedule() (gas: 1683748)\n[PASS] test_VestingRejectsStartInThePast() (gas: 2895533)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 4.71s (4.80s CPU time)\n\nRan 25 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_ReleasedNeverExceedsDeposited(uint96,uint32[6],uint32) (runs: 512, μ: 1965225, ~: 1981374)\n[PASS] test_BeneficiaryHandoverAtTheExactSecondAndAfter() (gas: 1638487)\n[PASS] test_CancelledProposalCanNotBeAccepted() (gas: 2142459)\n[PASS] test_CreateTreasuryRejectsBadArguments() (gas: 2523801)\n[PASS] test_CreateVestingRejectsZeroAmountAndZeroBeneficiary() (gas: 286042)\n[PASS] test_DuplicateDestinationsAreListedOnce() (gas: 2240121)\n[PASS] test_DurationFloorIsExactlyFourWeeks() (gas: 1478893)\n[PASS] test_FeeFailureLeavesNothingBehind() (gas: 3112130)\n[PASS] test_FeeIsChargedToTheCallerOnlyOnceEvenAcrossKinds() (gas: 4436074)\n[PASS] test_NothingAtTheCliffSecondThenLinear() (gas: 1408324)\n[PASS] test_OnlyTheBeneficiaryChangesTheSchedule() (gas: 1389279)\n[PASS] test_OpenVaultReleaseBeforeAnythingArrivesReverts() (gas: 1298798)\n[PASS] test_PayForRejectsZeroTokenAndSecondPayment() (gas: 166042)\n[PASS] test_PayForThisChainCoversTheFirstVault() (gas: 1392806)\n[PASS] test_ReleaseTwiceInOneBlockPaysOnce() (gas: 1436148)\n[PASS] test_SyncIsANoOpOnABoundVaultAndRefusesOtherAssets() (gas: 1372320)\n[PASS] test_TreasuryCapExactlyAvailableThenOneWeiOver() (gas: 2759403)\n[PASS] test_TreasuryDelayAndGraceBoundaries() (gas: 2740954)\n[PASS] test_TreasuryDepositAndQueueRejectZero() (gas: 2117614)\n[PASS] test_TreasuryDestinationRulesFailurePaths() (gas: 2307670)\n[PASS] test_TreasuryEthToARefusingDestinationRollsBackAndCanRetry() (gas: 2718142)\n[PASS] test_TreasuryLastSecondOfGraceExecutes() (gas: 2450717)\n[PASS] test_TreasuryManagerHandoverFailurePaths() (gas: 2327925)\n[PASS] test_TreasuryRuleBoundsAtTheEdges() (gas: 2293639)\n[PASS] test_TreasuryUnknownIdsRevert() (gas: 2118538)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 4.71s (117.33ms CPU time)\n\nRan 6 tests for test/Invariants.t.sol:VestingInvariantTest\n[PASS] invariant_conservation() (runs: 128, calls: 8192, reverts: 3)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 932   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 936   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 955   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 822   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 902   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 913   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 905   | 3       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 909   | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 918   | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] invariant_immutables() (runs: 256, calls: 16384, reverts: 3)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 1887  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 1775  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 1931  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 1684  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 1813  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 1817  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 1823  | 3       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 1843  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 1811  | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] invariant_noEarlyRelease() (runs: 256, calls: 16384, reverts: 3)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 1887  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 1775  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 1931  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 1684  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 1813  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 1817  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 1823  | 3       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 1843  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 1811  | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] invariant_openVaultClock() (runs: 256, calls: 16384, reverts: 3)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 1887  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 1775  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 1931  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 1684  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 1813  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 1817  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 1823  | 3       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 1843  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 1811  | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] invariant_schedule() (runs: 256, calls: 16384, reverts: 3)\n\n╭----------------+-------------------------+-------+---------+----------╮\n| Contract       | Selector                | Calls | Reverts | Discards |\n+=======================================================================+\n| VestingHandler | accept                  | 1887  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | extend                  | 1775  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | propose                 | 1931  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | recoverVested           | 1684  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | release                 | 1813  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | strangerExtendOrPropose | 1817  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | sync                    | 1823  | 3       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | topUp                   | 1843  | 0       | 0        |\n|----------------+-------------------------+-------+---------+----------|\n| VestingHandler | warp                    | 1811  | 0       | 0        |\n╰----------------+-------------------------+-------+---------+----------╯\n\n[PASS] test_HandlerReachesRelease() (gas: 1070291)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 4.71s (11.44s CPU time)\n\nRan 1 test for test/Invariants.t.sol:FactoryInvariantTest\n[PASS] invariant_feesAndRegistry() (runs: 256, calls: 16384, reverts: 0)\n\n╭----------------+---------------------+-------+---------+----------╮\n| Contract       | Selector            | Calls | Reverts | Discards |\n+===================================================================+\n| FactoryHandler | createLaunchVesting | 4059  | 0       | 0        |\n|----------------+---------------------+-------+---------+----------|\n| FactoryHandler | createTreasury      | 4149  | 0       | 0        |\n|----------------+---------------------+-------+---------+----------|\n| FactoryHandler | createVesting       | 4087  | 0       | 0        |\n|----------------+---------------------+-------+---------+----------|\n| FactoryHandler | payFor              | 4089  | 0       | 0        |\n╰----------------+---------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.75s (14.75s CPU time)\n\nRan 9 test suites in 14.75s (35.68s CPU time): 109 tests passed, 0 failed, 0 skipped (109 total tests)\n","passed":true},{"durationMs":117,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"TreasuryVault.acceptManager()\",\"TreasuryVault.cancel(uint256)\",\"TreasuryVault.cancelManager()\",\"TreasuryVault.decreaseRate(uint16)\",\"TreasuryVault.deposit(address,uint256)\",\"TreasuryVault.execute(uint256)\",\"TreasuryVault.increaseDelay(uint64)\",\"TreasuryVault.proposeManager(address)\",\"TreasuryVault.queue(address,address,uint256)\",\"TreasuryVault.receive()\",\"TreasuryVault.removeDestination(address)\",\"TreasuryVault.restrictDestinations(address[])\",\"VaultFactory.createLaunchVesting(address,uint64,uint64,uint64)\",\"VaultFactory.createTreasury(address,address,uint64,uint16,address[])\",\"VaultFactory.createVesting(address,address,uint256,uint64,uint64,uint64)\",\"VaultFactory.payFor(uint256,address)\",\"VestingVault.acceptBeneficiary()\",\"VestingVault.cancelBeneficiary()\",\"VestingVault.extendDuration(uint64)\",\"VestingVault.proposeBeneficiary(address)\",\"VestingVault.recover(address)\",\"VestingVault.release(address)\",\"VestingVault.sync(address)\"],\"files\":{\".gitignore\":6,\"ADAPTATION.md\":76,\"README.md\":115,\"SWARM_BRIEF.md\":131,\"foundry.toml\":23,\"src/TreasuryVault.sol\":354,\"src/VaultFactory.sol\":165,\"src/VestingVault.sol\":224,\"test/Audit.t.sol\":553,\"test/EdgeCases.t.sol\":514,\"test/Fork.t.sol\":37,\"test/Invariants.t.sol\":761,\"test/SharedCap.t.sol\":477,\"test/Vault.t.sol\":615},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"2720833a198d41c0f4b42095980f27695fc2a73dfe9888c396fb3d54cba684be","verifiedTreeHash":"5188399bc053ede4d9e1a4d79f524613b9bc207f","verifierVersion":"0.1.0+a6ee6548"},{"checks":[{"durationMs":2035,"exitCode":0,"name":"build","output":"Compiling 38 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.94s\nCompiler run successful!\n","passed":true},{"durationMs":139,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Fork.t.sol:ForkTest\n[PASS] test_RealImdFeeAndVesting() (gas: 3689)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 228.61µs (57.27µs CPU time)\n\nRan 15 tests for test/Audit.t.sol:AuditTest\n[PASS] testFuzz_LateArrivalNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64,uint64) (runs: 512, μ: 1383023, ~: 1387847)\n[PASS] testFuzz_VestingNeverMoreThan25PercentInFirstWeek(uint64,uint64,uint64) (runs: 512, μ: 166231, ~: 157772)\n[PASS] test_ArrivalInsideTheCliffKeepsTheSchedule() (gas: 1380294)\n[PASS] test_BoundVaultHasNoArrivalClock() (gas: 1353828)\n[PASS] test_EachTokenInAnOpenVaultHasItsOwnClock() (gas: 1512431)\n[PASS] test_ExtendDurationCannotOverflowTheSchedule() (gas: 1320607)\n[PASS] test_FirstReleaseOfANewTokenOnlyStartsItsClock() (gas: 1459684)\n[PASS] test_LateArrivalVestsFromArrival() (gas: 1614165)\n[PASS] test_OpenVaultRecoverAlwaysRefuses() (gas: 1281584)\n[PASS] test_RecoverRefusesASecondEntryPointToTheVestedToken() (gas: 2417595)\n[PASS] test_SyncIsIdempotentAndNeedsABalance() (gas: 1436917)\n[PASS] test_TreasuryConstructorEnforcesMinDelay() (gas: 8667)\n[PASS] test_TreasuryEpochBoundaryBoundIsRatePlusRateOfRemainder() (gas: 2509887)\n[PASS] test_VestingRejectsOverflowingSchedule() (gas: 1659284)\n[PASS] test_VestingRejectsStartInThePast() (gas: 2848529)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 33.69ms (63.75ms CPU time)\n\nRan 38 tests for test/Vault.t.sol:VaultTest\n[PASS] testFuzz_TreasuryNeverExceedsWeeklyCap(uint256,uint16,uint256[5]) (runs: 512, μ: 2898104, ~: 2905363)\n[PASS] testFuzz_VestingNeverOverpays(uint256,uint64,uint64,uint32[4]) (runs: 512, μ: 1570501, ~: 1539310)\n[PASS] testFuzz_VestingNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64) (runs: 512, μ: 1290531, ~: 1294501)\n[PASS] test_BoundVestingRejectsOtherTokens() (gas: 1353104)\n[PASS] test_CreateVestingNeedsAToken() (gas: 36022)\n[PASS] test_EachLaunchVaultPaysItsOwnFee() (gas: 4152513)\n[PASS] test_FactoryBurnsFeeOncePerProjectAndRegisters() (gas: 3008188)\n[PASS] test_FactoryChargesEachNewProject() (gas: 3535292)\n[PASS] test_FactoryNeedsRadarWalletWhenCharging() (gas: 6067)\n[PASS] test_FactoryRejectsShortDelay() (gas: 36546)\n[PASS] test_FactoryRevertsWithoutImd() (gas: 1681822)\n[PASS] test_FeeGoesToRadarNotBurned() (gas: 1277275)\n[PASS] test_FreeFactoryOnChainsWithoutImd() (gas: 1329764)\n[PASS] test_LaunchTreasuryReceivesRemainder() (gas: 2743462)\n[PASS] test_LaunchVestingReceivesRemainderAndVests() (gas: 2083511)\n[PASS] test_LaunchVestingVestsEveryTokenAndHasNoRecover() (gas: 2232717)\n[PASS] test_PayForRobinhoodProject() (gas: 149482)\n[PASS] test_RobinhoodLaunchVaultPaidOnEthereum() (gas: 1253428)\n[PASS] test_TreasuryCancelAndExpiry() (gas: 2195171)\n[PASS] test_TreasuryCapIgnoresDepositsWithinEpoch() (gas: 2332044)\n[PASS] test_TreasuryDelayAndAnyoneExecutes() (gas: 2271099)\n[PASS] test_TreasuryDestinationAllowlist() (gas: 1880115)\n[PASS] test_TreasuryEth() (gas: 2152869)\n[PASS] test_TreasuryManagerChangeIsDelayed() (gas: 1923037)\n[PASS] test_TreasuryOnlyManagerQueues() (gas: 1804546)\n[PASS] test_TreasuryRateCappedAt25Percent() (gas: 1860429)\n[PASS] test_TreasuryRulesOnlyTighten() (gas: 2209413)\n[PASS] test_TreasuryTighteningHoldsQueued() (gas: 2445259)\n[PASS] test_TreasuryWeeklyCap() (gas: 2391975)\n[PASS] test_VestingBeneficiaryCancel() (gas: 1364219)\n[PASS] test_VestingBeneficiaryChangeIsDelayed() (gas: 1495314)\n[PASS] test_VestingCliffThenLinear() (gas: 1576216)\n[PASS] test_VestingExtendAfterReleaseNeverUnderflows() (gas: 1391586)\n[PASS] test_VestingFeeOnTransferToken() (gas: 2047840)\n[PASS] test_VestingOnlyExtends() (gas: 1357271)\n[PASS] test_VestingRecoverOtherTokensOnly() (gas: 1412024)\n[PASS] test_VestingRejectsFasterThan25PercentAWeek() (gas: 1405749)\n[PASS] test_VestingTopUpFollowsSchedule() (gas: 1310793)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 36.69ms (101.83ms CPU time)\n\nRan 3 test suites in 37.30ms (70.62ms CPU time): 54 tests passed, 0 failed, 0 skipped (54 total tests)\n","passed":true},{"durationMs":74,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"TreasuryVault.acceptManager()\",\"TreasuryVault.cancel(uint256)\",\"TreasuryVault.cancelManager()\",\"TreasuryVault.decreaseRate(uint16)\",\"TreasuryVault.deposit(address,uint256)\",\"TreasuryVault.execute(uint256)\",\"TreasuryVault.increaseDelay(uint64)\",\"TreasuryVault.proposeManager(address)\",\"TreasuryVault.queue(address,address,uint256)\",\"TreasuryVault.receive()\",\"TreasuryVault.removeDestination(address)\",\"TreasuryVault.restrictDestinations(address[])\",\"VaultFactory.createLaunchVesting(address,uint64,uint64,uint64)\",\"VaultFactory.createTreasury(address,address,uint64,uint16,address[])\",\"VaultFactory.createVesting(address,address,uint256,uint64,uint64,uint64)\",\"VaultFactory.payFor(uint256,address)\",\"VestingVault.acceptBeneficiary()\",\"VestingVault.cancelBeneficiary()\",\"VestingVault.extendDuration(uint64)\",\"VestingVault.proposeBeneficiary(address)\",\"VestingVault.recover(address)\",\"VestingVault.release(address)\",\"VestingVault.sync(address)\"],\"files\":{\".gitignore\":6,\"ADAPTATION.md\":47,\"README.md\":88,\"SWARM_BRIEF.md\":131,\"foundry.toml\":23,\"launch.json\":13,\"src/TreasuryVault.sol\":285,\"src/VaultFactory.sol\":147,\"src/VestingVault.sol\":221,\"test/Audit.t.sol\":335,\"test/Fork.t.sol\":37,\"test/Vault.t.sol\":615},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"8ea29e8dcb82875e8b5a644e01687463f8a4e77c1d0996573934df58e9137244","verifiedTreeHash":"0c3ba9634f1812885e9a9f7c5da557007ed21cbe","verifierVersion":"0.1.0+a6ee6548"},{"checks":[{"durationMs":1821,"exitCode":0,"name":"build","output":"Compiling 38 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.75s\nCompiler run successful!\n","passed":true},{"durationMs":117,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Fork.t.sol:ForkTest\n[PASS] test_RealImdFeeAndVesting() (gas: 3689)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 272.50µs (79.65µs CPU time)\n\nRan 15 tests for test/Audit.t.sol:AuditTest\n[PASS] testFuzz_LateArrivalNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64,uint64) (runs: 512, μ: 1383023, ~: 1387847)\n[PASS] testFuzz_VestingNeverMoreThan25PercentInFirstWeek(uint64,uint64,uint64) (runs: 512, μ: 166231, ~: 157772)\n[PASS] test_ArrivalInsideTheCliffKeepsTheSchedule() (gas: 1380294)\n[PASS] test_BoundVaultHasNoArrivalClock() (gas: 1353828)\n[PASS] test_EachTokenInAnOpenVaultHasItsOwnClock() (gas: 1512431)\n[PASS] test_ExtendDurationCannotOverflowTheSchedule() (gas: 1320607)\n[PASS] test_FirstReleaseOfANewTokenOnlyStartsItsClock() (gas: 1459684)\n[PASS] test_LateArrivalVestsFromArrival() (gas: 1614165)\n[PASS] test_OpenVaultRecoverAlwaysRefuses() (gas: 1281584)\n[PASS] test_RecoverRefusesASecondEntryPointToTheVestedToken() (gas: 2417595)\n[PASS] test_SyncIsIdempotentAndNeedsABalance() (gas: 1436917)\n[PASS] test_TreasuryConstructorEnforcesMinDelay() (gas: 8667)\n[PASS] test_TreasuryEpochBoundaryBoundIsRatePlusRateOfRemainder() (gas: 2509887)\n[PASS] test_VestingRejectsOverflowingSchedule() (gas: 1659284)\n[PASS] test_VestingRejectsStartInThePast() (gas: 2848529)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 34.48ms (54.03ms CPU time)\n\nRan 38 tests for test/Vault.t.sol:VaultTest\n[PASS] testFuzz_TreasuryNeverExceedsWeeklyCap(uint256,uint16,uint256[5]) (runs: 512, μ: 2898104, ~: 2905363)\n[PASS] testFuzz_VestingNeverOverpays(uint256,uint64,uint64,uint32[4]) (runs: 512, μ: 1570501, ~: 1539310)\n[PASS] testFuzz_VestingNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64) (runs: 512, μ: 1290531, ~: 1294501)\n[PASS] test_BoundVestingRejectsOtherTokens() (gas: 1353104)\n[PASS] test_CreateVestingNeedsAToken() (gas: 36022)\n[PASS] test_EachLaunchVaultPaysItsOwnFee() (gas: 4152513)\n[PASS] test_FactoryBurnsFeeOncePerProjectAndRegisters() (gas: 3008188)\n[PASS] test_FactoryChargesEachNewProject() (gas: 3535292)\n[PASS] test_FactoryNeedsRadarWalletWhenCharging() (gas: 6067)\n[PASS] test_FactoryRejectsShortDelay() (gas: 36546)\n[PASS] test_FactoryRevertsWithoutImd() (gas: 1681822)\n[PASS] test_FeeGoesToRadarNotBurned() (gas: 1277275)\n[PASS] test_FreeFactoryOnChainsWithoutImd() (gas: 1329764)\n[PASS] test_LaunchTreasuryReceivesRemainder() (gas: 2743462)\n[PASS] test_LaunchVestingReceivesRemainderAndVests() (gas: 2083511)\n[PASS] test_LaunchVestingVestsEveryTokenAndHasNoRecover() (gas: 2232717)\n[PASS] test_PayForRobinhoodProject() (gas: 149482)\n[PASS] test_RobinhoodLaunchVaultPaidOnEthereum() (gas: 1253428)\n[PASS] test_TreasuryCancelAndExpiry() (gas: 2195171)\n[PASS] test_TreasuryCapIgnoresDepositsWithinEpoch() (gas: 2332044)\n[PASS] test_TreasuryDelayAndAnyoneExecutes() (gas: 2271099)\n[PASS] test_TreasuryDestinationAllowlist() (gas: 1880115)\n[PASS] test_TreasuryEth() (gas: 2152869)\n[PASS] test_TreasuryManagerChangeIsDelayed() (gas: 1923037)\n[PASS] test_TreasuryOnlyManagerQueues() (gas: 1804546)\n[PASS] test_TreasuryRateCappedAt25Percent() (gas: 1860429)\n[PASS] test_TreasuryRulesOnlyTighten() (gas: 2209413)\n[PASS] test_TreasuryTighteningHoldsQueued() (gas: 2445259)\n[PASS] test_TreasuryWeeklyCap() (gas: 2391975)\n[PASS] test_VestingBeneficiaryCancel() (gas: 1364219)\n[PASS] test_VestingBeneficiaryChangeIsDelayed() (gas: 1495314)\n[PASS] test_VestingCliffThenLinear() (gas: 1576216)\n[PASS] test_VestingExtendAfterReleaseNeverUnderflows() (gas: 1391586)\n[PASS] test_VestingFeeOnTransferToken() (gas: 2047840)\n[PASS] test_VestingOnlyExtends() (gas: 1357271)\n[PASS] test_VestingRecoverOtherTokensOnly() (gas: 1412024)\n[PASS] test_VestingRejectsFasterThan25PercentAWeek() (gas: 1405749)\n[PASS] test_VestingTopUpFollowsSchedule() (gas: 1310793)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 38.06ms (109.51ms CPU time)\n\nRan 3 test suites in 38.88ms (72.81ms CPU time): 54 tests passed, 0 failed, 0 skipped (54 total tests)\n","passed":true},{"durationMs":36,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"TreasuryVault.acceptManager()\",\"TreasuryVault.cancel(uint256)\",\"TreasuryVault.cancelManager()\",\"TreasuryVault.decreaseRate(uint16)\",\"TreasuryVault.deposit(address,uint256)\",\"TreasuryVault.execute(uint256)\",\"TreasuryVault.increaseDelay(uint64)\",\"TreasuryVault.proposeManager(address)\",\"TreasuryVault.queue(address,address,uint256)\",\"TreasuryVault.receive()\",\"TreasuryVault.removeDestination(address)\",\"TreasuryVault.restrictDestinations(address[])\",\"VaultFactory.createLaunchVesting(address,uint64,uint64,uint64)\",\"VaultFactory.createTreasury(address,address,uint64,uint16,address[])\",\"VaultFactory.createVesting(address,address,uint256,uint64,uint64,uint64)\",\"VaultFactory.payFor(uint256,address)\",\"VestingVault.acceptBeneficiary()\",\"VestingVault.cancelBeneficiary()\",\"VestingVault.extendDuration(uint64)\",\"VestingVault.proposeBeneficiary(address)\",\"VestingVault.recover(address)\",\"VestingVault.release(address)\",\"VestingVault.sync(address)\"],\"files\":{\".gitignore\":6,\"ADAPTATION.md\":47,\"README.md\":88,\"SWARM_BRIEF.md\":131,\"foundry.toml\":23,\"src/TreasuryVault.sol\":285,\"src/VaultFactory.sol\":147,\"src/VestingVault.sol\":221,\"test/Audit.t.sol\":335,\"test/Fork.t.sol\":37,\"test/Vault.t.sol\":615},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":757,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/VestingVault.sol:214: VestingVault._sync(address) (src/VestingVault.sol#214-220) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/VestingVault.sol:108: VestingVault.unlockStart(address) (src/VestingVault.sol#108-114) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/VestingVault.sol:146: VestingVault.release(address) (src/VestingVault.sol#146-158) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/VestingVault.sol:117: VestingVault.vestedAmount(address,uint64) (src/VestingVault.sol#117-124) uses a dangerous strict equality:\n[low/medium] missing-zero-check at src/TreasuryVault.sol:100: TreasuryVault.constructor(address,address,uint64,uint16,address[]).projectToken_ (src/TreasuryVault.sol#100) lacks a zero-check on :\n[low/medium] timestamp at src/VestingVault.sol:136: VestingVault.releasable(address) (src/VestingVault.sol#136-141) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:151: TreasuryVault.execute(uint256) (src/TreasuryVault.sol#151-178) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:257: TreasuryVault.availableThisWeek(address) (src/TreasuryVault.sol#257-261) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:117: VestingVault.vestedAmount(address,uint64) (src/VestingVault.sol#117-124) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:235: TreasuryVault.acceptManager() (src/TreasuryVault.sol#235-244) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:214: VestingVault._sync(address) (src/VestingVault.sol#214-220) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:108: VestingVault.unlockStart(address) (src/VestingVault.sol#108-114) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:186: VestingVault.acceptBeneficiary() (src/VestingVault.sol#186-195) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:146: VestingVault.release(address) (src/VestingVault.sol#146-158) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:277: TreasuryVault._get(uint256) (src/TreasuryVault.sol#277-280) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:72: VestingVault.constructor(IERC20,address,uint64,uint64,uint64,uint64) (src/VestingVault.sol#72-92) uses timestamp for comparisons","passed":true},{"durationMs":297,"exitCode":0,"name":"aderyn","output":"[low] costly-loop at src/TreasuryVault.sol:208: Costly operations inside loop (2 places)\n[low] large-numeric-literal at src/TreasuryVault.sol:27: Large Numeric Literal\n[low] require-revert-in-loop at src/TreasuryVault.sol:267: Loop Contains `require`/`revert`\n[low] unchecked-return at src/VestingVault.sol:132: Unchecked Return\n[low] uninitialized-local-variable at src/TreasuryVault.sol:267: Uninitialized Local Variable","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9ec423e3664e3b85c77ead03663711d214aa5104b2ec22c168dc1bb01ec79c1b","verifiedTreeHash":"40d4e255daaab445386081c0a2899ef65ce77a8d","verifierVersion":"0.1.0+a6ee6548"},{"checks":[{"durationMs":1989,"exitCode":0,"name":"build","output":"Compiling 38 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.92s\nCompiler run successful!\n","passed":true},{"durationMs":128,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Fork.t.sol:ForkTest\n[PASS] test_RealImdFeeAndVesting() (gas: 3689)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 244.52µs (56.85µs CPU time)\n\nRan 23 tests for test/Audit.t.sol:AuditTest\n[PASS] testFuzz_LateArrivalNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64,uint64) (runs: 512, μ: 1405884, ~: 1410505)\n[PASS] testFuzz_VestingNeverMoreThan25PercentInFirstWeek(uint64,uint64,uint64) (runs: 512, μ: 164173, ~: 157750)\n[PASS] test_ArrivalInsideTheCliffKeepsTheSchedule() (gas: 1402921)\n[PASS] test_BoundVaultHasNoArrivalClock() (gas: 1378328)\n[PASS] test_EachTokenInAnOpenVaultHasItsOwnClock() (gas: 1535004)\n[PASS] test_ExtendDurationCannotOverflowTheSchedule() (gas: 1345050)\n[PASS] test_FactoryRecordsTheCreatorOfEveryVault() (gas: 4478684)\n[PASS] test_FirstReleaseOfANewTokenOnlyStartsItsClock() (gas: 1482328)\n[PASS] test_IncreaseDelayNeverRevivesAnExpiredWithdrawal() (gas: 2429868)\n[PASS] test_IncreaseDelayPastTheExpiryClosesAQueuedWithdrawal() (gas: 2675690)\n[PASS] test_LateArrivalVestsFromArrival() (gas: 1636726)\n[PASS] test_OpenVaultRecoverAlwaysRefuses() (gas: 1304145)\n[PASS] test_PendingManagerFollowsTheCurrentDelay() (gas: 2205226)\n[PASS] test_RecoverRefusesASecondEntryPointToTheVestedToken() (gas: 2442231)\n[PASS] test_SyncIsIdempotentAndNeedsABalance() (gas: 1459456)\n[PASS] test_TreasuryCapIsSharedByTwoEntryPointsToOneBalance() (gas: 4153927)\n[PASS] test_TreasuryCapThroughLegacyAddressFirstIsSharedToo() (gas: 3602827)\n[PASS] test_TreasuryConstructorEnforcesMinDelay() (gas: 8689)\n[PASS] test_TreasuryEpochBoundaryBoundIsRatePlusRateOfRemainder() (gas: 2855323)\n[PASS] test_TreasuryTrackedAssetsNeverBlockOtherWithdrawals() (gas: 3379736)\n[PASS] test_VestingCreatedLogsWhatTheVaultReceived() (gas: 1971615)\n[PASS] test_VestingRejectsOverflowingSchedule() (gas: 1683748)\n[PASS] test_VestingRejectsStartInThePast() (gas: 2895533)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 41.22ms (64.20ms CPU time)\n\nRan 38 tests for test/Vault.t.sol:VaultTest\n[PASS] testFuzz_TreasuryNeverExceedsWeeklyCap(uint256,uint16,uint256[5]) (runs: 512, μ: 3256169, ~: 3270472)\n[PASS] testFuzz_VestingNeverOverpays(uint256,uint64,uint64,uint32[4]) (runs: 512, μ: 1595853, ~: 1563759)\n[PASS] testFuzz_VestingNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64) (runs: 512, μ: 1315226, ~: 1318946)\n[PASS] test_BoundVestingRejectsOtherTokens() (gas: 1377547)\n[PASS] test_CreateVestingNeedsAToken() (gas: 36022)\n[PASS] test_EachLaunchVaultPaysItsOwnFee() (gas: 4454024)\n[PASS] test_FactoryBurnsFeeOncePerProjectAndRegisters() (gas: 3289130)\n[PASS] test_FactoryChargesEachNewProject() (gas: 3816278)\n[PASS] test_FactoryNeedsRadarWalletWhenCharging() (gas: 6067)\n[PASS] test_FactoryRejectsShortDelay() (gas: 36613)\n[PASS] test_FactoryRevertsWithoutImd() (gas: 1915886)\n[PASS] test_FeeGoesToRadarNotBurned() (gas: 1301718)\n[PASS] test_FreeFactoryOnChainsWithoutImd() (gas: 1354185)\n[PASS] test_LaunchTreasuryReceivesRemainder() (gas: 3072408)\n[PASS] test_LaunchVestingReceivesRemainderAndVests() (gas: 2106028)\n[PASS] test_LaunchVestingVestsEveryTokenAndHasNoRecover() (gas: 2255256)\n[PASS] test_PayForRobinhoodProject() (gas: 149438)\n[PASS] test_RobinhoodLaunchVaultPaidOnEthereum() (gas: 1275945)\n[PASS] test_TreasuryCancelAndExpiry() (gas: 2521705)\n[PASS] test_TreasuryCapIgnoresDepositsWithinEpoch() (gas: 2669294)\n[PASS] test_TreasuryDelayAndAnyoneExecutes() (gas: 2600089)\n[PASS] test_TreasuryDestinationAllowlist() (gas: 2136658)\n[PASS] test_TreasuryEth() (gas: 2409792)\n[PASS] test_TreasuryManagerChangeIsDelayed() (gas: 2182109)\n[PASS] test_TreasuryOnlyManagerQueues() (gas: 2061089)\n[PASS] test_TreasuryRateCappedAt25Percent() (gas: 2117378)\n[PASS] test_TreasuryRulesOnlyTighten() (gas: 2466036)\n[PASS] test_TreasuryTighteningHoldsQueued() (gas: 2777026)\n[PASS] test_TreasuryWeeklyCap() (gas: 2734436)\n[PASS] test_VestingBeneficiaryCancel() (gas: 1388662)\n[PASS] test_VestingBeneficiaryChangeIsDelayed() (gas: 1519757)\n[PASS] test_VestingCliffThenLinear() (gas: 1600659)\n[PASS] test_VestingExtendAfterReleaseNeverUnderflows() (gas: 1416029)\n[PASS] test_VestingFeeOnTransferToken() (gas: 2072283)\n[PASS] test_VestingOnlyExtends() (gas: 1381714)\n[PASS] test_VestingRecoverOtherTokensOnly() (gas: 1436467)\n[PASS] test_VestingRejectsFasterThan25PercentAWeek() (gas: 1430192)\n[PASS] test_VestingTopUpFollowsSchedule() (gas: 1335236)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 41.19ms (117.82ms CPU time)\n\nRan 3 test suites in 42.10ms (82.66ms CPU time): 62 tests passed, 0 failed, 0 skipped (62 total tests)\n","passed":true},{"durationMs":32,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"TreasuryVault.acceptManager()\",\"TreasuryVault.cancel(uint256)\",\"TreasuryVault.cancelManager()\",\"TreasuryVault.decreaseRate(uint16)\",\"TreasuryVault.deposit(address,uint256)\",\"TreasuryVault.execute(uint256)\",\"TreasuryVault.increaseDelay(uint64)\",\"TreasuryVault.proposeManager(address)\",\"TreasuryVault.queue(address,address,uint256)\",\"TreasuryVault.receive()\",\"TreasuryVault.removeDestination(address)\",\"TreasuryVault.restrictDestinations(address[])\",\"VaultFactory.createLaunchVesting(address,uint64,uint64,uint64)\",\"VaultFactory.createTreasury(address,address,uint64,uint16,address[])\",\"VaultFactory.createVesting(address,address,uint256,uint64,uint64,uint64)\",\"VaultFactory.payFor(uint256,address)\",\"VestingVault.acceptBeneficiary()\",\"VestingVault.cancelBeneficiary()\",\"VestingVault.extendDuration(uint64)\",\"VestingVault.proposeBeneficiary(address)\",\"VestingVault.recover(address)\",\"VestingVault.release(address)\",\"VestingVault.sync(address)\"],\"files\":{\".gitignore\":6,\"ADAPTATION.md\":76,\"README.md\":115,\"SWARM_BRIEF.md\":131,\"foundry.toml\":23,\"launch.json\":13,\"src/TreasuryVault.sol\":354,\"src/VaultFactory.sol\":165,\"src/VestingVault.sol\":224,\"test/Audit.t.sol\":553,\"test/Fork.t.sol\":37,\"test/Vault.t.sol\":615},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"df054469ae9ace20ed1cd293c33ddd8aa06f325f4a3815d432b72e0179fc6a4d","verifiedTreeHash":"49f15a4f6c2473916522b720fc7650dba123a938","verifierVersion":"0.1.0+a6ee6548"},{"checks":[{"durationMs":2183,"exitCode":0,"name":"build","output":"Compiling 38 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.10s\nCompiler run successful!\n","passed":true},{"durationMs":125,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Fork.t.sol:ForkTest\n[PASS] test_RealImdFeeAndVesting() (gas: 3689)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 275.72µs (68.69µs CPU time)\n\nRan 23 tests for test/Audit.t.sol:AuditTest\n[PASS] testFuzz_LateArrivalNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64,uint64) (runs: 512, μ: 1405884, ~: 1410505)\n[PASS] testFuzz_VestingNeverMoreThan25PercentInFirstWeek(uint64,uint64,uint64) (runs: 512, μ: 164173, ~: 157750)\n[PASS] test_ArrivalInsideTheCliffKeepsTheSchedule() (gas: 1402921)\n[PASS] test_BoundVaultHasNoArrivalClock() (gas: 1378328)\n[PASS] test_EachTokenInAnOpenVaultHasItsOwnClock() (gas: 1535004)\n[PASS] test_ExtendDurationCannotOverflowTheSchedule() (gas: 1345050)\n[PASS] test_FactoryRecordsTheCreatorOfEveryVault() (gas: 4478684)\n[PASS] test_FirstReleaseOfANewTokenOnlyStartsItsClock() (gas: 1482328)\n[PASS] test_IncreaseDelayNeverRevivesAnExpiredWithdrawal() (gas: 2429868)\n[PASS] test_IncreaseDelayPastTheExpiryClosesAQueuedWithdrawal() (gas: 2675690)\n[PASS] test_LateArrivalVestsFromArrival() (gas: 1636726)\n[PASS] test_OpenVaultRecoverAlwaysRefuses() (gas: 1304145)\n[PASS] test_PendingManagerFollowsTheCurrentDelay() (gas: 2205226)\n[PASS] test_RecoverRefusesASecondEntryPointToTheVestedToken() (gas: 2442231)\n[PASS] test_SyncIsIdempotentAndNeedsABalance() (gas: 1459456)\n[PASS] test_TreasuryCapIsSharedByTwoEntryPointsToOneBalance() (gas: 4153927)\n[PASS] test_TreasuryCapThroughLegacyAddressFirstIsSharedToo() (gas: 3602827)\n[PASS] test_TreasuryConstructorEnforcesMinDelay() (gas: 8689)\n[PASS] test_TreasuryEpochBoundaryBoundIsRatePlusRateOfRemainder() (gas: 2855323)\n[PASS] test_TreasuryTrackedAssetsNeverBlockOtherWithdrawals() (gas: 3379736)\n[PASS] test_VestingCreatedLogsWhatTheVaultReceived() (gas: 1971615)\n[PASS] test_VestingRejectsOverflowingSchedule() (gas: 1683748)\n[PASS] test_VestingRejectsStartInThePast() (gas: 2895533)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 35.99ms (56.40ms CPU time)\n\nRan 38 tests for test/Vault.t.sol:VaultTest\n[PASS] testFuzz_TreasuryNeverExceedsWeeklyCap(uint256,uint16,uint256[5]) (runs: 512, μ: 3256169, ~: 3270472)\n[PASS] testFuzz_VestingNeverOverpays(uint256,uint64,uint64,uint32[4]) (runs: 512, μ: 1595853, ~: 1563759)\n[PASS] testFuzz_VestingNeverUnlocksMoreThan25PercentInAWeek(uint256,uint64,uint64,uint64) (runs: 512, μ: 1315226, ~: 1318946)\n[PASS] test_BoundVestingRejectsOtherTokens() (gas: 1377547)\n[PASS] test_CreateVestingNeedsAToken() (gas: 36022)\n[PASS] test_EachLaunchVaultPaysItsOwnFee() (gas: 4454024)\n[PASS] test_FactoryBurnsFeeOncePerProjectAndRegisters() (gas: 3289130)\n[PASS] test_FactoryChargesEachNewProject() (gas: 3816278)\n[PASS] test_FactoryNeedsRadarWalletWhenCharging() (gas: 6067)\n[PASS] test_FactoryRejectsShortDelay() (gas: 36613)\n[PASS] test_FactoryRevertsWithoutImd() (gas: 1915886)\n[PASS] test_FeeGoesToRadarNotBurned() (gas: 1301718)\n[PASS] test_FreeFactoryOnChainsWithoutImd() (gas: 1354185)\n[PASS] test_LaunchTreasuryReceivesRemainder() (gas: 3072408)\n[PASS] test_LaunchVestingReceivesRemainderAndVests() (gas: 2106028)\n[PASS] test_LaunchVestingVestsEveryTokenAndHasNoRecover() (gas: 2255256)\n[PASS] test_PayForRobinhoodProject() (gas: 149438)\n[PASS] test_RobinhoodLaunchVaultPaidOnEthereum() (gas: 1275945)\n[PASS] test_TreasuryCancelAndExpiry() (gas: 2521705)\n[PASS] test_TreasuryCapIgnoresDepositsWithinEpoch() (gas: 2669294)\n[PASS] test_TreasuryDelayAndAnyoneExecutes() (gas: 2600089)\n[PASS] test_TreasuryDestinationAllowlist() (gas: 2136658)\n[PASS] test_TreasuryEth() (gas: 2409792)\n[PASS] test_TreasuryManagerChangeIsDelayed() (gas: 2182109)\n[PASS] test_TreasuryOnlyManagerQueues() (gas: 2061089)\n[PASS] test_TreasuryRateCappedAt25Percent() (gas: 2117378)\n[PASS] test_TreasuryRulesOnlyTighten() (gas: 2466036)\n[PASS] test_TreasuryTighteningHoldsQueued() (gas: 2777026)\n[PASS] test_TreasuryWeeklyCap() (gas: 2734436)\n[PASS] test_VestingBeneficiaryCancel() (gas: 1388662)\n[PASS] test_VestingBeneficiaryChangeIsDelayed() (gas: 1519757)\n[PASS] test_VestingCliffThenLinear() (gas: 1600659)\n[PASS] test_VestingExtendAfterReleaseNeverUnderflows() (gas: 1416029)\n[PASS] test_VestingFeeOnTransferToken() (gas: 2072283)\n[PASS] test_VestingOnlyExtends() (gas: 1381714)\n[PASS] test_VestingRecoverOtherTokensOnly() (gas: 1436467)\n[PASS] test_VestingRejectsFasterThan25PercentAWeek() (gas: 1430192)\n[PASS] test_VestingTopUpFollowsSchedule() (gas: 1335236)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 44.40ms (125.33ms CPU time)\n\nRan 3 test suites in 45.18ms (80.67ms CPU time): 62 tests passed, 0 failed, 0 skipped (62 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"TreasuryVault.acceptManager()\",\"TreasuryVault.cancel(uint256)\",\"TreasuryVault.cancelManager()\",\"TreasuryVault.decreaseRate(uint16)\",\"TreasuryVault.deposit(address,uint256)\",\"TreasuryVault.execute(uint256)\",\"TreasuryVault.increaseDelay(uint64)\",\"TreasuryVault.proposeManager(address)\",\"TreasuryVault.queue(address,address,uint256)\",\"TreasuryVault.receive()\",\"TreasuryVault.removeDestination(address)\",\"TreasuryVault.restrictDestinations(address[])\",\"VaultFactory.createLaunchVesting(address,uint64,uint64,uint64)\",\"VaultFactory.createTreasury(address,address,uint64,uint16,address[])\",\"VaultFactory.createVesting(address,address,uint256,uint64,uint64,uint64)\",\"VaultFactory.payFor(uint256,address)\",\"VestingVault.acceptBeneficiary()\",\"VestingVault.cancelBeneficiary()\",\"VestingVault.extendDuration(uint64)\",\"VestingVault.proposeBeneficiary(address)\",\"VestingVault.recover(address)\",\"VestingVault.release(address)\",\"VestingVault.sync(address)\"],\"files\":{\".gitignore\":6,\"ADAPTATION.md\":76,\"README.md\":115,\"SWARM_BRIEF.md\":131,\"foundry.toml\":23,\"src/TreasuryVault.sol\":354,\"src/VaultFactory.sol\":165,\"src/VestingVault.sol\":224,\"test/Audit.t.sol\":553,\"test/Fork.t.sol\":37,\"test/Vault.t.sol\":615},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":872,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/VestingVault.sol:217: VestingVault._sync(address) (src/VestingVault.sol#217-223) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/TreasuryVault.sol:284: TreasuryVault.pendingManagerEta() (src/TreasuryVault.sol#284-287) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/VestingVault.sol:149: VestingVault.release(address) (src/VestingVault.sol#149-161) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/VestingVault.sol:120: VestingVault.vestedAmount(address,uint64) (src/VestingVault.sol#120-127) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/VestingVault.sol:111: VestingVault.unlockStart(address) (src/VestingVault.sol#111-117) uses a dangerous strict equality:\n[low/medium] missing-zero-check at src/TreasuryVault.sol:120: TreasuryVault.constructor(address,address,uint64,uint16,address[]).projectToken_ (src/TreasuryVault.sol#120) lacks a zero-check on :\n[low/medium] calls-loop at src/TreasuryVault.sol:334: TreasuryVault._tryBalance(address) (src/TreasuryVault.sol#334-339) has external calls inside a loop: (ok,data) = asset.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector,address(this))) (src/TreasuryVault.sol#335-336)\n[low/medium] timestamp at src/VestingVault.sol:120: VestingVault.vestedAmount(address,uint64) (src/VestingVault.sol#120-127) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:149: VestingVault.release(address) (src/VestingVault.sol#149-161) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:343: TreasuryVault._charge(TreasuryVault.Window,uint256,uint256) (src/TreasuryVault.sol#343-353) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:189: VestingVault.acceptBeneficiary() (src/VestingVault.sol#189-198) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:217: VestingVault._sync(address) (src/VestingVault.sol#217-223) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:284: TreasuryVault.pendingManagerEta() (src/TreasuryVault.sol#284-287) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:139: VestingVault.releasable(address) (src/VestingVault.sol#139-144) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:111: VestingVault.unlockStart(address) (src/VestingVault.sol#111-117) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:269: TreasuryVault.acceptManager() (src/TreasuryVault.sol#269-279) uses timestamp for comparisons\n[low/medium] timestamp at src/VestingVault.sol:75: VestingVault.constructor(IERC20,address,uint64,uint64,uint64,uint64) (src/VestingVault.sol#75-95) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:179: TreasuryVault.execute(uint256) (src/TreasuryVault.sol#179-211) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:303: TreasuryVault.availableThisWeek(address) (src/TreasuryVault.sol#303-307) uses timestamp for comparisons\n[low/medium] timestamp at src/TreasuryVault.sol:323: TreasuryVault._get(uint256) (src/TreasuryVault.sol#323-326) uses timestamp for comparisons","passed":true},{"durationMs":369,"exitCode":0,"name":"aderyn","output":"[low] costly-loop at src/TreasuryVault.sol:200: Costly operations inside loop (3 places)\n[low] large-numeric-literal at src/TreasuryVault.sol:35: Large Numeric Literal\n[low] require-revert-in-loop at src/TreasuryVault.sol:200: Loop Contains `require`/`revert` (2 places)\n[low] unchecked-return at src/VestingVault.sol:135: Unchecked Return\n[low] uninitialized-local-variable at src/TreasuryVault.sol:200: Uninitialized Local Variable (2 places)","passed":true},{"durationMs":1402,"exitCode":0,"name":"proof c693ed06fe5a","output":"Compiling 36 files with Solc 0.8.26\nSolc 0.8.26 finished in 870.33ms\nCompiler run successful!\n\nRan 1 test for test/imd-proof-02feeac5/Proof_c693ed06fe5a.t.sol:TreasuryDoubleEntryTest\n[PASS] test_TreasuryWeeklyCapHoldsAcrossTwoEntryPointsToOneBalance() (gas: 4249551)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 933.93µs (502.72µs CPU time)\n\nRan 1 test suite in 3.17ms (933.93µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f7257978f20c9ee2406682158ac6573bc26ddd5cfcc3776f8aa26ef82e317783","verifiedTreeHash":"73420fe53444732445740ad64a038b86abf51885","verifierVersion":"0.1.0+a6ee6548"}]}