{"assessments":[],"deployments":[{"attestationHash":"42aa007c2e93450d61a4563f0ecf080c6df01ffcdd4226a149c652b4a9644723","chainId":1,"contracts":[{"address":"0x65a783cc6725a02ce349dc4d72577994df1760cc","blockNumber":26154170,"name":"TreasuryFeeHook","role":"hook","txHash":"0x545df1adb27c4a2ad6de57dd3d4d28005306f1471c0002381d5518fbc1c6dd7d"},{"address":"0xfd1c234972768c23bb21d655966e0b122dd67a2c","blockNumber":26154170,"name":"PrismRiotToken","role":"token","txHash":"0x545df1adb27c4a2ad6de57dd3d4d28005306f1471c0002381d5518fbc1c6dd7d"},{"address":"0xd5873749535e5e558e3c2181ff8e9f43e4fc2927","blockNumber":26154170,"name":"MerkleDistributor","role":"distributor","txHash":"0x545df1adb27c4a2ad6de57dd3d4d28005306f1471c0002381d5518fbc1c6dd7d"}],"id":"f1ec36cc-db4f-4f19-b036-2fd578a0e923","manifestHash":"29cf49364cfd08d8cc35aff7b80afe4b38c8ed5a56e11d84ff20dff274aee680","status":"live"}],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"9bb0ae93-2a65-44d2-900e-e06b9ad5e794","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"fb2523231cf87ff72d7adaddaf909a6784d79777637ef1e26272ad170dc64295","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"1791255bca5ccddb5a8665fec05a867abfe95deed1b88efc6d637082de619ce0","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"506fdd77b0c73df21bc50103330aad38ceb38dcd1ab4faa3366599350e5eb49a","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"9703a836b3e4b9c418986ca1be0188efb6b8bb14a1d895ba64a2adf572b14227","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"dfe6331242c8579205569ae22b0d934368ca9deaed19b80642f4631fa27dfa34","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"c2a7eeee2e622e72c32d7271cb3e2e7cff4e3fbda3143e1ebe9229ca381b45a5","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6e3e328aa196f6965a2821b098b101e9ff09eef55cdaafaec1d6ea18007b96f6","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6e3e328aa196f6965a2821b098b101e9ff09eef55cdaafaec1d6ea18007b96f6","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"e1517165d047a4a5138ca1c29174f09722f1080df25e017b64b7fb0c157cf0fb","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"2238a04c42e12f01006a365c64a9af6322ab54c26b1d1a03137778ce0767e84c","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build, test, independently review and deploy PRISM RIOT contracts on Ethereum mainnet. Token name: Prism Riot. Symbol: PRIO. Launch kind: univ4_hook. Pair: ETH/PRIO. Use the platform's standard fixed-supply token with plain transfers. The paying wallet owns this new project. This order delivers contracts and server operator code only.\n\nPrior failed work: https://explorer.imdnotdead.fun/jobs/bbf45a6b-c2ae-40b2-a5e7-41d2f3e8c633 . Source: https://github.com/identity-md-launches/launch-1012-workflow-contract-stage-context . Read the source and audit findings, repair and retest the complete contract project. Check existing on-chain receipts before creating any duplicate deployment. Do not mark the job complete with source alone: deliver verified live token, application, treasury, hook and pool addresses and deployment receipts after successful independent review.\n\nTreasuryFeeHook: immutable extra 0.5% on the ETH leg, excluding fees, of buys AND sells in its ETH/PRIO pool. Send all of it to FeeTreasury. Platform fees remain separate. Ordinary transfers and other pools have no extra fee. Prove compatibility with the actual production factory/router, mined hook permission flags, secure configuration before initialization, treasury binding and hooked PoolKey. Never silently remove the fee. Test both directions, exact-input/output and wei-level rounding; document quote tick limits.\n\nFeeTreasury: use earned ETH fees only. Cap gas/operating reserve replenishment at 10% per allocation and document a finite reserve target. Allocate remaining ETH: 30% to buying IMD for agent work, 30% to buying PRIO rewards, 40% to owner. Split purchased PRIO equally between staking and game rewards. Enforce spending limits, slippage and reserve accounting. PRIO purchases must work independently of IMD request configuration. No owner operating advances; no income means no paid operations. Document fee-funded gas bootstrap and permissionless execution where possible.\n\nStakingVault: stake(), withdraw(), claim(). Distribute only funded PRIO rewards pro rata by stake and time using a documented schedule. Keep principal withdrawable and isolated from games, operating costs and rewards. Stop unfunded accrual. No fixed APY or extra minting.\n\nArena: Vault Raid, faction duels and cooperative boss challenges using choices and fixed published scoring. Each paid entry locks 100 PRIO escrow plus a 2 PRIO fee. Correct: return 100 PRIO escrow; wrong: return 90; missed reveal: return 80. Penalties never stack. Freeze rules, deadlines, maximum loss and funded prizes before entry. Use commit-reveal, bounded settlement and pull claims. Provide round-indexed entry/reveal/claim/refund functions so old rounds remain claimable. Never debit unapproved balances or staking principal. Cancel unresolved rounds 72 hours after their result deadline and refund all entry fees and escrow. Document game-pool allocations, ties, no-winner handling and multi-wallet risks.\n\nOracleAdapter: verify current IMD EIP-712 panel attestations, trusted signer/quorum, consumer/domain, pinned question, expiry and replay; store the result and evidence reference. Specify clock tolerance consistently with commit/reveal boundaries. Configure callbacks before paid requests. Missing or invalid evidence cannot settle a round.\n\nProvide a budget-limited server operator using supported IMD oracle.request and job.open flows: capabilities/check/quote/payment, polling, retries and result delivery. Keys stay server-side. Agent outputs are proposed challenges/artwork, never authority to replace active rules or spend player funds. Disable paid operations until correctly configured and funded by fees; allow manual result relay and claims.\n\nOnly owner configures future rounds and limited operator budgets; use two-step ownership without renunciation. Active rules, reserved payouts and the 0.5% fee cannot change. Keep staking principal, escrow, refunds and funded rewards separate. Fix every prior audit finding, including reserve/withdrawal caps and old-round claims. Deliver GitHub source, ABIs, launch manifest, operator instructions, meaningful Foundry regression/invariant tests and finding-to-fix evidence. Obtain fresh independent review before deployment. No guaranteed profit.","parentJobId":null,"planHash":"33e93b477b962ed5f94ef17bb506b83a83265e6c29835da1d067496ff4fc7d4f","previousHash":"84e72588ac64d0e1743ee787506bcdf71b3309095599390c97b7b48b90d2137f","projectId":"9bb0ae93-2a65-44d2-900e-e06b9ad5e794","publication":{"commit":"34e992ab84195173cd35219f895309b051b1944d","deliveredAt":"2026-10-09T10:10:41.930Z","repoUrl":"https://github.com/identity-md-launches/launch-1153-build-test-independently-review"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":null,"snapshotHash":"3beda4a16ddf30b56ee88264d0064a37edd9ca7493cf0552bd86855adb4f58b6","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"c86efe85d4b53e863bc6defe4ca0846fac8a1f5019763c2ac9c7161aef6f2d05","device":"e3a598aae0640402","findings":[],"hash":"149eef22732c75304416dbed2311b69a0a4875d1c2597f218aef64ebbe77c3b7","nodeId":"219f6226-b512-4d87-8c2b-2421bf0ace9e","outcome":"completed","summary":"The PRISM RIOT contract project is built, tested, documented and committed on `main` as commit `9a9f4f4`. A fresh offline clone builds and passes all checks.\n\n**What was delivered**\n\n- `src/PrismRiotToken.sol`: the standard fixed-supply token. \"Prism Riot\" / \"PRIO\", 1,000,000,000 tokens at 18 decimals minted to the deployer, no owner or extra functions.\n- `src/TreasuryFeeHook.sol`: an immutable 0.5% fee on the ETH leg of buys and sells in its one ETH/PRIO pool. It uses beforeSwap when ETH is the specified side and afterSwap when it is not, so all four swap shapes pay the fee in ETH. The fee goes to FeeTreasury in the same swap when the PoolManager holds ETH, and is minted as an ERC-6909 claim otherwise, redeemable by anyone. It refuses any pool but ETH/PRIO from the factory, validates its mined flags in the constructor, and never drops a fee.\n- `src/FeeTreasury.sol`: accepts ETH from the hook only, so no owner advances are possible. Allocation caps the reserve at 10% per allocation and at a finite target, then splits 30% IMD, 30% PRIO, 40% owner. Executor-gated, slippage-checked, budget-bounded swaps through the PoolManager. Purchased PRIO is split equally between staking and the arena.\n- `src/StakingVault.sol`: stake, withdraw, claim. Rewards stream only from funded notifications over a documented 30-day schedule and stop when the period ends. Principal is isolated.\n- `src/Arena.sol`: three modes on commit-reveal with fixed published scoring (100/90/80 returned, 2 PRIO fee, max loss 22), frozen rounds, O(1) settlement, round-indexed pull claims, and 72-hour cancellation with full refunds.\n- `src/OracleAdapter.sol`: the protocol's EIP-712 consumer with pinned questions, chain, panel and quorum minimums, issued-at boundary tied to the commit deadline, replay protection, intake callback and manual relay, and budget-limited paid requests that stay disabled until configured.\n- `operator/operator.py`: a stdlib-only server operator with daily IMD, gas and request caps, retries, polling, relay and proposal flows. Keys stay server-side.\n- Docs: README (assumptions, deployment parameters with manifest placeholders, after-launch settings, fee definition, quote limits), `docs/OPERATOR.md`, `docs/REVIEW.md` with the finding-to-fix table and self-review, and ABIs under `docs/abi/`.\n\n**Verification**\n\n| Check | Result |\n| --- | --- |\n| forge build, forge test, forge fmt --check | pass, 54 tests including fuzz, invariant and deploy rehearsal |\n| IMD protected floor tests (token and hook) run locally against the built bytecode | 11 of 11 pass |\n| Operator unit tests | 5 of 5 pass |\n| Fresh clone built and tested with no network | pass |\n\n**What this submission cannot contain**\n\nNo contract was deployed. This assignment controls no funded wallet or keys, and the launch's manifest step deploys from the bytecode after independent review. The prior job never broadcast a transaction, so there is no on-chain deployment to duplicate. Live addresses and receipts therefore come from the launch step, not from this source delivery. The prior repository on GitHub holds only the empty workspace commit, so the prior audit text was not retrievable; every finding listed in the brief is mapped to its fix and test in `docs/REVIEW.md`.\n\n**Open items for the owner**\n\n- Set the IMD trading venue with `FeeTreasury.setImdPool` once known. PRIO purchases do not depend on it.\n- Complete the after-launch wiring listed in the README before enabling paid operations.\n- Obtain the independent adversarial review the brief requires before release.","treeHash":"a605f168a067921e6a3981dfad4ec930bd04f921","usage":{"cachedInputTokens":5130790,"inputTokens":1026,"model":"claude-fable-5-1","outputTokens":117302,"runtime":"claude","turns":60,"wallClockMs":1514630}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2a9662a76cb5f51d","findings":[{"citation":"resolved","description":"In the two beforeSwap paths (buy with exact ETH input, sell with exact ETH output) the fee is computed from `params.amountSpecified`, i.e. the amount the swapper asked for, and collected immediately with `_collect`. The pool then swaps `amountToSwap = amountSpecified +/- fee` only as far as `params.sqrtPriceLimitX96` (or the liquidity it has) allows; when the swap stops early (a partial fill, which v4 does not revert on) the ETH the pool actually exchanged is smaller than `leg`, but the fee already taken is unchanged. The swapper therefore pays 0.5% of the *requested* input/output, which for a tight limit or an exhausted range is many times 0.5% of the real ETH leg (in the proof ~98x: 0.0498 ETH fee on a ~0.1 ETH leg; the sell-exact-output case keeps 0.0503 ETH while paying out ~0.1 ETH). The afterSwap paths are correct because they read the actual `delta.amount0()`. This contradicts the brief ('0.5% on the ETH leg') and the README section 'Quote tick limits', which states 'the ETH leg used for the fee is whatever the pool exchanged within that limit, so a partially filled swap pays 0.5% of the partial leg'. Routers that pass an explicit `sqrtPriceLimitX96` (aggregators, custom integrations) and any swap larger than the launch pool's seeded range (the factory seeds PRIO only, in a bounded range) hit this. Fix options: stash `leg` in transient storage in beforeSwap and in afterSwap compare it with the actual `delta.amount0()`; either revert on a partial fill (`PartialFill()`), or compute the correct fee from the actual leg and return the excess to the swapper via the afterSwap unspecified delta is not possible (ETH is the specified currency), so reverting or documenting and quoting the real behaviour are the honest choices. Update the README either way; the current text misdescribes the fee.","line":238,"path":"src/TreasuryFeeHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {PoolId} from \"v4-core/src/types/PoolId.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\n\n/// @notice The hook's `beforeSwap` paths (buy with exact ETH in, sell with exact ETH out) compute the\n/// 0.5% fee from `params.amountSpecified`, the amount the swapper *asked* for, not from the ETH the\n/// pool actually exchanged. A swap that stops at its `sqrtPriceLimitX96` (a partial fill) therefore\n/// pays 0.5% of the requested amount, which can be many times 0.5% of the real ETH leg. The README\n/// (\"Quote tick limits\") promises the opposite: \"a partially filled swap pays 0.5% of the partial leg\".\ncontract PartialFillFeeTest is Test {\n    using StateLibrary for IPoolManager;\n\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint24 constant POOL_FEE = 12500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode,\n            abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        treasury.setPrio(address(token));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: POOL_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        // Full-range liquidity on both sides.\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        vm.deal(factory, 1_000_000 ether);\n        lpRouter.modifyLiquidity{value: 100_000 ether}(\n            key,\n            ModifyLiquidityParams(\n                TickMath.minUsableTick(TICK_SPACING), TickMath.maxUsableTick(TICK_SPACING), int256(10_000 ether), 0\n            ),\n            \"\"\n        );\n        vm.stopPrank();\n\n        vm.deal(trader, 1_000 ether);\n        vm.prank(factory);\n        token.transfer(trader, 10_000_000 ether);\n        vm.prank(trader);\n        token.approve(address(swapRouter), type(uint256).max);\n    }\n\n    function swap(bool zeroForOne, int256 amountSpecified, uint160 limit, uint256 value)\n        internal\n        returns (BalanceDelta d)\n    {\n        vm.prank(trader);\n        d = swapRouter.swap{value: value}(\n            key, SwapParams(zeroForOne, amountSpecified, limit), PoolSwapTest.TestSettings(false, false), \"\"\n        );\n    }\n\n    /// @dev Buy with 10 ETH exact input but a price limit 0.001% below the current price: the pool\n    /// exchanges only ~0.1 ETH, yet the hook keeps ceil(10e18 * 50 / 10050) ~ 0.0498 ETH, about 50% of\n    /// the real leg instead of 0.5%.\n    function test_buyExactInput_partialFill_feeIsNotHalfPercentOfTheLeg() public {\n        // Sanity: an ordinary full fill works on this code.\n        swap(true, -1 ether, TickMath.MIN_SQRT_PRICE + 1, 1 ether);\n        uint256 chargedBefore = hook.totalFeeCharged();\n\n        (uint160 sqrtPrice,,,) = IPoolManager(address(manager)).getSlot0(key.toId());\n        uint160 limit = sqrtPrice - sqrtPrice / 100_000;\n        uint256 grossIn = 10 ether;\n        uint256 ethBefore = trader.balance;\n\n        try this.doSwap(true, -int256(grossIn), limit, grossIn) returns (BalanceDelta d) {\n            uint256 paid = ethBefore - trader.balance;\n            assertEq(paid, uint256(uint128(-d.amount0())), \"router charges the swapper the whole delta\");\n            uint256 fee = hook.totalFeeCharged() - chargedBefore;\n            uint256 leg = paid - fee; // the ETH the pool itself exchanged\n            assertLt(leg, grossIn / 10, \"the fill was partial\");\n            // What the hook documents: 0.5% of the ETH leg the pool exchanged, rounded up.\n            assertEq(fee, hook.feeOnLeg(leg), \"fee must be 0.5% of the partial ETH leg, not of the requested input\");\n        } catch {\n            // A hook that refuses partial fills instead of over-charging them is an acceptable repair.\n        }\n    }\n\n    /// @dev Sell for 10 ETH exact output with a price limit just above the current price: the pool can\n    /// pay out only part of it, the swapper receives far less than 10 ETH, and the hook still keeps\n    /// ceil(10e18 * 50 / 9950) ~ 0.0503 ETH computed on the full 10 ETH.\n    function test_sellExactOutput_partialFill_feeIsNotHalfPercentOfTheLeg() public {\n        swap(false, int256(1 ether), TickMath.MAX_SQRT_PRICE - 1, 0);\n        uint256 chargedBefore = hook.totalFeeCharged();\n\n        (uint160 sqrtPrice,,,) = IPoolManager(address(manager)).getSlot0(key.toId());\n        uint160 limit = sqrtPrice + sqrtPrice / 100_000;\n        uint256 wantOut = 10 ether;\n        uint256 ethBefore = trader.balance;\n\n        try this.doSwap(false, int256(wantOut), limit, 0) returns (BalanceDelta d) {\n            uint256 received = trader.balance - ethBefore;\n            assertEq(received, uint256(uint128(d.amount0())));\n            uint256 fee = hook.totalFeeCharged() - chargedBefore;\n            uint256 leg = received + fee; // the ETH the pool itself paid out\n            assertLt(received, wantOut / 10, \"the fill was partial\");\n            assertEq(fee, hook.feeOnLeg(leg), \"fee must be 0.5% of the partial ETH leg, not of the requested output\");\n        } catch {\n            // Refusing the partial fill is an acceptable repair.\n        }\n    }\n\n    function doSwap(bool zeroForOne, int256 amountSpecified, uint160 limit, uint256 value)\n        external\n        returns (BalanceDelta)\n    {\n        require(msg.sender == address(this));\n        return swap(zeroForOne, amountSpecified, limit, value);\n    }\n}","reproduction":"Pool at 1:1 with full-range liquidity (L = 1e22). Trader calls PoolSwapTest.swap(key, {zeroForOne: true, amountSpecified: -10e18, sqrtPriceLimitX96: currentSqrtPrice - currentSqrtPrice/100000}) with 10 ETH. Expected (per README/brief): fee = ceil(0.005 * ethLeg) where ethLeg is the ETH the pool exchanged (~0.1 ETH), i.e. ~5.06e14 wei. Actual: hook.totalFeeCharged() increases by 49751243781094528 (= ceil(10e18*50/10050)), the swapper pays leg + 0.0498 ETH for ~0.1 ETH of pool volume. Same for sell exact-output 10 ETH with limit currentSqrtPrice + currentSqrtPrice/100000: swapper receives ~0.1 ETH and the hook keeps 50251256281407036 wei. See test/scratch/PartialFillFee.t.sol (fails on this code).","severity":"medium","snippet":"        if (exactInput) {\n            // Buy with exact ETH in: the pool gets the input minus the fee.\n            (fee, leg) = quoteBuyExactInput(uint256(-params.amountSpecified));\n        } else {\n            // Sell with exact ETH out: the pool pays out the requested ETH plus the fee.\n            (fee, leg) = quoteSellExactOutput(uint256(params.amountSpecified));\n        }\n        _collect(fee, params.zeroForOne, exactInput, leg);","title":"TreasuryFeeHook.beforeSwap charges 0.5% of the requested amount, not of the ETH leg the pool exchanged: a partially filled swap is over-charged (README promises the opposite)"},{"citation":"resolved","description":"`buyPrio` (and identically `buyImd`) executes `prioBudget -= ethIn` and then swaps with `amountSpecified: -int256(ethIn)` and the extreme price limit. In `unlockCallback` the treasury pays `owed = -delta.amount0()` which is only checked with `if (owed > ethIn) revert`; the case `owed < ethIn` (the pool ran out of liquidity in range and the swap stopped at MIN_SQRT_PRICE+1) is accepted. The `ethIn - owed` ETH that never left the contract is no longer in `prioBudget`, is not added back to `unallocated`, and the treasury has no sweep: `withdrawOwner` is capped by `ownerBudget`, `withdrawReserve` by `reserve`, `allocate` only moves `unallocated`. The invariant `address(this).balance == reserve + imdBudget + prioBudget + ownerBudget + unallocated` (which holds on every other path, including the hook's fee flowing back through `receive`) breaks and fee income is lost. This is reachable on the launch pool, whose buy-side liquidity is a bounded PRIO-only range that a large buy exhausts; the executor's `minPrioOut` does not prevent it because a partial fill can still exceed a loose minimum. Fix: in `unlockCallback` return `owed` alongside `out` and in `buyPrio`/`buyImd` credit `prioBudget += ethIn - owed` (or revert when `owed != ethIn`).","line":270,"path":"src/FeeTreasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\nimport {Arena} from \"src/Arena.sol\";\n\n/// @notice `FeeTreasury.buyPrio` debits `prioBudget` by the full `ethIn` before the swap, but\n/// `unlockCallback` settles only `owed`, the ETH the pool actually took. When the hooked pool cannot\n/// absorb `ethIn` (liquidity exhausted, the swap stops at MIN_SQRT_PRICE + 1), `owed < ethIn` and the\n/// difference stays in the treasury without belonging to any budget line: no function can ever move it.\ncontract TreasuryStrandedTest is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint24 constant POOL_FEE = 12500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n    address executor = makeAddr(\"executor\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    StakingVault vault;\n    Arena arena;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode,\n            abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vault = new StakingVault(owner, address(token));\n        arena = new Arena(owner, address(token));\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        treasury.setPrio(address(token));\n        treasury.setSinks(address(vault), address(arena), makeAddr(\"adapter\"));\n        treasury.setExecutor(executor);\n        vault.setRewardFunder(address(treasury));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: POOL_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        // A launch-style seed: PRIO only, in one bounded range just below the opening price.\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(-600, 0, int256(2e20), 0), \"\");\n        vm.stopPrank();\n\n        vm.deal(trader, 1_000 ether);\n        vm.prank(factory);\n        token.transfer(trader, 10_000_000 ether);\n        vm.prank(trader);\n        token.approve(address(swapRouter), type(uint256).max);\n    }\n\n    function swap(bool zeroForOne, int256 amountSpecified, uint256 value) internal returns (BalanceDelta d) {\n        vm.prank(trader);\n        d = swapRouter.swap{value: value}(\n            key,\n            SwapParams(\n                zeroForOne, amountSpecified, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1\n            ),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n    }\n\n    function accounted() internal view returns (uint256) {\n        return treasury.reserve() + treasury.imdBudget() + treasury.prioBudget() + treasury.ownerBudget()\n            + treasury.unallocated();\n    }\n\n    function test_buyPrio_partialFill_strandsEthOutsideEveryBudget() public {\n        // A buyer drains the seeded range: the pool absorbs ~6 ETH of the 100 requested, the hook earns\n        // its fee, and the treasury has income to allocate.\n        swap(true, -100 ether, 100 ether);\n        // The pool was PRIO-only, so the first fee was minted as a claim; redeem it now that the manager holds ETH.\n        if (hook.pendingClaims() > 0) hook.redeemClaims(hook.pendingClaims());\n        treasury.allocate();\n        uint256 budget = treasury.prioBudget();\n        assertGt(budget, 0.05 ether, \"there is a PRIO budget to spend\");\n        assertEq(address(treasury).balance, accounted(), \"every wei is on a budget line before the buy\");\n\n        // A small sell re-opens a little buy-side capacity: far less than the PRIO budget.\n        swap(false, -int256(0.02 ether), 0);\n\n        vm.prank(executor);\n        try treasury.buyPrio(budget, 1) returns (uint256 out) {\n            assertGt(out, 0);\n            assertEq(treasury.prioBudget(), 0, \"the whole budget line was debited\");\n            // Every wei the treasury holds must belong to a budget line (reserve, IMD, PRIO, owner or\n            // unallocated income). Here `ethIn - owed` belongs to none and can never be withdrawn or spent.\n            assertEq(address(treasury).balance, accounted(), \"unspent ETH of a partial fill is stranded\");\n        } catch {\n            // Refusing the partial fill (hook or treasury) is an acceptable repair: nothing is stranded.\n            assertEq(address(treasury).balance, accounted());\n        }\n    }\n}","reproduction":"Seed the hooked pool PRIO-only in ticks [-600, 0] with liquidity 2e20. (1) Trader buys 100 ETH exact-in: the range absorbs ~6 ETH, hook fee 0.4975 ETH minted as a claim; redeemClaims delivers it; allocate() gives prioBudget ~0.134 ETH. (2) Trader sells 0.02 PRIO so a little buy-side capacity (~0.02 ETH) exists again. (3) executor calls buyPrio(prioBudget, 1). Expected: either the full budget is spent or the unspent part stays on a budget line. Actual: prioBudget == 0, out > 0, and address(treasury).balance (476382776726149822) exceeds reserve+imdBudget+prioBudget+ownerBudget+unallocated (363957225267235285) by ~0.112 ETH that no function can move. See test/scratch/TreasuryStranded.t.sol (fails on this code).","severity":"medium","snippet":"        if (owed > ethIn) revert ExceedsBudget();\n        poolManager.settle{value: owed}();","title":"FeeTreasury.buyPrio/buyImd debit the full ethIn but settle only what the pool took: on a partial fill the difference is stranded outside every budget line forever"},{"citation":"resolved","description":"`_accept` only requires `issuedAt + ISSUED_AT_TOLERANCE >= notBefore` (the round's commit deadline). The tolerance is applied in the loosening direction, so an attestation issued at any time in [commitDeadline - 5 min, commitDeadline) is accepted, and `submitAttestation` is permissionless with no on-chain time gate (`block.timestamp` is never compared with `notBefore`). Once stored, `resultOf(roundId)` exposes `answer`; `Arena.enter` accepts commitments until `commitDeadline`, so anyone reading the result enters the winning choice `(answer % choiceCount) + 1`, reveals, and claims 100 PRIO plus the prize share. The Arena's own check in `settle` (`issuedAt + tolerance >= commitDeadline`) has the same slack and does not help. Who can trigger it: per the oracle-consumer reference the plane signs in the callback target's domain, so anyone may pay the Intake for the pinned body (public via `pinned(roundId).body`) with callback target = this adapter and relay the signed attestation; the honest operator can also do it by running `request-round` shortly before the deadline (operator.py has no timing guard). The brief's 'clock tolerance consistent with commit/reveal boundaries' needs the boundary enforced on the chain's own clock: in `_accept` (and in `request()`, where an early paid request is otherwise wasted on an `IssuedTooEarly` callback revert) require `block.timestamp >= p.notBefore`; keep the issuedAt check as defence in depth. Also document that the pinned question's block window must end after the commit deadline, since nothing on chain checks `a.fromBlock/toBlock`.","line":248,"path":"src/OracleAdapter.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {Arena, IRoundOracle} from \"src/Arena.sol\";\nimport {OracleAdapter} from \"src/OracleAdapter.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\n\n/// @notice `OracleAdapter._accept` stores a result whenever `issuedAt + 5 minutes >= notBefore`, and\n/// `submitAttestation` is permissionless with no on-chain time gate. An attestation issued inside the\n/// last five minutes of the commit window is therefore stored and readable through `resultOf` while\n/// `Arena.enter` is still open, so a late entrant commits the winning choice with certainty.\ncontract EarlyResultTest is Test {\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n    bytes32 constant QUESTION = keccak256(\"round question\");\n    uint64 constant T0 = 1_800_000_000;\n\n    PrismRiotToken token;\n    Arena arena;\n    OracleAdapter adapter;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    address mallory = makeAddr(\"mallory\");\n\n    function setUp() public {\n        vm.warp(T0);\n        token = new PrismRiotToken();\n        arena = new Arena(owner, address(token));\n        adapter = new OracleAdapter(owner, SIGNER);\n        vm.prank(owner);\n        arena.setOracle(IRoundOracle(address(adapter)));\n        token.transfer(alice, 1_000 ether);\n        token.transfer(mallory, 1_000 ether);\n        vm.prank(alice);\n        token.approve(address(arena), type(uint256).max);\n        vm.prank(mallory);\n        token.approve(address(arena), type(uint256).max);\n        token.approve(address(arena), type(uint256).max);\n        arena.fundPrizes(1_000 ether);\n    }\n\n    function test_resultStoredBeforeCommitDeadline_letsLateEntrantWin() public {\n        uint64 commitDeadline = T0 + 1 hours;\n        vm.startPrank(owner);\n        uint256 id = arena.createRound(\n            Arena.Mode.VaultRaid, 4, commitDeadline, commitDeadline + 1 hours, commitDeadline + 2 hours, 300 ether, 0, 0\n        );\n        adapter.pinQuestion(id, QUESTION, 1, 5, 4, commitDeadline, \"\");\n        vm.stopPrank();\n\n        // An honest player commits early, to choice 1.\n        bytes32 saltA = keccak256(\"alice\");\n        bytes32 commitA = arena.commitmentOf(id, alice, 1, saltA);\n        vm.prank(alice);\n        arena.enter(id, commitA);\n\n        // Four minutes before commits close, a correctly signed attestation issued now (answer 6, so the\n        // winning choice is 6 % 4 + 1 = 3) is relayed by anyone.\n        vm.warp(commitDeadline - 4 minutes);\n        OracleAttestation.Attestation memory a = OracleAttestation.Attestation({\n            requestId: keccak256(\"req\"),\n            chainId: 1,\n            questionHash: QUESTION,\n            answerType: OracleAttestation.ANSWER_UINT256,\n            answer: abi.encode(uint256(6)),\n            figure: 0,\n            fromBlock: 1,\n            toBlock: 2,\n            blockHash: bytes32(uint256(1)),\n            panelJobId: keccak256(\"job\"),\n            panelSize: 5,\n            quorum: 4,\n            agreed: 5,\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 days)\n        });\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, adapter.attestationDigest(a));\n        try adapter.submitAttestation(id, a, abi.encodePacked(r, s, v)) {}\n        catch {\n            // The repaired adapter refuses to store a result before the round's commit boundary.\n            return;\n        }\n\n        // The answer is now public while commits are still open: the attacker reads it and enters.\n        OracleAdapter.Result memory res = adapter.resultOf(id);\n        assertTrue(res.settled);\n        uint8 winning = uint8(res.answer % 4) + 1;\n        bytes32 saltM = keccak256(\"mallory\");\n        bytes32 commitM = arena.commitmentOf(id, mallory, winning, saltM);\n        vm.prank(mallory);\n        arena.enter(id, commitM);\n\n        vm.warp(commitDeadline);\n        vm.prank(alice);\n        arena.reveal(id, 1, saltA);\n        vm.prank(mallory);\n        arena.reveal(id, winning, saltM);\n        vm.warp(commitDeadline + 1 hours);\n        arena.settle(id);\n        vm.prank(mallory);\n        arena.claim(id);\n\n        // Mallory risked nothing and took the whole prize. A commit-reveal game must never let an entry\n        // be placed after the result is readable on chain.\n        assertLe(token.balanceOf(mallory), 1_000 ether, \"an entrant who saw the stored result must not profit\");\n    }\n}","reproduction":"createRound(VaultRaid, 4 choices, commitDeadline = T0+1h, reveal T0+2h, result T0+3h, prize 300 PRIO); pinQuestion(id, Q, 1, 5, 4, notBefore = commitDeadline, ''). Alice enters choice 1 at T0. At commitDeadline - 4 min, anyone calls submitAttestation(id, a, sig) with a.issuedAt = now, answer = 6, signed by the oracle signer. Expected: refused (commits are still open) or at least not readable. Actual: stored; resultOf(id).settled == true, answer 6 -> winning choice 3. Mallory enters choice 3 at commitDeadline - 3 min, reveals, settle(id), claim(id): Mallory's balance goes from 1000 to 1298 PRIO; Alice loses 12. See test/scratch/EarlyResult.t.sol (fails on this code).","severity":"medium","snippet":"        if (a.issuedAt + ISSUED_AT_TOLERANCE < p.notBefore) revert IssuedTooEarly(a.issuedAt, p.notBefore);","title":"OracleAdapter stores and publishes a round's answer up to 5 minutes before the commit deadline, while Arena.enter is still open: a late entrant commits the winning choice with certainty"},{"citation":"resolved","description":"`cancel` becomes callable at `resultDeadline + 72h` while the round is Open; `settle` stays callable for an Open round forever once a result exists. If the adapter receives a valid result after the grace (attestations may carry a long `expiresAt`, and `submitAttestation` is permissionless), two mutually exclusive permissionless transitions are available at once: a player who revealed a losing choice (or missed the reveal) calls cancel() to be refunded 102 instead of 90/80, while a winner calls settle() to be paid 100 + prize. Whoever is mined first decides, and a searcher can order it. The brief says unresolved rounds are cancelled 72 hours after the result deadline; the code should make that the only outcome after the grace: in `settle` require `block.timestamp < resultDeadline + CANCEL_GRACE` (or let `settle` refuse once `cancel` is possible).","line":281,"path":"src/Arena.sol","reproduction":"Round with resultDeadline D; alice reveals choice 1, bob reveals choice 2, no result until D + 72h + 1. Then the oracle result (winning 2) is relayed. State: both `arena.cancel(id)` and `arena.settle(id)` succeed from any address. If alice's cancel is mined first: alice 102 back, bob 102 back, prize returns to pool. If bob's settle is first: alice 90, bob 100 + prize. Expected: a single deterministic outcome after the grace (cancellation).","severity":"low","snippet":"    function cancel(uint256 roundId) external nonReentrant {\n        Round storage r = _rounds[roundId];\n        if (r.state != RoundState.Open) revert NotOpen();\n        if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();","title":"Arena.settle has no upper time bound, so after the 72-hour grace a late result makes settle() and cancel() both live and the round's outcome depends on transaction ordering"},{"citation":"resolved","description":"`createRound` checks that an oracle is set but does not record it in the Round; `settle` reads the current `oracle`. The owner can therefore point open rounds at a different adapter (a different signer, pinned question or tolerance) after players have committed. This is an owner-only action and is reported as a violation of the explicit requirement 'Active rules, reserved payouts and the 0.5% fee cannot change' and of 'Freeze rules ... before entry', not as an exploit by an unprivileged party. Fix: store `oracle` in the Round at creation and read `r.oracle` in `settle`, so `setOracle` only affects future rounds.","line":147,"path":"src/Arena.sol","reproduction":"owner: setOracle(adapterA); createRound(...) -> id; players enter and reveal. owner: setOracle(adapterB) where adapterB (owner-chosen signer) has a result for `id`. settle(id) succeeds using adapterB's answer. Expected: the round settles only with the adapter that was active when it was created.","severity":"low","snippet":"    function setOracle(IRoundOracle to) external onlyOwner {\n        if (address(to) == address(0)) revert ZeroAddress();\n        oracle = to;\n        emit OracleSet(address(to));\n    }","title":"Arena.setOracle replaces the result source of rounds that are already open; the oracle is not frozen per round although the brief freezes active rules"},{"citation":"resolved","description":"PoolManager.swap checks `params.amountSpecified == 0` on the swapper's original params, before beforeSwap; Pool.swap with the hook-adjusted `amountToSwap == 0` returns a zero delta instead of reverting. For grossIn = 1 wei, quoteBuyExactInput gives fee 1, poolLeg 0, so the swap completes with amount0 = -1, amount1 = 0: the whole input is fee. Dust-level, but the README's 'Quote tick limits'/rounding text is wrong and the brief asked for wei-level rounding to be documented; inputs up to 200 wei pay a 1-wei fee of >= 0.5%.","line":149,"path":"src/TreasuryFeeHook.sol","reproduction":"PoolSwapTest.swap{value: 1}(key, {zeroForOne: true, amountSpecified: -1, limit: MIN_SQRT_PRICE+1}). Expected per README: revert. Actual: success; delta (-1, 0); hook.totalFeeCharged() == 1; treasury.totalIncome() == 1. See test/scratch/OneWei.t.sol.","severity":"info","snippet":"    function quoteBuyExactInput(uint256 grossIn) public pure returns (uint256 fee, uint256 poolLeg) {\n        fee = (grossIn * FEE_BPS + (BPS + FEE_BPS - 1)) / (BPS + FEE_BPS);\n        poolLeg = grossIn - fee;\n    }","title":"README says a 1-wei buy 'cannot execute (the pool refuses a zero swap)'; it executes, the swapper pays the wei as fee and receives nothing"}],"hash":"1791255bca5ccddb5a8665fec05a867abfe95deed1b88efc6d637082de619ce0","nodeId":"d3f5ebb2-6178-45d1-9423-d9795732fc16","outcome":"completed","summary":"The review is complete. The findings file is written and validated, no source file was changed, and the only new files are the review output and my scratch proofs under `test/scratch/`.\n\n**Outcome.** The baseline builds and all 54 existing tests pass, but tracing the control flow found three medium defects, each reproduced by a Foundry test that fails on the current code, plus two low and one informational issue. All six are in `.imd-findings.json` with a coverage row for every one of the 65 listed entry points (11 marked `finding`, 58 `holds`, none `unreached`) and four invariant rows.\n\n**Findings, most severe first:**\n\n1. **Hook fee on partial fills (medium).** `beforeSwap` computes the 0.5% from `params.amountSpecified`, not from the ETH the pool actually exchanged. A buy of 10 ETH that stops at a tight price limit swaps about 0.1 ETH yet pays about 0.0498 ETH in fee, roughly 50% of the real leg. The sell-exact-output path has the same defect. The README explicitly promises partial fills pay 0.5% of the partial leg. Proof: `test/scratch/PartialFillFee.t.sol`.\n2. **FeeTreasury strands ETH on partial fills (medium).** `buyPrio` and `buyImd` debit the full `ethIn` from the budget line but `unlockCallback` settles only what the pool took. The difference stays in the contract outside every budget and no function can ever move it. Proof: `test/scratch/TreasuryStranded.t.sol`.\n3. **Oracle result readable before commits close (medium).** The adapter accepts an attestation issued up to 5 minutes before the commit deadline and stores it immediately, with no check against the chain's own clock. A late entrant reads `resultOf`, commits the winning choice and takes the prize. The proof shows an attacker ending with 1298 PRIO from 1000. The oracle-consumer reference confirms anyone can obtain an attestation signed for this adapter's domain by paying the Intake with it as callback target. Proof: `test/scratch/EarlyResult.t.sol`.\n4. **Settle has no upper time bound (low).** After the 72-hour grace both `settle` and `cancel` are live, so a late result makes the outcome depend on transaction ordering.\n5. **setOracle changes open rounds (low).** The oracle is not snapshotted per round, contrary to the brief's frozen-rules requirement. Owner-only, reported as a requirement violation.\n6. **README 1-wei claim is wrong (info).** A 1-wei buy executes, paying the whole wei as fee for zero output.\n\n**Not reached in depth.** The operator script was read for timing and key handling only, not exercised. Economic sizing of prizes against multi-wallet entry was outside my area.","treeHash":null,"usage":{"cachedInputTokens":3026526,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":73807,"runtime":"claude","turns":51,"wallClockMs":1074063}},{"artifacts":[],"attempt":1,"bundleHash":"6ffa4b600e31c65c586365dd525c95bed5b5fee8446079af1ceeec61dfdb8393","device":"fbcdfc017217af1f","findings":[{"description":"FeeTreasury.unlockCallback always swaps with sqrtPriceLimitX96 = TickMath.MIN_SQRT_PRICE + 1. When a buy has consumed every PRIO position in the pool the pool price lands exactly on that limit (the hook's nested leg stops at limit + 1 and the outer remainder swap moves it to the limit). From then on, until someone sells PRIO and lifts the price, every buyPrio call reverts in v4 with PriceLimitAlreadyExceeded instead of spending nothing. The revert undoes the whole call, so no budget line, window counter or bucket is affected and no funds are at risk; the executor simply sees a failed transaction (gas lost) rather than a zero-output purchase. Reproduced in test/FeeTreasuryEdge.t.sol (FeeTreasuryThinPoolTest.test_rollingWindowCountsOnlyTheEthActuallySpent), which asserts the revert and that the budget is untouched. The operator should treat PriceLimitAlreadyExceeded as 'pool empty, retry later' and the README could say so; no contract change is required.","line":317,"path":"src/FeeTreasury.sol","reproduction":"Launch fixture, LP removes all full-range liquidity and leaves a 1e18 PRIO-only position in ticks [-3000,-1200]; executor calls buyPrio(0.5 ether, 0): fills ~0.1 ETH (expected, partial). Executor calls buyPrio(<remaining budget>, 0) again: expected either a zero-output no-op or a treasury-level refusal; actual: revert PriceLimitAlreadyExceeded(4295128740, 4295128740) from the PoolManager.","severity":"info","title":"FeeTreasury.buyPrio reverts, rather than returning zero, once the pool's PRIO side is fully drained to the minimum price"}],"hash":"2238a04c42e12f01006a365c64a9af6322ab54c26b1d1a03137778ce0767e84c","nodeId":"23b1ae83-a7f0-4b09-bfec-fc23e45ea1ae","outcome":"completed","summary":"Done. All 164 tests pass under the default seed and three fixed fuzz seeds, and the diff touches only the six edge and invariant files under `test/`.\n\n**What was broken and why.** The other contributor's revision changed the implementation after my previous round was accepted, which left 20 of my tests failing against the merged tree. Every failure was my test lagging the new behaviour, not a defect in the contracts:\n\n- **Arena** now requires the round's question to be pinned on the adapter before the round exists. My helpers pinned after creation, so every round creation reverted with a missing pin. Both the edge suite and the invariant handler now pin first.\n- **FeeTreasury** now refuses purchases until the owner sets price floors, and sinks are one-shot and non-zero. My fixtures set floors, and the unconfigured-state tests moved to a fresh treasury. The buy-order of refusals changed too, so the max-spend check is now exercised with an amount inside the budget.\n- **OracleAdapter** pins the signer with the question. The rotation test now proves the old key still settles the round pinned under it, the new key cannot, and a round pinned after rotation accepts only the new key.\n- **The hook** refuses a one-wei buy as too small instead of swallowing it as fee. The dust tests now assert the refusal, that nothing is charged, and that the fuzz over all shapes excludes only that one input.\n\n**A real bug in my own handler.** The Arena invariant handler passed a view call as the argument to the pranked `enter`, so the prank was consumed by the view and every entry came from the handler, which held no approval. Every entry reverted silently, so the Arena invariants had never exercised entries, reveals, claims or refunds. That is fixed and the handler now records zero reverts on entry.\n\n**New coverage added** for the behaviours this revision introduced: the price floor boundary measured on the ETH actually spent, the rolling spend window at its exact edges and reset, the window counting only what a partially filled buy took, a pin whose boundary is off by one second either way, the exclusivity of cancel and settle after the grace, the chain-clock gate on both relay and paid request, and a thin-pool suite where the LP leaves and the treasury's buy fills partly.\n\n**Findings.** One informational note in `.imd-findings.json`: once a buy drains the pool's PRIO side to the minimum price, the treasury's fixed swap limit makes the next purchase revert with `PriceLimitAlreadyExceeded` rather than spend nothing. No funds or accounting are affected, and the test asserts the budget is untouched. The operator should treat that revert as \"pool empty, retry later\". I found no defect in the nested-swap fee path, the vault's idle pause, or the frozen result source that warranted a proof.","treeHash":"ecc210284230012a781800baa955efd6bcb0960d","usage":{"cachedInputTokens":2553804,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":54898,"runtime":"claude","turns":54,"wallClockMs":756387}},{"artifacts":[],"attempt":1,"bundleHash":"d94979ca231c826cd7d4126807b5a008e24f3113bb0fe397bd5fd24ee16ae539","device":"e8e860c7f2306473","findings":[{"description":"In beforeSwap the fee for buy-exact-input and sell-exact-output is derived from params.amountSpecified (quoteBuyExactInput / quoteSellExactOutput) and collected before the pool runs. When the swapper's sqrtPriceLimitX96 stops the fill early, the pool exchanges less ETH than requested but the fee is unchanged, so the effective rate on the ETH actually exchanged is far above 0.5%. With a tight enough limit on a sell-exact-output the fee can exceed the ETH the pool pays and the swapper's ETH delta goes negative (pays ETH and PRIO). The README's 'Quote tick limits' paragraph asserts that a partially filled swap pays 0.5% of the partial leg, which is false for these two shapes (it is true for the two afterSwap shapes, which read the real delta). Routers that pass the full-range limit are unaffected; routers or quoters that use a real price limit mis-quote and overpay. Fix: compute the fee for the ETH-specified shapes in afterSwap from the actual BalanceDelta ETH amount (as the other two shapes already do), or correct the documentation and name the limitation.","line":240,"path":"src/TreasuryFeeHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\n\n/// @notice Finding: on the two ETH-specified shapes (buy exact input, sell exact output) the hook computes\n/// its fee in beforeSwap from `params.amountSpecified`, the amount the swapper *asked* for. When the swap's\n/// `sqrtPriceLimitX96` stops the fill early, the pool exchanges less ETH than asked, but the fee stays\n/// 0.5% of the requested amount, so the effective rate on the executed ETH leg is far above 0.5%. The\n/// README states \"a partially filled swap pays 0.5% of the partial leg\"; this test shows it does not.\n/// Expected: fee == ceil(0.5% of the ETH the pool exchanged) (+1 wei rounding at most).\n/// Actual: fee == 0.5% of the full requested amount, ~10x the executed leg's fee in this scenario.\ncontract ProofPartialFillOverchargeTest is Test {\n    using StateLibrary for IPoolManager;\n\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint160 constant FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode,\n            abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12500, 60, IHooks(at));\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        vm.startPrank(factory);\n        vm.deal(factory, 1_000_000 ether);\n        token.approve(address(lpRouter), type(uint256).max);\n        lpRouter.modifyLiquidity{value: 100_000 ether}(\n            key,\n            ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10_000 ether, bytes32(0)),\n            \"\"\n        );\n        token.transfer(trader, 1_000_000 ether);\n        vm.stopPrank();\n        vm.deal(trader, 1_000 ether);\n    }\n\n    function swapLimited(bool zeroForOne, int256 amountSpecified, uint160 limit, uint256 value) internal {\n        vm.startPrank(trader);\n        token.approve(address(swapRouter), type(uint256).max);\n        swapRouter.swap{value: value}(\n            key,\n            SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: limit}),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        vm.stopPrank();\n    }\n\n    function test_buyExactInput_partialFill_feeFollowsExecutedLeg() public {\n        (uint160 price,,,) = IPoolManager(address(manager)).getSlot0(key.toId());\n        uint256 ethBefore = trader.balance;\n        // Ask to spend 100 ETH but only allow the price to move 0.1%: the pool takes ~10 ETH.\n        swapLimited(true, -100 ether, price - price / 1000, 100 ether);\n        uint256 paid = ethBefore - trader.balance;\n        uint256 fee = treasury.totalIncome();\n        uint256 executedLeg = paid - fee;\n        assertLt(executedLeg, 20 ether, \"the fill was partial\");\n        // 0.5% of the executed ETH leg, rounded up, allowing the one-wei self-consistency rounding.\n        assertLe(fee, hook.feeOnLeg(executedLeg) + 1, \"fee must be 0.5% of the ETH the pool exchanged\");\n    }\n\n    function test_sellExactOutput_partialFill_feeFollowsExecutedLeg() public {\n        (uint160 price,,,) = IPoolManager(address(manager)).getSlot0(key.toId());\n        uint256 ethBefore = trader.balance;\n        // Ask for 100 ETH out but only allow the price to move 0.1%: the pool pays ~10 ETH.\n        swapLimited(false, int256(100 ether), price + price / 1000, 0);\n        uint256 received = trader.balance - ethBefore;\n        uint256 fee = treasury.totalIncome();\n        uint256 executedLeg = received + fee;\n        assertLt(executedLeg, 20 ether, \"the fill was partial\");\n        assertLe(fee, hook.feeOnLeg(executedLeg) + 1, \"fee must be 0.5% of the ETH the pool exchanged\");\n    }\n}","reproduction":"Pool at 1:1 with 10_000e18 full-range liquidity, trader buys with amountSpecified=-100 ether and sqrtPriceLimitX96 = price - price/1000. Pool exchanges 10.1367 ETH; trader pays 10.6342 ETH; hook fee = 0.497512 ETH (0.5% of 100 ETH). Expected fee: feeOnLeg(10.1367 ETH) = 0.050684 ETH (+1 wei). Sell-exact-output of 100 ETH with limit price + price/1000: pool pays 9.99 ETH, fee 0.502513 ETH, trader receives 9.4875 ETH (effective 5.0%). Run: forge test --match-path test/scratch/ProofPartialFillOvercharge.t.sol","severity":"medium","title":"Hook fee on ETH-specified swaps is 0.5% of the requested amount, not of the executed ETH leg (partial fills overcharge)"},{"description":"setOracle is onlyOwner with no state check, and the Round struct does not record the oracle it was created under; settle() always reads the current `oracle`. After players have committed and revealed, the owner can install a contract that returns any answer and settle the open round with it, deciding who loses 10 PRIO and whether the funded prize is paid or returned to the pool the owner later allocates. OracleAdapter.setSigner has the same shape: the trusted signer can be rotated to an owner key and a matching attestation signed for any pinned question. The brief requires rules to be frozen before entry and says active rules cannot change; the result source is the decisive active rule. Fix: snapshot the oracle address (and ideally the adapter's signer) into the Round at creation and read that in settle(), or refuse setOracle/setSigner while any round is Open.","line":147,"path":"src/Arena.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {Arena, IRoundOracle} from \"src/Arena.sol\";\nimport {OracleAdapter} from \"src/OracleAdapter.sol\";\n\n/// @dev An \"oracle\" the owner controls: it reports whatever answer the owner wants for any round.\ncontract OwnerOracle is IRoundOracle {\n    uint256 public answer;\n\n    function set(uint256 a) external {\n        answer = a;\n    }\n\n    function resultOf(uint256) external view returns (Result memory r) {\n        r.answer = answer;\n        r.settled = true;\n        r.issuedAt = uint64(block.timestamp);\n    }\n\n    function ISSUED_AT_TOLERANCE() external pure returns (uint64) {\n        return 5 minutes;\n    }\n}\n\n/// @notice Finding: `Arena.setOracle` has no restriction while rounds are open, so the result source of a\n/// round that players have already paid into can be replaced after their commitments are in. The brief\n/// freezes \"rules, deadlines, maximum loss and funded prizes before entry\" and says active rules cannot\n/// change; the oracle that decides the winning choice is the most consequential of those rules.\n/// Expected: a round settles only through the oracle it was created with (or setOracle is refused while\n/// any round is open). Actual: the owner points the Arena at a contract of their own and settles the\n/// open round with an answer of their choosing, taking the prize from the players who chose correctly.\ncontract ProofOracleSwapMidRoundTest is Test {\n    address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n    uint64 constant T0 = 1_800_000_000;\n\n    PrismRiotToken token;\n    Arena arena;\n    OracleAdapter adapter;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        vm.warp(T0);\n        token = new PrismRiotToken();\n        arena = new Arena(owner, address(token));\n        adapter = new OracleAdapter(owner, SIGNER);\n        vm.prank(owner);\n        arena.setOracle(IRoundOracle(address(adapter)));\n        token.approve(address(arena), 1_000 ether);\n        arena.fundPrizes(1_000 ether);\n        token.transfer(alice, 102 ether);\n    }\n\n    function test_openRoundCannotBeSettledByAnOracleInstalledAfterEntries() public {\n        uint64 commitDeadline = T0 + 1 hours;\n        vm.prank(owner);\n        uint256 id = arena.createRound(\n            Arena.Mode.FactionDuel, 2, commitDeadline, commitDeadline + 1 hours, commitDeadline + 2 hours, 500 ether, 0, 0\n        );\n        // Alice commits to faction 2 and reveals it.\n        bytes32 salt = keccak256(\"salt\");\n        bytes32 c = arena.commitmentOf(id, alice, 2, salt);\n        vm.startPrank(alice);\n        token.approve(address(arena), 102 ether);\n        arena.enter(id, c);\n        vm.stopPrank();\n        vm.warp(commitDeadline);\n        vm.prank(alice);\n        arena.reveal(id, 2, salt);\n        vm.warp(commitDeadline + 1 hours);\n\n        // The owner swaps the oracle under the open round and makes faction 1 win: alice loses 10 PRIO and\n        // the 500 PRIO prize goes back to the pool the owner allocates.\n        OwnerOracle mine = new OwnerOracle();\n        mine.set(0); // 0 % 2 + 1 = 1\n        vm.prank(owner);\n        try arena.setOracle(IRoundOracle(address(mine))) {} catch {}\n        try arena.settle(id) {} catch {}\n\n        Arena.Round memory r = arena.rounds(id);\n        assertTrue(\n            r.state != Arena.RoundState.Settled || r.winningChoice == 0,\n            \"an open round was settled through an oracle installed after players had paid in\"\n        );\n    }\n}","reproduction":"Create a FactionDuel round with a 500 PRIO prize via the real OracleAdapter; alice enters and reveals faction 2; after the reveal deadline the owner calls arena.setOracle(ownerControlledOracle) whose resultOf() returns answer 0 (winning 1) and calls settle(id). Expected: settle reverts (NoResult from the original adapter) or setOracle is refused. Actual: round is Settled with winningChoice 1, alice's payout is 90 PRIO and the 500 PRIO prize returns to unallocatedPrizePool. Run: forge test --match-path test/scratch/ProofOracleSwapMidRound.t.sol","severity":"medium","title":"Arena.setOracle (and OracleAdapter.setSigner) can replace the result source of rounds that are already open"},{"description":"rewardPerToken() does not advance while totalStaked == 0 but lastUpdateTime does, so reward for that interval is never attributed to anyone. It stays counted in rewardsOwed and in the vault balance forever: a later notifyReward only rolls over the remaining period of the current stream (zero after periodFinish), and no owner/funder recovery path exists. Treasury purchases that land when nobody is staked (e.g. the first buys after launch) are lost to the reward programme. Fix: when totalStaked == 0 during _update, carry the undistributed amount into a `pendingUndistributed` bucket that the next notification adds to `total`, or let the funder reclaim rewards that could not be distributed.","line":80,"path":"src/StakingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\n\n/// @notice Finding: rewards that stream while `totalStaked == 0` are counted as `rewardsOwed` but can never\n/// be earned or claimed by anyone, and no path returns them to the funder or rolls them into a later\n/// stream. Expected: funded PRIO is either distributed to stakers or recoverable/rolled forward. Actual:\n/// after a 30 day stream with no stakers, 300 PRIO sit in the vault forever, `rewardsOwed` stays 300, and a\n/// staker who arrives afterwards earns nothing from it.\ncontract ProofStrandedRewardsTest is Test {\n    PrismRiotToken token;\n    StakingVault vault;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        token = new PrismRiotToken();\n        vault = new StakingVault(owner, address(token));\n        vm.prank(owner);\n        vault.setRewardFunder(address(this));\n        token.approve(address(vault), type(uint256).max);\n        token.transfer(alice, 100 ether);\n        vm.prank(alice);\n        token.approve(address(vault), type(uint256).max);\n    }\n\n    function test_rewardsStreamedWithNoStakersAreNotStranded() public {\n        vault.notifyReward(300 ether);\n        skip(30 days); // the whole stream elapses with nobody staked\n        vm.prank(alice);\n        vault.stake(100 ether);\n        skip(30 days);\n        assertEq(vault.earned(alice), 0, \"nothing accrued to the late staker: expected under the schedule\");\n        // The 300 PRIO must still be reachable somehow: rolled into a later stream, or recoverable by the\n        // funder/owner. Neither exists: a new notification only rolls over the *remaining* period (zero).\n        vault.notifyReward(1 ether);\n        skip(30 days);\n        uint256 reachable = vault.earned(alice);\n        vm.prank(alice);\n        vault.claim();\n        uint256 strandedInVault = token.balanceOf(address(vault)) - vault.totalStaked();\n        assertLe(strandedInVault, 1e9, \"funded rewards are stuck in the vault with no way to distribute or recover\");\n        reachable;\n    }\n}","reproduction":"notifyReward(300e18) with no stakers; skip 30 days; alice stakes 100e18; skip 30 days: earned(alice) == 0, rewardsOwed == 300e18, vault balance - totalStaked == 300e18 with no function that can move it. Run: forge test --match-path test/scratch/ProofStrandedRewards.t.sol","severity":"low","title":"StakingVault strands rewards streamed while totalStaked == 0"},{"description":"setSinks, setImd and setImdPool have no one-shot guard and accept any addresses (setSinks even accepts zero). The owner can point stakingVault/arena at contracts of their own that implement notifyReward/fundPrizes, or set `imd` to a token they issued and `imdPoolSet` to a pool they seeded, so buyPrio/buyImd deliver value to the owner instead of to rewards and agent work. The brief fixes the split at 30/30/40 with 40% to the owner; the on-chain enforcement only holds while the owner behaves. Fix: make setSinks/setImd/setImdPool one-shot like setPrio/bindHook (or timelocked), and reject zero addresses in setSinks.","line":137,"path":"src/FeeTreasury.sol","reproduction":"owner.setSinks(ownerVault, ownerArena, owner) where ownerVault/ownerArena are trivial contracts with notifyReward/fundPrizes that keep the tokens; executor.buyPrio(x, 1) sends all purchased PRIO to owner-controlled contracts. Expected: PRIO reaches the project's StakingVault and Arena; actual: reaches whatever the owner set last.","severity":"low","title":"FeeTreasury owner can redirect the 30% IMD and 30% PRIO budgets through mutable sink/venue settings"},{"description":"settle() only requires block.timestamp >= revealDeadline and a stored result; it never checks resultDeadline. A result stored any time later, even after resultDeadline + 72h, still settles the round if nobody has called cancel() first, so the brief's 'cancel unresolved rounds 72 hours after their result deadline, refund all' depends on a race between the two permissionless calls. Players expecting a 102 PRIO refund may instead receive 80/90. Fix: in settle() require block.timestamp <= resultDeadline + CANCEL_GRACE (or <= resultDeadline), so a result that arrives after the window can only be followed by cancellation.","line":256,"path":"src/Arena.sol","reproduction":"Round with resultDeadline D; nobody acts until D + 72h + 1; submit a valid attestation to the adapter and call settle(id) before anyone calls cancel(id). Expected: settle refused (round cancellable only). Actual: round Settled; a player who missed the reveal gets 80 instead of 102.","severity":"low","title":"Arena.settle does not enforce resultDeadline; late results race with cancel()"},{"description":"receive() is unrestricted so that PoolManager.take can pay native ETH to the hook, but any other sender's ETH is also accepted. Such ETH is not added to pendingEth, so flush() never forwards it and no other function can move it: it is permanently stuck. Fix: restrict receive() to msg.sender == address(poolManager), or make flush() forward address(this).balance (all ETH the hook holds is by design fee ETH) and keep pendingEth as the accounting mirror.","line":207,"path":"src/TreasuryFeeHook.sol","reproduction":"vm.deal(alice, 1 ether); alice sends 1 ether to the hook: call succeeds, hook.pendingEth() unchanged, hook.balance == pendingEth + 1 ether, flush() forwards only pendingEth. Expected: refused or forwarded; actual: stuck.","severity":"low","title":"TreasuryFeeHook.receive() accepts arbitrary ETH that is never accounted and cannot be recovered"},{"description":"quoteBuyExactInput solves fee = ceil(gross*50/10050) and quoteSellExactOutput fee = ceil(net*50/9950). These are the smallest self-consistent fees but, measured against the resulting pool leg, can be one wei above feeOnLeg(leg). The docs say 'rounded up to the next wei'; the deviation is at most one wei and is covered by TreasuryFeeHookEdgeTest.testFuzz_feeIsHalfPercentOfExecutedLeg, which allows it. Document it or compute the fee from the actual leg in afterSwap.","line":150,"path":"src/TreasuryFeeHook.sol","reproduction":"grossIn such that ceil(grossIn*50/10050) == feeOnLeg(grossIn - fee) + 1, e.g. the counterexample bound(8303, 1e6, 50 ether) from the fuzz run: fee 248756218905467704 vs feeOnLeg 248756218905467703.","severity":"info","title":"ETH-specified shapes can charge one wei above ceil(0.5% of the ETH leg)"},{"description":"exit() calls withdraw(staked[msg.sender]) first, which reverts with ZeroAmount when the stake is zero, so a user who withdrew and still has unclaimed rewards must call claim() directly. Separately, FeeTreasury.buyPrio splits `out` in half and calls StakingVault.notifyReward(out/2), which reverts with ZeroAmount when out < 2, so a dust purchase reverts rather than forwarding everything to the Arena. Both are usability edges with no fund impact.","line":125,"path":"src/StakingVault.sol","reproduction":"stake(1), withdraw(1) during a funded stream, exit() -> ZeroAmount. buyPrio(ethIn) on a pool where ethIn buys 1 wei of PRIO -> ZeroAmount from the vault.","severity":"info","title":"StakingVault.exit() reverts for a user with rewards but no stake; buyPrio reverts when the purchase yields < 2 wei"}],"hash":"25eda4ce6dce5fc5384770670bca58f4b1ba847e9e384b5103be4eb84284d4b4","nodeId":"23b1ae83-a7f0-4b09-bfec-fc23e45ea1ae","outcome":"completed","summary":"The findings file is in place at the repository root and is intentionally excluded from git by the harness's own exclude rules, so the harness picks it up separately. Work is complete.\n\n## Summary\n\n**Delivered: seven new test files under `test/`, 81 new tests, 135 total, all passing.** Build is clean. The existing 54 tests were left untouched and built upon.\n\nNew suites and what they cover:\n\n- **TreasuryFeeHookEdge.t.sol**: 1 wei and dust swaps, tiny exact outputs both directions, every swap shape from 1 wei to 100 ETH, fee equals 0.5% of the executed ETH leg measured from the swapper's balance, partial fills on the afterSwap shapes, a treasury that refuses ETH (swap succeeds, fee waits, anyone flushes later), fees charged before binding, a manager that cannot cover a claim, a hookless ETH/PRIO pool paying no hook fee, a forged key naming the hook, no fee or treasury setter, two-step ownership, zero-address constructor guards.\n- **TreasuryInvariants.t.sol**: a handler driving all four swap shapes from three traders plus flush, redeemClaims, allocate, owner and reserve withdrawals, buyPrio and reserve-target changes. Invariants: treasury ETH fully bucketed, income conservation against ghost outflows, hook fee conservation (charged = delivered + pendingEth + pendingClaims, 6909 balance = claims), reserve cap, purchased PRIO forwarded and split.\n- **ArenaInvariants.t.sol**: full lifecycle handler with real signed attestations through the adapter across many rounds. Invariants: balance accounting, token conservation, finished rounds frozen, locked prizes equal open rounds' prizes, players never extract more than entries plus funding. Per-entry payout is asserted inside the handler (80 / 90 / 100 + share / 102).\n- **ArenaEdge.t.sol**, **StakingVaultEdge.t.sol**, **OracleAdapterEdge.t.sol**, **FeeTreasuryEdge.t.sol**: failure paths (unapproved balances, exact half-open deadline boundaries, wrong states, non-entrants, double claims, prize dust, zero amounts, over-withdrawal, strangers funding, not-yet-valid clocks, malformed signatures, signer rotation, callback after manual relay, budget window edge, rejecting ETH recipients, purchase rollback, IMD venue flow on a second pool), plus fuzzed properties (payout bounds, prize split, pro rata by stake and time, claims never exceed funding, allocation arithmetic).\n\n**Findings reported in `.imd-findings.json` (8, three with runnable proofs that fail on the current code):**\n\n1. **Medium**: the hook's fee on buy-exact-input and sell-exact-output is 0.5% of the *requested* amount, not of the ETH the pool actually exchanged. A price-limited 100 ETH buy that filled 10.1 ETH paid 0.4975 ETH instead of 0.0507 ETH. The README's claim about partial fills is false for these two shapes.\n2. **Medium**: `Arena.setOracle` and `OracleAdapter.setSigner` can replace the result source of a round players have already paid into. Proof settles an open round through an owner-controlled oracle.\n3. **Low**: StakingVault strands rewards streamed while nobody is staked, with no recovery or roll-forward. Proof included.\n4. **Low**: FeeTreasury sinks and IMD venue are mutable, so the owner can redirect the 30/30 budgets.\n5. **Low**: `Arena.settle` ignores `resultDeadline`, so a late result races `cancel`.\n6. **Low**: the hook's `receive()` accepts unaccounted ETH that is stuck forever.\n7. **Info**: ETH-specified shapes may charge one wei above ceil(0.5%) of the leg (the fuzz test allows this and documents it).\n8. **Info**: `exit()` reverts with zero stake; `buyPrio` reverts on sub-2-wei output.\n\nNothing was committed, in line with the instruction to commit only when asked; the new files are untracked in `test/` and the findings file sits at the root under the harness's exclude rule.","treeHash":"a7727b698de04b98d9f6207af97fc8c9b46c5829","usage":{"cachedInputTokens":2572650,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":91903,"runtime":"claude","turns":53,"wallClockMs":1187866}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5739ce0d803a43cd","findings":[{"citation":"resolved","description":"Kept from write_foundry_tests (reproduced; no contract change required). unlockCallback always swaps with sqrtPriceLimitX96 = MIN_SQRT_PRICE + 1. After a purchase that consumes every PRIO position the pool price sits exactly at that limit (the hook's nested leg stops at limit + 1 and the outer remainder moves it to the limit), so until someone sells PRIO every further buyPrio/buyImd call reverts inside v4 with PriceLimitAlreadyExceeded instead of spending nothing. The revert is atomic: no budget line, window counter or bucket changes and no funds are at risk. It is the same behaviour an unhooked v4 pool shows at its limit. The gap is documentation: README and docs/OPERATOR.md do not tell the operator that this revert means 'pool empty, retry after a sell', so an unattended operator loop would keep paying gas for reverting transactions. No revision of the contracts is needed; a sentence in OPERATOR.md (treat PriceLimitAlreadyExceeded as empty pool, back off) resolves it.","line":317,"path":"src/FeeTreasury.sol","reproduction":"Launch fixture, LP removes all full-range liquidity and leaves a 1e18 PRIO-only position in ticks [-3000,-1200]; executor calls buyPrio(0.5 ether, 0): fills ~0.1 ETH (partial, the rest stays on prioBudget). Executor calls buyPrio(<remaining budget>, 0) again. Expected per OPERATOR.md: documented outcome; actual: revert PriceLimitAlreadyExceeded(4295128740, 4295128740) from the PoolManager, budget untouched. Reproduced by the tree's own test/FeeTreasuryEdge.t.sol FeeTreasuryThinPoolTest.test_rollingWindowCountsOnlyTheEthActuallySpent (asserts the revert).","severity":"info","snippet":"                zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1","title":"FeeTreasury.buyPrio/buyImd revert with PriceLimitAlreadyExceeded, not a zero-output no-op, once a buy has drained the pool to the minimum price; undocumented for the operator"}],"hash":"506fdd77b0c73df21bc50103330aad38ceb38dcd1ab4faa3366599350e5eb49a","nodeId":"c8234b0a-239e-4496-a946-4c0a50e330b8","outcome":"completed","summary":"The review is complete and the report is written.\n\n**Outcome.** Every finding from my first round is fixed in the revised tree. All eight attached proofs now pass, the project's own suite passes (173 tests), and my own probes found no defect introduced by the fixes. The report at `.imd-findings.json` carries one informational note and a coverage row for each of the 67 entry points plus three invariants.\n\n**How each prior finding was settled.**\n- Hook fee on partial fills (high): the nested-swap fix is sound. The v4 library skips hook callbacks when the hook is the swap caller, so the hook can run the leg itself. My probes charged exactly 0.5% of the actual ETH leg at four price limits in both directions, the hook nets to zero across mixed swaps, and a seller never pays ETH.\n- Vault rewards stranded while nothing is staked (high): the stream now pauses while idle. A multi-cycle sequence of idle, stake, exit, re-notify and re-stake left only 250 wei of rounding dust in the vault.\n- Early oracle answer during commits (medium): both the intake callback and the public relay refuse before the pinned boundary on the chain's own clock, and request() refuses too.\n- Treasury partial fills stranding ETH (medium): unspent ETH returns to its budget line. The balance-equals-lines invariant holds.\n- Oracle not frozen per round (medium): createRound requires a pinned question whose boundary equals the commit deadline and snapshots the oracle. The pinned signer outlives rotation.\n- Cancel racing settle (medium): a resolved round can no longer be cancelled.\n- All six advisory items (per-window spend cap and price floors, IMD pool coupling, treasury binding checks, hook receive restriction, one-shot sinks and non-withdrawable IMD, 1-wei buy refused) are implemented and tested.\n\n**The one item kept.** An informational note from the test writer: once a treasury purchase drains the pool to the minimum price, further purchases revert with the PoolManager's price-limit error rather than spending nothing. Funds are untouched and the behaviour matches any v4 pool at its limit. The gap is that the operator docs do not say to treat that revert as \"pool empty, back off\". No contract change is needed.\n\n**Also verified.** The manifest's constructor order and permission flags match the hook, the fee tier is accepted, notes are under the length cap, and all deployed sizes are well inside the limits.","treeHash":null,"usage":{"cachedInputTokens":3759476,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":51506,"runtime":"claude","turns":43,"wallClockMs":728722}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ddcecd7483f623f7","findings":[],"hash":"608dfa3b250bbed0a0821943e4ab0f9968cd8756db7d0075ce3beeb1df0cd1ec","nodeId":"c8234b0a-239e-4496-a946-4c0a50e330b8","outcome":"failed","summary":"fetch failed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":12169}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"56b13350bba65fd7","findings":[{"citation":"resolved","description":"For a buy with exact ETH input and a sell with exact ETH output the fee is fixed in beforeSwap from params.amountSpecified and settled immediately through _collect (take or mint). Uniswap v4's Pool.swap stops at sqrtPriceLimitX96 without reverting (Pool.sol line 344) and reports only the filled part; Hooks.afterSwap then subtracts the hook's full beforeSwap delta from the swapper (Hooks.sol line 312). So when a swapper sets a price limit for slippage protection and only part of the order fills, the whole fee on the requested amount is still charged. The effective rate is 0.5% x requested/actual, e.g. ~0.98% when half fills, and it is unbounded as the fill shrinks. This breaks the brief's 'immutable extra 0.5% on the ETH leg' and the README's claim that 'a partially filled swap pays 0.5% of the partial leg' (README, 'Quote tick limits'), which is only true for the two afterSwap shapes. The treasury receives ETH the swapper never agreed to pay. Fix: for the two beforeSwap shapes, settle the final fee in afterSwap from the actual delta.amount0() (the hook already has afterSwapReturnDelta): either move the entire fee to afterSwap for all four shapes (the swapper then pays leg+fee / receives leg-fee, documented), or keep the beforeSwap estimate and in afterSwap return the difference between the estimate and feeOnLeg(actual leg) as a negative unspecified delta refund. A revert on partial fill for these shapes is the smaller change but should be documented as a quote-tick limit.","line":240,"path":"src/TreasuryFeeHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\n\n/// @dev A swap that stops at the swapper's sqrtPriceLimitX96 (a partial fill, which v4 allows without\n/// reverting) still pays the hook fee that beforeSwap computed on the *requested* ETH amount. The fee is then\n/// far above 0.5% of the ETH the pool actually exchanged. Fails on the current code; passes once the fee\n/// charged is 0.5% of the actual ETH leg (within 1 wei of rounding) on every swap shape.\ncontract PartialFillOverchargeTest is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint24 constant POOL_FEE = 12500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode, abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), POOL_FEE, TICK_SPACING, IHooks(at));\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        vm.deal(factory, 1_000_000 ether);\n        lpRouter.modifyLiquidity{value: 100_000 ether}(\n            key,\n            ModifyLiquidityParams(\n                TickMath.minUsableTick(TICK_SPACING), TickMath.maxUsableTick(TICK_SPACING), 10_000 ether, bytes32(0)\n            ),\n            \"\"\n        );\n        token.transfer(trader, 10_000_000 ether);\n        vm.stopPrank();\n        vm.deal(trader, 1_000 ether);\n    }\n\n    function swapWithLimit(bool zeroForOne, int256 amountSpecified, uint160 limit, uint256 value)\n        internal\n        returns (BalanceDelta d)\n    {\n        vm.startPrank(trader);\n        token.approve(address(swapRouter), type(uint256).max);\n        d = swapRouter.swap{value: value}(\n            key, SwapParams(zeroForOne, amountSpecified, limit), PoolSwapTest.TestSettings(false, false), \"\"\n        );\n        vm.stopPrank();\n    }\n\n    /// Buy with exact ETH input 100 ETH, price limit ~1% below spot: the pool takes only part of the input.\n    function test_buyExactInput_partialFill_feeIsOnActualLeg() public {\n        uint256 grossIn = 100 ether;\n        uint160 limit = uint160((uint256(SQRT_PRICE_1_1) * 995) / 1000);\n        BalanceDelta d = swapWithLimit(true, -int256(grossIn), limit, grossIn);\n        uint256 paid = uint256(uint128(-d.amount0()));\n        uint256 fee = treasury.totalIncome();\n        uint256 leg = paid - fee;\n        assertLt(paid, grossIn, \"the swap was only partially filled\");\n        // The brief: 0.5% of the ETH the pool exchanged, rounded up. Allow 1 wei of rounding.\n        assertLe(fee, hook.feeOnLeg(leg) + 1, \"fee must be 0.5% of the actual ETH leg, not of the requested input\");\n    }\n\n    /// Sell with exact ETH output 50 ETH, price limit ~1% above spot: the pool pays out only part of it.\n    function test_sellExactOutput_partialFill_feeIsOnActualLeg() public {\n        uint256 wantOut = 50 ether;\n        uint160 limit = uint160((uint256(SQRT_PRICE_1_1) * 1005) / 1000);\n        uint256 before = trader.balance;\n        BalanceDelta d = swapWithLimit(false, int256(wantOut), limit, 0);\n        uint256 received = trader.balance - before;\n        uint256 fee = treasury.totalIncome();\n        uint256 leg = received + fee;\n        assertLt(uint256(uint128(d.amount0())), wantOut, \"the swap was only partially filled\");\n        assertLe(fee, hook.feeOnLeg(leg) + 1, \"fee must be 0.5% of the actual ETH leg, not of the requested output\");\n    }\n}","reproduction":"Pool at sqrtPrice 1:1 with 10_000e18 full-range liquidity, treasury bound. Trader swaps zeroForOne exact input amountSpecified = -100 ether with sqrtPriceLimitX96 = SQRT_PRICE_1_1 * 995 / 1000. beforeSwap: fee = ceil(100e18*50/10050) = 497512437810945274 wei taken at once. The pool fills only until the limit: swapper's delta.amount0 = -(50.89e18 + 0.4975e18) approx. Actual ETH leg = 50886... e18; feeOnLeg(leg) = 254436740665352078. Expected fee <= 254436740665352079; actual fee charged and delivered to the treasury = 497512437810945274 (about 0.98% of the leg). Same for oneForZero exact output 50 ether with limit SQRT_PRICE_1_1 * 1005 / 1000: fee 251256281407035176 charged vs feeOnLeg(actual leg) 248756218905472638. With MIN_SQRT_PRICE/MAX_SQRT_PRICE limits (every existing test) the fill is complete and the fee is exact, which is why the suite does not see it.","severity":"high","snippet":"            (fee, leg) = quoteBuyExactInput(uint256(-params.amountSpecified));","title":"Hook fee on ETH-specified swaps is computed on the requested amount, so a partial fill at the swapper's price limit pays far more than 0.5% of the actual ETH leg"},{"citation":"resolved","description":"rewardPerToken does not advance while totalStaked == 0, but lastUpdateTime does advance on the next _update, and periodFinish is fixed at notification time. Every second of a stream with no stakers therefore distributes rewardRate to nobody. Those tokens stay in the vault, are still included in rewardsOwed (claim is the only decrement) and no function (owner or funder) can recover or re-queue them. In this launch the sequence is the normal one: FeeTreasury.buyPrio calls notifyReward on the vault before any staker exists (the treasury is the only funder and there is no 'stake first' gate), so the first streams are lost in proportion to the time before the first stake, and any later window where every staker exits loses the same way. The brief asks that 'only funded PRIO rewards' be distributed 'pro rata by stake and time' and that accrual stop when unfunded; here funded rewards are destroyed instead of distributed. Fix: in notifyReward (or _update) pause the stream while totalStaked == 0 (do not move lastUpdateTime, extend periodFinish by the idle time), or roll the undistributed amount into the next notification's leftover, or add a funder-only sweep of balance - totalStaked - (rewards actually accrued).","line":81,"path":"src/StakingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\n\n/// @dev Rewards stream by wall clock whether or not anyone is staked. With `totalStaked == 0` the\n/// `rewardPerToken` accumulator does not move, so every wei streamed in that window belongs to nobody,\n/// stays counted in `rewardsOwed` forever and has no recovery path. Fails now; passes once streaming\n/// pauses while nothing is staked, or the unearned part is re-queued or recoverable by the funder.\ncontract StakingStrandedTest is Test {\n    PrismRiotToken token;\n    StakingVault vault;\n    address owner = makeAddr(\"owner\");\n    address funder = makeAddr(\"funder\");\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        token = new PrismRiotToken();\n        vault = new StakingVault(owner, address(token));\n        vm.prank(owner);\n        vault.setRewardFunder(funder);\n        token.transfer(alice, 1_000 ether);\n        token.transfer(funder, 10_000 ether);\n        vm.prank(alice);\n        token.approve(address(vault), type(uint256).max);\n        vm.prank(funder);\n        token.approve(address(vault), type(uint256).max);\n    }\n\n    function test_rewardsStreamedWithNoStakersAreLostForever() public {\n        // The treasury's first PRIO purchase funds the vault before anyone has staked.\n        vm.prank(funder);\n        vault.notifyReward(3_000 ether);\n        skip(15 days); // half the stream passes with totalStaked == 0\n\n        vm.prank(alice);\n        vault.stake(100 ether);\n        skip(30 days); // well past periodFinish\n        vm.prank(alice);\n        vault.exit();\n\n        uint256 paid = token.balanceOf(alice) - 1_000 ether;\n        uint256 stranded = token.balanceOf(address(vault)); // nothing is staked any more\n        // Every funded reward must be claimable by someone or recoverable; nothing may be stranded\n        // (allow dust from fixed-point rounding).\n        assertLe(stranded, 1e9, \"half the funded rewards are stranded with no stakers and no recovery path\");\n        assertGe(paid, 3_000 ether - 1e9);\n    }\n}","reproduction":"Funder calls notifyReward(3000 ether) with totalStaked == 0 (rewardsDuration 30 days). skip(15 days). Alice stakes 100 ether, skip(30 days), Alice exit(). Alice receives 1500e18 - dust; the vault still holds 1500000000000000000100 wei with totalStaked == 0 and rewardsOwed == 1500e18 + dust. No call can move those tokens: notifyReward only adds, claim pays rewards[account] which is 0 for everyone, there is no sweep. Expected: the full 3000e18 is distributed to stakers (or recoverable by the funder).","severity":"high","snippet":"        if (totalStaked == 0) return rewardPerTokenStored;","title":"StakingVault streams rewards by wall clock while totalStaked is zero; everything streamed in that window is unearnable, permanently counted in rewardsOwed and unrecoverable"},{"citation":"resolved","description":"The tolerance is applied in the direction that admits early answers: an attestation with issuedAt in [commitDeadline - 5 min, commitDeadline) passes both _verifyAttestation (issuedAt <= now + 5 min) and this check, and _accept writes _results[roundId].settled = true immediately. Nothing compares block.timestamp with p.notBefore, so the stored answer is readable via resultOf while Arena.enter still accepts commitments (enter requires block.timestamp < commitDeadline). Anyone watching the adapter (or the executor, who chooses when to call request()) enters (answer % choiceCount) + 1 and shares the prize risk-free; the prize is split with informed entrants and honest players' odds collapse. The same window exists through the Intake callback onOracleResult. The brief requires clock tolerance 'consistent with commit/reveal boundaries': tolerance should cover clock skew of the oracle's timestamp, not allow the result to be known before the boundary. Fix: in _accept require block.timestamp >= p.notBefore (store nothing before the commit deadline), and keep the issuedAt tolerance only as a skew allowance; alternatively Arena.settle should refuse results whose storage block precedes commitDeadline, but the adapter is the place that can prevent the leak.","line":248,"path":"src/OracleAdapter.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {Arena, IRoundOracle} from \"src/Arena.sol\";\nimport {OracleAdapter} from \"src/OracleAdapter.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\n\n/// @dev The adapter stores a result as soon as `issuedAt + 5 min >= notBefore`, with no check that the\n/// commit deadline has passed on chain. An attestation issued 5 minutes before the commit deadline is\n/// accepted at once, the answer is public in `resultOf`, and anyone still inside the commit window\n/// enters the winning choice. Fails now; passes once a result cannot be stored (or read as settled)\n/// before the round's commit deadline.\ncontract EarlyResultLeakTest is Test {\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n    bytes32 constant QUESTION = keccak256(\"round question\");\n\n    PrismRiotToken token;\n    Arena arena;\n    OracleAdapter adapter;\n    address owner = makeAddr(\"owner\");\n    address honest = makeAddr(\"honest\");\n    address sniper = makeAddr(\"sniper\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        token = new PrismRiotToken();\n        arena = new Arena(owner, address(token));\n        adapter = new OracleAdapter(owner, SIGNER);\n        vm.prank(owner);\n        arena.setOracle(IRoundOracle(address(adapter)));\n        token.transfer(honest, 1_000 ether);\n        token.transfer(sniper, 1_000 ether);\n        vm.prank(honest);\n        token.approve(address(arena), 102 ether);\n        vm.prank(sniper);\n        token.approve(address(arena), 102 ether);\n        token.approve(address(arena), 1_000 ether);\n        arena.fundPrizes(1_000 ether);\n    }\n\n    function test_resultStoredBeforeCommitDeadlineIsReadable() public {\n        uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n        vm.startPrank(owner);\n        uint256 id = arena.createRound(\n            Arena.Mode.VaultRaid, 4, commitDeadline, commitDeadline + 1 hours, commitDeadline + 2 hours, 300 ether, 0, 0\n        );\n        adapter.pinQuestion(id, QUESTION, 1, 5, 4, commitDeadline, \"\");\n        vm.stopPrank();\n\n        bytes32 cH = arena.commitmentOf(id, honest, 1, keccak256(\"h\"));\n        vm.prank(honest);\n        arena.enter(id, cH);\n\n        // Five minutes before commits close, an attestation issued at exactly commitDeadline - 5 min arrives.\n        vm.warp(commitDeadline - 5 minutes);\n        OracleAttestation.Attestation memory a = OracleAttestation.Attestation({\n            requestId: keccak256(\"req\"),\n            chainId: 1,\n            questionHash: QUESTION,\n            answerType: OracleAttestation.ANSWER_UINT256,\n            answer: abi.encode(uint256(6)), // 6 % 4 + 1 = 3\n            figure: 0,\n            fromBlock: 1,\n            toBlock: 2,\n            blockHash: bytes32(uint256(1)),\n            panelJobId: keccak256(\"job\"),\n            panelSize: 5,\n            quorum: 4,\n            agreed: 5,\n            issuedAt: commitDeadline - 5 minutes,\n            expiresAt: uint64(block.timestamp + 1 days)\n        });\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, adapter.attestationDigest(a));\n        try adapter.submitAttestation(id, a, abi.encodePacked(r, s, v)) {} catch {}\n\n        // Before the commit deadline no round result may be visible as settled.\n        assertFalse(adapter.resultOf(id).settled, \"result readable before the commit deadline\");\n\n        // What the sniper does with it on the current code: read the answer and enter the winning choice.\n        uint8 winning = uint8(adapter.resultOf(id).answer % 4) + 1;\n        bytes32 cS = arena.commitmentOf(id, sniper, winning, keccak256(\"s\"));\n        vm.prank(sniper);\n        arena.enter(id, cS);\n    }\n}","reproduction":"Round id with commitDeadline = T (VaultRaid, 4 choices, prize 300e18), question pinned with notBefore = T. At block.timestamp = T - 5 min, anyone calls submitAttestation(id, a, sig) with a valid signer signature, a.issuedAt = T - 5 min, a.answer = abi.encode(6). Checks: T-5min <= (T-5min)+5min ok; (T-5min)+5min < T is false so IssuedTooEarly does not fire; result stored, resultOf(id).settled == true, answer == 6. A sniper reads it, computes winning = 6 % 4 + 1 = 3, and enter(id, commitmentOf(id, sniper, 3, salt)) succeeds because block.timestamp < T. After reveal and settle the sniper is a guaranteed winner. Expected: resultOf(id).settled is false until T.","severity":"medium","snippet":"        if (a.issuedAt + ISSUED_AT_TOLERANCE < p.notBefore) revert IssuedTooEarly(a.issuedAt, p.notBefore);","title":"OracleAdapter stores a round result as soon as issuedAt + 5 min >= commitDeadline, so a result can be public on chain up to 5 minutes before commits close and entrants can pick the winning choice"},{"citation":"resolved","description":"cancel checks only state == Open and the time. settle and cancel are both permissionless and both valid once block.timestamp >= resultDeadline + 72h if a result exists, so whoever lands first decides the round. A late-arriving result (panel answered after the grace) leaves a pure race; a result arriving shortly before the grace ends gives honest players minutes to settle. A loser who sees an unfavourable result stored on chain simply calls cancel: they get 102 back instead of 90 (or 80), and every correct player loses the prize share they were entitled to (prize returns to the game pool). The brief says to 'cancel unresolved rounds 72 hours after their result deadline'; a round whose result is on file is resolved. Fix: in cancel, revert if oracle.resultOf(roundId).settled is true (and issuedAt passes the same tolerance check settle uses), so a round with a valid result can only be settled.","line":284,"path":"src/Arena.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {Arena, IRoundOracle} from \"src/Arena.sol\";\nimport {OracleAdapter} from \"src/OracleAdapter.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\n\n/// @dev A round whose verified result is already stored can still be cancelled once the 72-hour grace has\n/// passed: `cancel` never looks at the oracle. A losing player calls `cancel` and gets 102 PRIO back\n/// instead of 90, and the winner's prize share disappears. Fails now; passes once `cancel` refuses a\n/// round that has a settled oracle result.\ncontract ArenaCancelResolvedTest is Test {\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n    bytes32 constant QUESTION = keccak256(\"round question\");\n\n    PrismRiotToken token;\n    Arena arena;\n    OracleAdapter adapter;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        token = new PrismRiotToken();\n        arena = new Arena(owner, address(token));\n        adapter = new OracleAdapter(owner, SIGNER);\n        vm.prank(owner);\n        arena.setOracle(IRoundOracle(address(adapter)));\n        token.transfer(alice, 1_000 ether);\n        token.transfer(bob, 1_000 ether);\n        vm.prank(alice);\n        token.approve(address(arena), 102 ether);\n        vm.prank(bob);\n        token.approve(address(arena), 102 ether);\n        token.approve(address(arena), 1_000 ether);\n        arena.fundPrizes(1_000 ether);\n    }\n\n    function test_loserCancelsRoundThatHasAResult() public {\n        uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n        vm.startPrank(owner);\n        uint256 id = arena.createRound(\n            Arena.Mode.FactionDuel, 2, commitDeadline, commitDeadline + 1 hours, commitDeadline + 2 hours, 300 ether, 0, 0\n        );\n        adapter.pinQuestion(id, QUESTION, 1, 5, 4, commitDeadline, \"\");\n        vm.stopPrank();\n\n        bytes32 saltA = keccak256(\"a\");\n        bytes32 saltB = keccak256(\"b\");\n        bytes32 cA = arena.commitmentOf(id, alice, 2, saltA); // will be correct\n        bytes32 cB = arena.commitmentOf(id, bob, 1, saltB); // will be wrong\n        vm.prank(alice);\n        arena.enter(id, cA);\n        vm.prank(bob);\n        arena.enter(id, cB);\n        skip(1 hours);\n        vm.prank(alice);\n        arena.reveal(id, 2, saltA);\n        vm.prank(bob);\n        arena.reveal(id, 1, saltB);\n\n        // The panel answers late: 72 hours after the result deadline (T0+3h), but the result is valid and stored.\n        skip(2 hours + 72 hours);\n        OracleAttestation.Attestation memory a = OracleAttestation.Attestation({\n            requestId: keccak256(\"req\"),\n            chainId: 1,\n            questionHash: QUESTION,\n            answerType: OracleAttestation.ANSWER_UINT256,\n            answer: abi.encode(uint256(1)), // 1 % 2 + 1 = 2: alice correct\n            figure: 0,\n            fromBlock: 1,\n            toBlock: 2,\n            blockHash: bytes32(uint256(1)),\n            panelJobId: keccak256(\"job\"),\n            panelSize: 5,\n            quorum: 4,\n            agreed: 5,\n            issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 days)\n        });\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, adapter.attestationDigest(a));\n        adapter.submitAttestation(id, a, abi.encodePacked(r, s, v));\n        assertTrue(adapter.resultOf(id).settled, \"a verified result exists for the round\");\n\n        // Bob, the loser, cancels before anyone settles. With a result on file this must be refused.\n        vm.prank(bob);\n        try arena.cancel(id) {} catch {}\n        // The round must still be settleable: a round with a verified result is not \"unresolved\".\n        arena.settle(id);\n        assertEq(arena.payoutOf(id, alice), 400 ether, \"winner keeps escrow plus the full prize\");\n        assertEq(arena.payoutOf(id, bob), 90 ether, \"loser gets 90, not a 102 refund\");\n    }\n}","reproduction":"FactionDuel round (choices 2, prize 300e18), commitDeadline T0+1h, revealDeadline T0+2h, resultDeadline T0+3h. Alice commits/reveals 2 (correct), Bob 1 (wrong). At T0+75h (= resultDeadline + 72h) a valid attestation with answer 1 is submitted; resultOf(id).settled == true. Bob calls cancel(id): passes (state Open, time >= T0+75h), state becomes Cancelled, lockedPrizes -= 300e18. settle(id) now reverts NotOpen. payoutOf(id, alice) == 102e18 instead of 400e18; payoutOf(id, bob) == 102e18 instead of 90e18. Expected: cancel reverts while a verified result exists and settle pays alice 400e18, bob 90e18.","severity":"medium","snippet":"        if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();","title":"Arena.cancel ignores an existing verified oracle result: once the 72-hour grace passes, a losing player can cancel a resolved round, converting a 90 PRIO payout into a 102 PRIO refund and erasing the "},{"citation":"resolved","description":"buyPrio/buyImd subtract ethIn from prioBudget/imdBudget before the swap. unlockCallback swaps exact input with sqrtPriceLimitX96 = MIN_SQRT_PRICE + 1 and settles only `owed`, the amount the pool actually consumed. If the pool has no liquidity left below the current price (the launch LP range is finite; a token-only seed range ends at tick 0), Pool.swap moves the price to the limit without consuming the rest of the input, so owed < ethIn. The difference stays as ETH in the treasury, is not re-credited to any budget (reserve, imdBudget, prioBudget, ownerBudget, unallocated all unchanged) and no function can withdraw unaccounted ETH, so it is stranded. Fix: after the swap re-credit (ethIn - owed) to the originating budget, or revert when owed != ethIn.","line":270,"path":"src/FeeTreasury.sol","reproduction":"Seed the hooked pool with PRIO-only liquidity in [minUsableTick, 0] (as Fixture.seedLiquidity(…, false) does) of a size the treasury can exhaust, earn and allocate so prioBudget >= 1 ether, setMaxSpendPerSwap(1 ether). Executor calls buyPrio(1 ether, 1). The swap crosses tick 0 into the empty region and stops at the limit having consumed owed < 1e18; prioBudget decreased by exactly 1e18; address(treasury).balance == reserve + imdBudget + prioBudget + ownerBudget + unallocated + (1e18 - owed). The last term is reachable by no withdraw path. Expected: budgets sum to the contract balance.","severity":"low","snippet":"        if (owed > ethIn) revert ExceedsBudget();","title":"FeeTreasury debits the full ethIn from the budget but only checks owed <= ethIn; when the pool's PRIO liquidity is exhausted the unspent remainder has no budget line and is stuck"}],"hash":"9703a836b3e4b9c418986ca1be0188efb6b8bb14a1d895ba64a2adf572b14227","nodeId":"130db136-192e-4792-995a-b92abad6b870","outcome":"completed","summary":"## Audit Complete\n\nI've completed a math precision, boundary, and numerical gap audit of the PRISM RIOT contracts. The review covered all 65 entry points from the assignment's list.\n\n**Findings:** 5 substantiated defects with concrete reproductions\n\n1. **High severity** — TreasuryFeeHook charges the fee on the requested swap amount in `beforeSwap`, but when a swap hits the swapper's price limit and only partially fills, the full fee is still taken. This can charge nearly 1% effective rate instead of 0.5% of the actual ETH leg exchanged. The brief requires \"0.5% on the ETH leg\" and the README claims \"a partially filled swap pays 0.5% of the partial leg.\" Test proof shows a buy requesting 100 ETH with a 1% price limit fills ~51 ETH but pays a 0.497 ETH fee (0.98% effective rate).\n\n2. **High severity** — StakingVault streams rewards by wall clock even when `totalStaked == 0`. The `rewardPerToken` accumulator doesn't advance while nobody is staked, so those rewards belong to no one and are permanently stranded in the vault with no recovery path. In the normal launch sequence, FeeTreasury calls `notifyReward` before any staker exists, losing rewards proportional to the delay before the first stake. Test proof: 3000 ether funded with no stakers, 15 days pass, first staker arrives and receives only half; the other 1500 ether remains in the vault unclaimable.\n\n3. **Medium severity** — OracleAdapter stores a round result as soon as `issuedAt + 5 min >= commitDeadline`, which admits attestations issued up to 5 minutes before the commit window closes. The stored answer is immediately readable via `resultOf().settled == true` while Arena still accepts entries. A watcher enters the winning choice risk-free. Test proof shows an attestation at T-5min passing validation and being stored before T.\n\n4. **Medium severity** — Arena allows `cancel()` on a round that has a verified oracle result on file. Once 72 hours pass after the result deadline, `cancel` and `settle` race; a losing player calls cancel first, converts their 90 PRIO payout into a 102 PRIO refund, and erases the winners' prize shares. Test proof shows a round with a stored result being cancelled, with winners losing their prize.\n\n5. **Low severity** — FeeTreasury debits the full `ethIn` from the budget before the swap, but `unlockCallback` only checks `owed <= ethIn`. When pool liquidity is exhausted and the swap consumes less than requested, the unspent remainder has no budget line and no recovery path.\n\n**Coverage:** All 65 entry points documented with verdict (holds/finding/unreached) and reason. Zero entry points marked unreached. Four test proofs written under `test/scratch/` demonstrating findings 1-4 fail on the current code as described.\n\nThe findings file `.imd-findings.json` is written with the complete list, reproductions, snippets verified against source line numbers, and coverage array. The existing 54-test project suite passes unchanged.","treeHash":null,"usage":{"cachedInputTokens":3718109,"inputTokens":2422,"model":"claude-fable-5-1","outputTokens":55290,"runtime":"claude","turns":58,"wallClockMs":1595236}},{"artifacts":[],"attempt":1,"bundleHash":"060bdb4e0ebd35afc0468dd20269ebc4507668c8450dfdabc259428e8023c89c","device":"e8816d4386532a66","findings":[],"hash":"bd519b8139a9d7f52b2cae5561559ae28ab633de388a5cb245fc2a4525c641f6","nodeId":"8ca3af35-7fcd-4ec6-b63e-9f6cfed1ebf8","outcome":"completed","summary":"Everything checks out. The only changed path in the working tree is launch.json, and the scratch test is in the ignored test/scratch directory.\n\n**Delivered:** `launch.json` at the repository root describing the accepted PRISM RIOT tree.\n\n- **Discriminator** is the first key, `\"kind\": \"univ4_hook\"`.\n- **Hook** is `TreasuryFeeHook` with constructor arguments `$poolManager`, `$token`, `$factory`, `$owner`, matching the compiled constructor's declaration order (poolManager_, token_, factory_, owner_). No literal addresses or stand-ins appear anywhere.\n- **Permissions** are beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta and afterSwapReturnDelta. A scratch Foundry test deployed the hook and confirmed getHookPermissions returns exactly those five flags and nothing else, with flag mask 0x20CC.\n- **Token** is `PrismRiotToken`, name Prism Riot, symbol PRIO, 18 decimals, no constructor arguments.\n- **Pool** uses the launch's fixed values: native ETH as pairedCurrency, fee 12500, tickSpacing 60, initialPrice 79228162514264337593543950336. The hook's beforeInitialize accepts the 12500 fee and refuses only the dynamic-fee flag, so no fee refusal needed flagging in notes.\n- **Notes** run 3470 characters, under the 4000 cap, and cover fee mechanics, ETH-claim fallback on a token-only seeded pool, pool binding, owner powers and the deployment status.\n\n**Verification:** `forge build` and `forge test` pass with all 54 existing tests. The manifest parses as JSON and its constructor argument count matches the ABI.\n\n**Noted for the reviewer:** no contracts are deployed yet and the prior job broadcast nothing, so there are no on-chain receipts to duplicate. Live addresses come from the deployer after independent review, as the README and notes state.","treeHash":"3402b02fc8c2cd0635b86cd196071658e45c50c0","usage":{"cachedInputTokens":337073,"inputTokens":162,"model":"claude-fable-5-1","outputTokens":5843,"runtime":"claude","turns":13,"wallClockMs":101080}},{"artifacts":[],"attempt":1,"bundleHash":"eaf2a851b22f9678c20dc04f0a252efac8fe4c002ff59ddf1e570db410577ce6","device":"f5666f1d1aa75678","findings":[],"hash":"c2a7eeee2e622e72c32d7271cb3e2e7cff4e3fbda3143e1ebe9229ca381b45a5","nodeId":"219f6226-b512-4d87-8c2b-2421bf0ace9e","outcome":"completed","summary":"All work is complete and verified. Here is the recap.\n\n**Every finding reproduced, and every one is fixed.** All four reviewer proofs failed on the starting tree (6 failing tests) and pass now. The answers are in `.imd-responses.json` with verdict \"fixed\" for all 12 findings, including the advisory ones, which I judged real.\n\n**Contract changes**\n- **TreasuryFeeHook (high):** for the two ETH-specified shapes, `beforeSwap` now runs the pool leg itself through a nested swap at the swapper's limit less one sqrt-price unit, reads the ETH the pool actually exchanged, and charges 0.5% of that. A full fill still costs exactly the specified amount; a partial fill charges only the filled leg, and a seller never pays ETH. Also: `receive` is PoolManager-only, a 1-wei buy reverts instead of being swallowed, and the treasury binding refuses a treasury bound to another hook and stays correctable until the first delivery.\n- **StakingVault (high):** the stream pauses while nothing is staked. Idle time extends `periodFinish`, so the whole funded amount reaches later stakers.\n- **OracleAdapter (medium):** nothing is stored or requested before the pinned `notBefore` on the chain clock. The trusted signer is recorded per pinned question, so rotation only affects future rounds. The payment asset cannot be withdrawn.\n- **Arena (medium x2):** each round snapshots its adapter and question hash, and creation requires the question pinned at exactly the commit deadline. `cancel` refuses a round whose valid result is on file, so after the grace exactly one outcome is live.\n- **FeeTreasury (medium + low):** partial fills credit unspent ETH back to the budget line, purchases are bounded per rolling day and by owner-set price floors, `setImd` unsets the pool key, and sinks are one-shot and non-zero.\n\n**Verification**\n\n| Check | Result |\n| --- | --- |\n| `forge build` | clean |\n| `forge test` | 84 passed (78 suite + 6 proof tests) |\n| `forge fmt --check` | clean |\n| Protected floor tests (hook + token) on new bytecode | 11 of 11 pass |\n| Operator unit tests | 6 of 6 pass |\n\n**Docs and deliverables updated:** README (fee and partial-fill semantics, corrected wei-level text, reordered \"After launch\" steps with the new required price floors and pin-before-create rule, a trust-assumptions section), `docs/REVIEW.md` (round-2 finding-to-fix table), `docs/OPERATOR.md`, regenerated ABIs, and an operator timing guard that refuses a paid request before the commit boundary.\n\n**Not in scope of this tree:** no contract is deployed. This job controls no wallet, so live addresses and receipts still come from the launch's manifest step after the fresh independent review, as the README states.","treeHash":"4040d1af9e38c94b570b7b6aad1e79b37a43618c","usage":{"cachedInputTokens":11793436,"inputTokens":1634,"model":"claude-fable-5-1","outputTokens":134515,"runtime":"claude","turns":126,"wallClockMs":1740947}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c35be49d2f8f8def","findings":[{"citation":"resolved","description":"Access x asymmetry (Trust Gap seam 3/4). createRound freezes mode, choices, deadlines, prize and rulesHash, but settle() reads the live `oracle` storage variable, and Arena.setOracle (src/Arena.sol:147-151) is onlyOwner and re-settable at any time with no open-round check. OracleAdapter.setSigner (src/OracleAdapter.sol:149-151) has the same shape: a new signer can sign an attestation for an already pinned question and anyone may relay it through submitAttestation. Reveals are public from commitDeadline on, so after the reveal window the owner (or whoever holds the owner key) knows every player's choice and can choose the winning choice: point setOracle at a contract whose resultOf returns the wanted answer (or rotate the signer and sign one), then call settle. Wrong players lose 10 PRIO of escrow each and missed reveals 20 PRIO, which flow to the game pool the owner allocates as prizes; the prize goes to the owner's own entries. The brief requires that active rules cannot change and that rounds settle only on verified IMD attestations. Fix: snapshot `oracle` (and, in the adapter, the signer or the pinned question's signer) into the Round at createRound and settle against that snapshot, or refuse setOracle/setSigner while any round is Open (and make pinQuestion record the signer).","line":260,"path":"src/Arena.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {Arena, IRoundOracle} from \"src/Arena.sol\";\n\ncontract FixedOracle is IRoundOracle {\n    uint256 public answer;\n\n    constructor(uint256 answer_) {\n        answer = answer_;\n    }\n\n    function resultOf(uint256) external view returns (Result memory r) {\n        r.answer = answer;\n        r.settled = true;\n        r.issuedAt = uint64(block.timestamp);\n    }\n\n    function ISSUED_AT_TOLERANCE() external pure returns (uint64) {\n        return 5 minutes;\n    }\n}\n\n/// @dev The oracle a round settles against is read from `Arena.oracle` at settle time, not frozen in the\n/// round. After every reveal is public the owner points `setOracle` at a contract returning the answer that\n/// makes the owner's own wallet the winner, then settles. The round's rulesHash and deadlines were frozen;\n/// the result source was not.\ncontract ArenaOracleSwapProof is Test {\n    uint64 constant T0 = 1_800_000_000;\n\n    PrismRiotToken token;\n    Arena arena;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    address ownerWallet = makeAddr(\"ownerWallet\");\n\n    function setUp() public {\n        vm.warp(T0);\n        token = new PrismRiotToken();\n        arena = new Arena(owner, address(token));\n        address[2] memory players = [alice, ownerWallet];\n        for (uint256 i; i < players.length; i++) {\n            token.transfer(players[i], 1_000 ether);\n            vm.prank(players[i]);\n            token.approve(address(arena), 102 ether);\n        }\n        token.approve(address(arena), type(uint256).max);\n        arena.fundPrizes(1_000 ether);\n    }\n\n    function test_ownerCannotPickTheWinnerAfterReveals() public {\n        // The oracle bound when the round opens says answer 0 -> winning choice 1 (alice).\n        FixedOracle honest = new FixedOracle(0);\n        vm.prank(owner);\n        arena.setOracle(IRoundOracle(address(honest)));\n        uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n        vm.prank(owner);\n        uint256 id = arena.createRound(\n            Arena.Mode.FactionDuel, 2, commitDeadline, commitDeadline + 1 hours, commitDeadline + 2 hours, 500 ether, 0, keccak256(\"rules\")\n        );\n        bytes32 salt = keccak256(\"s\");\n        bytes32 cAlice = arena.commitmentOf(id, alice, 1, salt);\n        bytes32 cOwner = arena.commitmentOf(id, ownerWallet, 2, salt);\n        vm.prank(alice);\n        arena.enter(id, cAlice);\n        vm.prank(ownerWallet);\n        arena.enter(id, cOwner);\n        vm.warp(commitDeadline);\n        vm.prank(alice);\n        arena.reveal(id, 1, salt);\n        vm.prank(ownerWallet);\n        arena.reveal(id, 2, salt);\n        vm.warp(commitDeadline + 1 hours);\n\n        // Every reveal is public. The owner swaps the result source for one that says answer 1 -> choice 2.\n        FixedOracle rigged = new FixedOracle(1);\n        vm.prank(owner);\n        try arena.setOracle(IRoundOracle(address(rigged))) {} catch {}\n        arena.settle(id);\n\n        assertEq(arena.rounds(id).winningChoice, 1, \"the round must settle against the oracle it opened with\");\n        assertEq(arena.payoutOf(id, alice), 100 ether + 500 ether, \"alice won under the frozen rules\");\n        assertEq(arena.payoutOf(id, ownerWallet), 90 ether, \"the owner's wallet was wrong\");\n    }\n}","reproduction":"Owner sets oracle A (answer 0 -> winning choice 1). createRound(FactionDuel, 2, now+1h, now+2h, now+3h, 500e18, 0, rules). alice enters with commitment for choice 1, ownerWallet with choice 2; both reveal in the window. At revealDeadline the owner deploys oracle B returning answer 1 (settled=true, issuedAt=now), calls setOracle(B) and settle(id). Expected: round settles on the oracle it opened with (winningChoice 1, alice payout 600e18, ownerWallet 90e18). Actual: winningChoice == 2, alice payoutOf == 90e18, ownerWallet payoutOf == 600e18. Proof test fails with 'the round must settle against the oracle it opened with: 2 != 1'.","severity":"medium","snippet":"        IRoundOracle.Result memory res = oracle.resultOf(roundId);\n        if (!res.settled) revert NoResult();\n        // The same clock tolerance the adapter uses: the answer may not predate the commit boundary.\n        if (uint256(res.issuedAt) + oracle.ISSUED_AT_TOLERANCE() < r.commitDeadline) revert ResultTooEarly();\n        uint8 winning = uint8(res.answer % r.choiceCount) + 1;","title":"Arena settles against whichever oracle/signer the owner points at after reveals: result source is not frozen per round"},{"citation":"resolved","description":"Branch asymmetry between beforeSwap and afterSwap. For PRIO-specified swaps afterSwap computes the fee from delta.amount0(), the ETH the pool really moved. For ETH-specified swaps (exact-input buy, exact-output sell) beforeSwap quotes the fee from params.amountSpecified before the pool runs and returns it as the hook's specified delta, so it is charged in full even when sqrtPriceLimitX96 stops the swap after a fraction of the input (the normal way routers and the v4 Quoter bound slippage). The swapper pays fee on ETH that never entered the pool; for an exact-output sell the same overcharge comes out of the ETH the swapper receives. README 'Quote tick limits' (line 131-134) states the opposite: 'a partially filled swap pays 0.5% of the partial leg'. Fix: for ETH-specified swaps compute the actual ETH leg in afterSwap from delta.amount0() and charge only that (take the specified-side delta there, or refund the difference), or document and enforce that the hook only serves full fills.","line":238,"path":"src/TreasuryFeeHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\n\n/// @dev An ETH-specified swap that a price limit stops early pays 0.5% of the *specified* ETH, not of the\n/// ETH leg the pool actually exchanged: beforeSwap quotes the fee on params.amountSpecified before the\n/// pool runs. README \"Quote tick limits\" promises the partial leg.\ncontract HookPartialFillProof is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint24 constant POOL_FEE = 12500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode,\n            abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: POOL_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n        // Full-range liquidity, both sides.\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        vm.deal(factory, 1_000_000 ether);\n        lpRouter.modifyLiquidity{value: 100_000 ether}(\n            key,\n            ModifyLiquidityParams(\n                TickMath.minUsableTick(TICK_SPACING), TickMath.maxUsableTick(TICK_SPACING), int256(10_000 ether), bytes32(0)\n            ),\n            \"\"\n        );\n        vm.stopPrank();\n    }\n\n    function test_partialFillBuyPaysFeeOnlyOnExchangedLeg() public {\n        vm.deal(trader, 100 ether);\n        uint256 grossIn = 10 ether;\n        // Slippage protection one tick below the current price: the pool stops after a fraction of the input.\n        uint160 limit = TickMath.getSqrtPriceAtTick(-1);\n        vm.startPrank(trader);\n        BalanceDelta d = swapRouter.swap{value: grossIn}(\n            key,\n            SwapParams({zeroForOne: true, amountSpecified: -int256(grossIn), sqrtPriceLimitX96: limit}),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        vm.stopPrank();\n        uint256 ethPaid = uint256(uint128(-d.amount0()));\n        uint256 feeCharged = hook.totalFeeCharged();\n        uint256 poolLeg = ethPaid - feeCharged;\n        assertLt(poolLeg, grossIn / 10, \"the price limit left most of the input unswapped\");\n        // The brief and the README: 0.5% of the ETH the pool exchanged, rounded up to the next wei.\n        assertLe(feeCharged, hook.feeOnLeg(poolLeg), \"fee charged on ETH the pool never exchanged\");\n    }\n}","reproduction":"Pool at 1:1 with 10,000e18 full-range liquidity, treasury bound. Trader swaps zeroForOne, amountSpecified = -10e18, sqrtPriceLimitX96 = TickMath.getSqrtPriceAtTick(-1). Pool exchanges 0.506 ETH (ethPaid - fee). Expected fee <= feeOnLeg(0.506e18) = 2,531,582,281,645,372 wei. Actual hook.totalFeeCharged() == 49,751,243,781,094,528 wei (0.5% of 10 ETH, about 9.8% of the exchanged leg). Proof test fails with 'fee charged on ETH the pool never exchanged: 49751243781094528 > 2531582281645372'.","severity":"medium","snippet":"        if (exactInput) {\n            // Buy with exact ETH in: the pool gets the input minus the fee.\n            (fee, leg) = quoteBuyExactInput(uint256(-params.amountSpecified));\n        } else {\n            // Sell with exact ETH out: the pool pays out the requested ETH plus the fee.\n            (fee, leg) = quoteSellExactOutput(uint256(params.amountSpecified));\n        }\n        _collect(fee, params.zeroForOne, exactInput, leg);\n        return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);","title":"ETH-specified swaps stopped early by a price limit pay 0.5% of the specified ETH, not of the ETH leg the pool exchanged"},{"citation":"resolved","description":"Boundary asymmetry between the adapter and the Arena. The adapter accepts an attestation whose issuedAt is as early as notBefore - ISSUED_AT_TOLERANCE (5 minutes) and nothing in _accept, submitAttestation (permissionless) or request() requires block.timestamp >= notBefore, so the result (and the ResultStored event) is public up to ten minutes before commitDeadline (issuedAt = notBefore - 5min relayed at notBefore - 10min passes `issuedAt > now + TOL`). Arena.enter only checks block.timestamp < commitDeadline, so anyone who reads resultOf(roundId) enters with the winning choice and takes an equal share of the prize, diluting honest winners and converting honest wrong guesses into 10 PRIO penalties. The tolerance was meant for clock skew on the signer side, not as a window in which the answer may be public. Fix: in _accept require block.timestamp >= p.notBefore (store nothing before the commit boundary) and in request() refuse requests before p.notBefore; keep the tolerance only for the issuedAt comparison.","line":248,"path":"src/OracleAdapter.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {Arena, IRoundOracle} from \"src/Arena.sol\";\nimport {OracleAdapter} from \"src/OracleAdapter.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\n\n/// @dev The adapter stores a result as soon as a signed attestation with issuedAt >= notBefore - 5 minutes\n/// is relayed, and `submitAttestation` is permissionless. Nothing requires block.timestamp >= notBefore, so\n/// the winning answer is readable on chain (ResultStored, resultOf) while the Arena still accepts entries.\ncontract EarlyResultProof is Test {\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n    bytes32 constant QUESTION = keccak256(\"which vault holds the prism?\");\n    uint64 constant T0 = 1_800_000_000;\n\n    PrismRiotToken token;\n    Arena arena;\n    OracleAdapter adapter;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    address sniper = makeAddr(\"sniper\");\n\n    function setUp() public {\n        vm.warp(T0);\n        token = new PrismRiotToken();\n        arena = new Arena(owner, address(token));\n        adapter = new OracleAdapter(owner, SIGNER);\n        vm.prank(owner);\n        arena.setOracle(IRoundOracle(address(adapter)));\n        address[2] memory players = [alice, sniper];\n        for (uint256 i; i < players.length; i++) {\n            token.transfer(players[i], 1_000 ether);\n            vm.prank(players[i]);\n            token.approve(address(arena), 102 ether);\n        }\n        token.approve(address(arena), type(uint256).max);\n        arena.fundPrizes(1_000 ether);\n    }\n\n    function attestation(uint256 answer, uint64 issuedAt) internal pure returns (OracleAttestation.Attestation memory a) {\n        a = OracleAttestation.Attestation({\n            requestId: keccak256(abi.encode(\"round\", answer, issuedAt)),\n            chainId: 1,\n            questionHash: QUESTION,\n            answerType: OracleAttestation.ANSWER_UINT256,\n            answer: abi.encode(answer),\n            figure: 0,\n            fromBlock: 100,\n            toBlock: 200,\n            blockHash: bytes32(uint256(7)),\n            panelJobId: bytes32(uint256(9)),\n            panelSize: 5,\n            quorum: 4,\n            agreed: 5,\n            issuedAt: issuedAt,\n            expiresAt: issuedAt + 1 days\n        });\n    }\n\n    function sign(OracleAttestation.Attestation memory a) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, adapter.attestationDigest(a));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function test_resultCannotBeVisibleBeforeCommitDeadline() public {\n        uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n        vm.startPrank(owner);\n        adapter.pinQuestion(1, QUESTION, 1, 5, 4, commitDeadline, \"\");\n        uint256 id = arena.createRound(\n            Arena.Mode.VaultRaid, 4, commitDeadline, commitDeadline + 1 hours, commitDeadline + 2 hours, 500 ether, 0, keccak256(\"rules\")\n        );\n        vm.stopPrank();\n        assertEq(id, 1);\n        bytes32 salt = keccak256(\"s\");\n        bytes32 cAlice = arena.commitmentOf(id, alice, 1, salt);\n        vm.prank(alice);\n        arena.enter(id, cAlice);\n\n        // Five minutes before commits close, the panel's signed answer (2 -> winning choice 3) is relayed.\n        // On the current code it is accepted and stored: anyone can read it and enter with choice 3\n        // (see the sniper flow in the finding). The adapter must not expose a result before notBefore.\n        vm.warp(commitDeadline - 5 minutes);\n        OracleAttestation.Attestation memory early = attestation(2, uint64(block.timestamp));\n        try adapter.submitAttestation(id, early, sign(early)) {} catch {}\n        assertFalse(adapter.resultOf(id).settled, \"answer stored and readable while entries are still open\");\n\n        // The honest flow: the same answer relayed once commits are closed settles the round normally.\n        vm.warp(commitDeadline);\n        OracleAttestation.Attestation memory onTime = attestation(2, uint64(block.timestamp));\n        adapter.submitAttestation(id, onTime, sign(onTime));\n        vm.prank(alice);\n        arena.reveal(id, 1, salt);\n        vm.warp(commitDeadline + 1 hours);\n        arena.settle(id);\n        assertEq(arena.rounds(id).winningChoice, 3);\n        assertEq(arena.payoutOf(id, alice), 90 ether);\n    }\n}","reproduction":"pinQuestion(1, Q, 1, 5, 4, notBefore = commitDeadline); createRound(VaultRaid, 4, commitDeadline, +1h, +2h, 500e18). alice enters choice 1. At commitDeadline - 5 minutes anyone calls submitAttestation(1, a, sig) with a valid signer signature, answer = 2, issuedAt = now. Expected: rejected or not visible until commitDeadline (resultOf(1).settled == false). Actual: stored; resultOf(1).answer == 2 is readable; a sniper enters choice 3 before commitDeadline, reveals, settle() pays the sniper 600e18 and alice 90e18. Proof test fails with 'answer stored and readable while entries are still open'.","severity":"medium","snippet":"        if (a.issuedAt + ISSUED_AT_TOLERANCE < p.notBefore) revert IssuedTooEarly(a.issuedAt, p.notBefore);","title":"A panel result can be stored and read on chain while the Arena still accepts entries (5-minute early window, no notBefore timestamp check)"},{"citation":"resolved","description":"Writer/reader asymmetry on the budget lines. buyPrio does `prioBudget -= ethIn` (src/FeeTreasury.sol:225) and buyImd `imdBudget -= ethIn` (line 243) before the swap, but unlockCallback settles only `owed` = ETH the pool consumed plus the hook fee, and only checks `owed > ethIn`. With a full-range price limit the pool consumes less than the input whenever its liquidity is exhausted (thin or withdrawn liquidity, a launch pool seeded on one side, or a market move), so `ethIn - owed` stays in the contract but belongs to no budget: reserve + imdBudget + prioBudget + ownerBudget + unallocated no longer equals the balance, and FeeTreasury has no sweep or re-credit path, so that fee income is permanently unspendable. Fix: credit `ethIn - owed` back to the budget it was taken from (return `owed` from unlockCallback), or revert when the fill is partial.","line":267,"path":"src/FeeTreasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {PoolIdLibrary} from \"v4-core/src/types/PoolId.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\nimport {Arena} from \"src/Arena.sol\";\n\ncontract StrandedEthProof is Test {\n    using PoolIdLibrary for PoolKey;\n    using StateLibrary for PoolManager;\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint24 constant POOL_FEE = 12500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n    address executor = makeAddr(\"executor\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    StakingVault vault;\n    Arena arena;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode,\n            abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vault = new StakingVault(owner, address(token));\n        arena = new Arena(owner, address(token));\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        treasury.setPrio(address(token));\n        treasury.setSinks(address(vault), address(arena), makeAddr(\"adapter\"));\n        treasury.setExecutor(executor);\n        vault.setRewardFunder(address(treasury));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: POOL_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        vm.deal(factory, 1_000_000 ether);\n        lpRouter.modifyLiquidity{value: 100_000 ether}(\n            key,\n            ModifyLiquidityParams(\n                TickMath.minUsableTick(TICK_SPACING), TickMath.maxUsableTick(TICK_SPACING), int256(10_000 ether), bytes32(0)\n            ),\n            \"\"\n        );\n        vm.stopPrank();\n    }\n\n    function test_budgetDebitedForEthNeverSpent() public {\n        // Income on deep liquidity: 100 ETH buy -> ~0.5 ETH fee -> ~0.135 ETH PRIO budget.\n        vm.deal(trader, 100 ether);\n        vm.startPrank(trader);\n        swapRouter.swap{value: 100 ether}(\n            key,\n            SwapParams({zeroForOne: true, amountSpecified: -int256(100 ether), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        vm.stopPrank();\n        // The LP pulls the deep position and leaves a thin PRIO-only slice (about 0.03 ETH of capacity).\n        vm.startPrank(factory);\n        lpRouter.modifyLiquidity(\n            key,\n            ModifyLiquidityParams(\n                TickMath.minUsableTick(TICK_SPACING), TickMath.maxUsableTick(TICK_SPACING), -int256(10_000 ether), bytes32(0)\n            ),\n            \"\"\n        );\n        (, int24 tick,,) = manager.getSlot0(key.toId());\n        int24 upper = (tick / TICK_SPACING) * TICK_SPACING - TICK_SPACING;\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(upper - 600, upper, int256(1 ether), bytes32(0)), \"\");\n        vm.stopPrank();\n        treasury.allocate();\n        uint256 budget = treasury.prioBudget();\n        assertGt(budget, 0);\n        uint256 balBefore = address(treasury).balance;\n        vm.prank(executor);\n        treasury.buyPrio(budget, 0);\n        uint256 spent = balBefore - address(treasury).balance;\n        emit log_named_uint(\"budget debited\", budget);\n        emit log_named_uint(\"eth actually spent\", spent);\n        uint256 tracked = treasury.reserve() + treasury.imdBudget() + treasury.prioBudget() + treasury.ownerBudget()\n            + treasury.unallocated();\n        emit log_named_uint(\"untracked eth\", address(treasury).balance - tracked);\n        assertEq(address(treasury).balance, tracked, \"ETH left the budgets without leaving the contract\");\n    }\n}","reproduction":"Deep pool, trader buys 100 ETH -> treasury income, allocate(): prioBudget = 134,328,358,208,955,224 wei. LP removes the deep position and leaves 1e18 liquidity in [tick-660, tick-60] (about 0.03 ETH of capacity). Executor calls buyPrio(prioBudget, 0). Expected: contract balance == reserve + imdBudget + prioBudget + ownerBudget + unallocated. Actual: ETH spent = 31,879,041,714,695,502 wei, budget debited 134,328,358,208,955,224 wei, 102,449,316,494,259,722 wei untracked; balance 465,633,396,096,249,772 != tracked 363,184,079,601,990,050. Proof test fails with 'ETH left the budgets without leaving the contract'.","severity":"medium","snippet":"        uint256 owed = uint256(uint128(-delta.amount0()));\n        uint256 out = uint256(uint128(delta.amount1()));\n        // The hook's 0.5% is inside `owed`; a bounded spend never exceeds the budget line.\n        if (owed > ethIn) revert ExceedsBudget();\n        poolManager.settle{value: owed}();\n        poolManager.take(key.currency1, address(this), out);","title":"buyPrio/buyImd debit the full ethIn from the budget while the swap may spend less; the difference is stranded in no budget"},{"citation":"resolved","description":"Access x economics (Trust Gap seam 1) plus an asymmetry with OracleAdapter, whose executor budget is per BUDGET_WINDOW (1 day). maxSpendPerSwap bounds one call; nothing bounds calls per block or per period, and minPrioOut/minImdOut are supplied by the executor with sqrtPriceLimitX96 = MIN_SQRT_PRICE + 1 inside unlockCallback. The brief asks for a budget-limited operator so that a server-side key compromise is bounded; here the executor (or anyone who steals its key) can, in one transaction, push the pool price with its own wallet, loop buyPrio(maxSpendPerSwap, 0) until prioBudget is zero (and the same for buyImd on the IMD pool), then sell back: the treasury receives a fraction of the PRIO its ETH was worth and the rest of the value is captured by the executor's wallet. The owner can only react after the fact. Fix: add a rolling-window spend cap like OracleAdapter's (e.g. maxSpendPerWindow), and an owner-set price floor (minimum PRIO per ETH or a max deviation from the pool price observed in a previous block) so minOut cannot be zero.","line":219,"path":"src/FeeTreasury.sol","reproduction":"Deep 1:1 pool. Trader buys 1,000 ETH -> allocate(): prioBudget = 1,343,283,582,089,552,239 wei, maxSpendPerSwap = 1e18. Executor wallet buys 30,000 ETH of PRIO (price pumped), then in the same block loops: ethIn = min(prioBudget, maxSpendPerSwap); buyPrio(ethIn, 0). Expected: spend in one block bounded (<= one maxSpendPerSwap or a window cap). Actual: two calls succeed and spent == 1,343,283,582,089,552,239 wei, the whole budget, at the manipulated price; the executor then sells its PRIO back. Scratch test test/scratch/ExecutorDrain.t.sol fails with 'one block drained more than one bounded spend of the PRIO budget: 1343283582089552239 > 1000000000000000000'.","severity":"medium","snippet":"    function buyPrio(uint256 ethIn, uint256 minPrioOut) external nonReentrant returns (uint256 out) {\n        if (msg.sender != executor) revert NotExecutor();\n        if (hook == address(0) || address(prio) == address(0)) revert NotConfigured(\"prio\");\n        if (stakingVault == address(0) || arena == address(0)) revert NotConfigured(\"sinks\");\n        if (ethIn > maxSpendPerSwap) revert ExceedsMaxSpend();\n        if (ethIn == 0 || ethIn > prioBudget) revert ExceedsBudget();\n        prioBudget -= ethIn;","title":"Executor budget is per call only and slippage is executor-chosen: a compromised operator key empties the PRIO and IMD budgets in one block by self-sandwich"},{"citation":"resolved","description":"settle/cancel asymmetry. settle() requires a stored result; cancel() only requires state Open and block.timestamp >= resultDeadline + 72h, without checking oracle.resultOf(roundId).settled. Once the grace period passes, settle and cancel race on the same Open round, and cancel refunds 102 PRIO to everyone (including wrong and non-revealing players) and returns the prize to the pool, so a wrong player who lands cancel first erases their 10/20 PRIO penalty and the winners' prize. The brief says to cancel rounds that are 'unresolved'. Fix: in cancel, revert if oracle.resultOf(roundId).settled and the result passes settle's timing check (or simply call settle's path when a result exists).","line":281,"path":"src/Arena.sol","reproduction":"Round with alice correct and bob wrong, valid result stored in the adapter at revealDeadline but nobody calls settle. At resultDeadline + 72h bob calls cancel(id): succeeds, state Cancelled, payoutOf(id, bob) == 102e18 instead of 90e18 and alice gets 102e18 instead of 100e18 + prize. settle(id) afterwards reverts NotOpen.","severity":"low","snippet":"    function cancel(uint256 roundId) external nonReentrant {\n        Round storage r = _rounds[roundId];\n        if (r.state != RoundState.Open) revert NotOpen();\n        if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();","title":"Arena.cancel ignores an available oracle result: a losing player can cancel a settleable round and avoid the penalty"},{"citation":"resolved","description":"Setter/consumer asymmetry. setImdPool freezes currency1 = address(imd) into _imdPoolKey at call time; setImd (src/FeeTreasury.sol:131-135) may be called again afterwards and only rewrites `imd`. buyImd then swaps on the old pool (taking the old token into the treasury) but `imd.safeTransfer(oracleAdapter, out)` moves the new token. With no new-token balance the whole call reverts (IMD purchases disabled until the owner re-calls setImdPool, which nothing tells them to do); with a balance of the new token (any sender can push one), the adapter receives the wrong token and the old IMD stays in FeeTreasury, which has no ERC20 sweep. Fix: have setImd also clear imdPoolSet (or make setImdPool the only writer of imd), and check key.currency1 == imd in buyImd.","line":243,"path":"src/FeeTreasury.sol","reproduction":"Owner: setImd(A); setImdPool(3000, 60, 0); later setImd(B). Executor: buyImd(1e17, 0). Expected: either refuse until the pool matches, or buy B. Actual: swap executes on the ETH/A pool, out tokens of A arrive in FeeTreasury, then IERC20(B).safeTransfer reverts (balance 0) and the call fails; if someone has sent >= out of B to the treasury, B is forwarded and A is stuck in the treasury.","severity":"low","snippet":"        imdBudget -= ethIn;\n        out = _swapEthFor(_imdPoolKey, ethIn, minImdOut);\n        imd.safeTransfer(oracleAdapter, out);\n        emit ImdBought(ethIn, out);","title":"setImd and setImdPool are not coupled: changing IMD after the pool is set makes buyImd swap one token and transfer another"},{"citation":"resolved","description":"Lifecycle asymmetry: FeeTreasury.receive accepts ETH only from its bound hook, but TreasuryFeeHook.bindTreasury accepts any non-zero address once and never checks that the target can receive from this hook. If the owner binds an address that is not the FeeTreasury bound to this hook (a typo, the treasury of another deployment, an EOA-less contract that rejects ETH), flush() fails silently on every swap, pendingEth and claims grow for the life of the pool, and there is no re-bind, no rescue and no alternative recipient: the fee is collected but can never be delivered, which the brief forbids ('never silently remove the fee'). Fix: require FeeTreasury(treasury_).hook() == address(this) in bindTreasury (an interface check), or allow the owner to re-bind while pendingEth remains undeliverable.","line":164,"path":"src/TreasuryFeeHook.sol","reproduction":"Owner calls hook.bindTreasury(X) where X is a FeeTreasury whose bindHook was called with a different address (or any contract without a payable receive). Trader buys 10 ETH: FeeCharged emitted, hook.pendingEth == fee, FeeDelivered never emitted; flush() returns with pendingEth unchanged; bindTreasury(correct) reverts TreasuryAlreadyBound. The ETH is unreachable by any function.","severity":"low","snippet":"    function bindTreasury(address payable treasury_) external onlyOwner {\n        if (treasury != address(0)) revert TreasuryAlreadyBound();\n        if (treasury_ == address(0)) revert ZeroAddress();\n        treasury = treasury_;\n        emit TreasuryBound(treasury_);\n    }","title":"bindTreasury is one-shot and unverified: a wrong or non-receiving address strands every fee forever"},{"citation":"resolved","description":"Documented as a trust assumption, not a bypass: the brief says the paying wallet owns the project. But the README and contract notices present the 30/30/40 split as a property of the system, while only the hook, prio and treasury bindings are one-shot. setSinks is re-settable with no zero or interface check (Slither missing-zero-check), so purchased PRIO can be sent to any contract exposing notifyReward/fundPrizes (an owner-controlled drain); setImdPool accepts any hooks address, so the IMD budget can be routed through an owner-controlled pool; OracleAdapter.withdrawToken (src/OracleAdapter.sol:266-268) lets the owner pull all IMD bought 'for agent work' at any time. Combined with the 40% owner budget the owner can take 100% of fee income after launch. If the split is meant as a guarantee, make the sinks one-shot like setPrio/bindHook, restrict withdrawToken to tokens other than the configured asset, and bound the IMD venue to a known pool key.","line":137,"path":"src/FeeTreasury.sol","reproduction":"After launch the owner deploys Drain with notifyReward(uint256 a){prio.transferFrom(msg.sender,owner,a);} and fundPrizes identical, calls setSinks(Drain, Drain, owner); the executor's next buyPrio sends all purchased PRIO to the owner. Owner also calls adapter.withdrawToken(IMD, owner, balance). No revert; the 30/30 allocations end in the owner's wallet.","severity":"low","snippet":"    function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n        stakingVault = stakingVault_;\n        arena = arena_;\n        oracleAdapter = oracleAdapter_;\n        emit SinksSet(stakingVault_, arena_, oracleAdapter_);\n    }","title":"Trust assumption: the owner can redirect the 30% PRIO and 30% IMD allocations to itself (setSinks, setImdPool, OracleAdapter.withdrawToken)"},{"citation":"resolved","description":"notifyReward commits `amount` to rewardsOwed and starts the clock immediately, but rewardPerToken does not advance while totalStaked == 0, so every second of a stream with no stakers (the first funding after launch, or any gap) is reward that no account can ever earn. It remains in the vault's balance and in rewardsOwed forever: there is no recovery, no re-stream and the overstated rewardsOwed reduces nothing but also never clears. The 'documented schedule' thus silently loses the share of each period that elapses before the first stake. Fix: in notifyReward, if totalStaked == 0 hold the amount as undistributed and start (or restart) the stream on the first stake, or let the funder re-notify the unearned remainder.","line":80,"path":"src/StakingVault.sol","reproduction":"Fresh vault, no stakers. Funder notifyReward(100e18) (rewardsDuration 30 days). Warp 15 days, alice stakes 1e18, warp to periodFinish, alice claim(): receives about 50e18. rewardsOwed stays 50e18, rewardReserve() 50e18, and no call can ever pay or re-stream those 50e18 (a second notifyReward adds to the stream but never the stuck half).","severity":"low","snippet":"    function rewardPerToken() public view returns (uint256) {\n        if (totalStaked == 0) return rewardPerTokenStored;\n        return rewardPerTokenStored + ((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate) / totalStaked;\n    }","title":"Rewards streamed while totalStaked is zero are never distributable and stay counted in rewardsOwed"}],"hash":"dfe6331242c8579205569ae22b0d934368ca9deaed19b80642f4631fa27dfa34","nodeId":"68e15ef0-1795-43c1-a5c2-040ef43e266f","outcome":"completed","summary":"The review is complete. The findings file is at the repository root with 10 findings, 4 proofs and 67 coverage rows; no source files were changed and the only other files are under test/scratch/.\n\n**Outcome.** I audited the permission model (Access Control, Trust Gap, Asymmetry) across all six contracts and found no unguarded state writers or privilege escalations. The defects are in what privileged or paired paths are allowed to do, and in branches that should mirror each other but do not. Five medium findings and five lows, each with a concrete reproduction; four carry a self-contained Foundry test that fails on the current tree for the stated reason.\n\n**Medium findings**\n- **Arena settles against the live oracle, not one frozen per round.** The owner can call `setOracle` after all reveals are public, or rotate the adapter signer, and pick the winning choice. Proof test shows winningChoice 2 instead of 1 and the owner's wallet taking the prize.\n- **Hook overcharges on partially filled ETH-specified swaps.** beforeSwap quotes 0.5% of `amountSpecified` before the pool runs, so a buy stopped by a price limit paid 0.0498 ETH of fee on a 0.506 ETH leg. The README promises the partial leg.\n- **A panel result can be stored and read before the commit deadline.** The 5-minute tolerance plus no `block.timestamp >= notBefore` check lets anyone relay the answer while entries are still open and enter with it.\n- **FeeTreasury debits `ethIn` but settles only what the pool consumed.** With thin liquidity 0.102 ETH of the 0.134 ETH budget stayed in the contract outside every budget line, with no sweep.\n- **Executor budget is per call only, with executor-chosen slippage.** A compromised operator key drained the whole PRIO budget in one block at a self-manipulated price. The OracleAdapter has a daily window; the treasury does not.\n\n**Low findings.** Arena.cancel ignores an available result so a loser can erase penalties; setImd and setImdPool are not coupled; bindTreasury is one-shot and unverified, so a wrong address strands fees forever; the owner can redirect the 30/30 community allocations via setSinks, setImdPool and withdrawToken (reported as a trust assumption); and rewards streamed while nobody is staked are stuck in the vault forever.\n\n**Coverage.** All 65 entry points have a row: 48 hold, 17 reference a finding, none unreached. Two invariant rows were added for Arena fund accounting and the msg.sender-only transferFrom rule. Not covered in depth: the operator Python code and the Intake gas budget, which fall outside my area.","treeHash":null,"usage":{"cachedInputTokens":2683500,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":71317,"runtime":"claude","turns":41,"wallClockMs":974618}},{"artifacts":[],"attempt":1,"bundleHash":"148cda2845325de6817172da22766609f766e5e15bed7253d459ee97375b45ed","device":"d9c671373bfd83b7","findings":[],"hash":"e1517165d047a4a5138ca1c29174f09722f1080df25e017b64b7fb0c157cf0fb","nodeId":"8ca3af35-7fcd-4ec6-b63e-9f6cfed1ebf8","outcome":"completed","summary":"The manifest is updated and verified. Only launch.json changed.\n\n**What I did**\n\n- Checked the existing launch.json against the accepted hook source. The constructor order (`$poolManager`, `$token`, `$factory`, `$owner`), the five declared permissions, the token name and symbol, and the pool values all already matched the implementation.\n- Ran the full Foundry build and test suite. All 78 tests pass.\n- Corrected two stale sentences in the notes. The treasury binding is correctable by the owner until the first fee is delivered and immutable afterward, which the old text described as a one-shot bind. The test count is 78, not 54.\n\n**Verification**\n\n| Check | Result |\n| --- | --- |\n| First key is `\"kind\": \"univ4_hook\"` | yes |\n| Permissions match getHookPermissions | yes, flag mask 0x20CC |\n| Notes length | 3660 of 4000 characters |\n| Files changed | launch.json only |\n| forge build / forge test | pass, 78 tests |\n\nNothing has been deployed, and there are no on-chain receipts to duplicate. Live addresses come from the deployer after independent review.","treeHash":"205acaa8740d2ada4096b95ee9db5874a0cefec5","usage":{"cachedInputTokens":481846,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":4192,"runtime":"claude","turns":10,"wallClockMs":87295}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"0238a59bba722237","findings":[{"citation":"resolved","description":"For the two ETH-specified shapes (zeroForOne exact input = buy with exact ETH in, oneForZero exact output = sell with exact ETH out) beforeSwap fixes the fee from params.amountSpecified and settles it at once through _collect, before the pool runs. Pool.swap stops at the swapper's sqrtPriceLimitX96 or where liquidity ends without reverting and reports only the filled part; Hooks.afterSwap still subtracts the hook's full beforeSwap delta from the swapper. So on any partial fill the fee stays 0.5% of the full request while the real ETH leg is smaller: the effective rate is unbounded as the fill shrinks. For an exact-output sell the fee is deducted from whatever ETH the pool paid, so once the request exceeds the pool's ETH depth the seller's ETH delta goes negative: they hand over PRIO and pay ETH on top (or the sell reverts if the router attached no ETH). This breaks the brief's immutable '0.5% on the ETH leg' and the README 'Quote tick limits' paragraph, which states that a partially filled swap pays 0.5% of the partial leg. Partial fills are ordinary on a thin launch pool (seeded with PRIO only, so ETH depth is only what buyers put in) and with any router-set price limit; no attacker is needed. The two PRIO-specified shapes are computed in afterSwap from delta.amount0() and are correct. Merged from audit_economics, audit_permissions, audit_flow, audit_math and write_foundry_tests (same root cause). Fix: for the ETH-specified shapes compare the actual delta.amount0() in afterSwap with the leg quoted in beforeSwap (transient storage) and revert on a partial fill, or move the fee for those shapes to afterSwap; update the README either way.","line":240,"path":"src/TreasuryFeeHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\n\n/// @dev ETH-specified swaps (buy exact ETH in, sell exact ETH out) have their 0.5% fixed in beforeSwap on\n/// the *requested* amount. When the pool fills only part of it (price limit or liquidity exhausted) the\n/// swapper still pays 0.5% of the full request: far more than 0.5% of the ETH leg the pool exchanged, and\n/// on a sell the swapper can end up paying ETH net. README \"Quote tick limits\" promises the opposite.\ncontract HookPartialFillTest is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint24 constant POOL_FEE = 12500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode,\n            abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: POOL_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        // A thin pool: liquidity only in [-60, +60] around 1:1, both sides.\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        vm.deal(factory, 1_000 ether);\n        lpRouter.modifyLiquidity{value: 10 ether}(\n            key, ModifyLiquidityParams(-60, 60, int256(1e21), bytes32(0)), \"\"\n        );\n        vm.stopPrank();\n\n        vm.deal(trader, 1_000 ether);\n        vm.prank(factory);\n        token.transfer(trader, 10_000_000 ether);\n        vm.prank(trader);\n        token.approve(address(swapRouter), type(uint256).max);\n    }\n\n    function swap(bool zeroForOne, int256 amountSpecified, uint160 limit, uint256 value)\n        internal\n        returns (BalanceDelta d)\n    {\n        vm.prank(trader);\n        d = swapRouter.swap{value: value}(\n            key,\n            SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: limit}),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n    }\n\n    /// Buy with exact 100 ETH in: liquidity ends at tick -60, so the pool exchanges ~3 ETH, but the\n    /// hook charged 0.5% of 100 ETH. README: \"a partially filled swap pays 0.5% of the partial leg\".\n    function test_buyExactInput_partialFill_feeExceedsHalfPercentOfLeg() public {\n        uint256 grossIn = 100 ether;\n        BalanceDelta d = swap(true, -int256(grossIn), TickMath.MIN_SQRT_PRICE + 1, grossIn);\n        uint256 paid = uint256(uint128(-d.amount0()));\n        uint256 fee = treasury.totalIncome();\n        uint256 leg = paid - fee;\n        assertLt(leg, 10 ether, \"the pool filled only a small part of the request\");\n        // What the brief and README promise: 0.5% of the ETH leg actually exchanged, rounded up.\n        assertLe(fee, hook.feeOnLeg(leg), \"fee must be 0.5% of the partial leg\");\n    }\n\n    /// Sell with exact 100 ETH out: the pool can only pay ~3 ETH before tick +60, yet the hook asked for\n    /// 100 ETH * 50 / 9950 ~ 0.5025 ETH of it. The seller still receives at most the pool's ETH minus\n    /// the full fee, i.e. an effective fee far above 0.5%.\n    function test_sellExactOutput_partialFill_feeExceedsHalfPercentOfLeg() public {\n        uint256 wantOut = 100 ether;\n        BalanceDelta d = swap(false, int256(wantOut), TickMath.MAX_SQRT_PRICE - 1, 0);\n        int128 a0 = d.amount0();\n        uint256 fee = treasury.totalIncome();\n        assertGt(fee, 0);\n        // The pool's ETH leg is what the swapper received plus the fee (when the swapper received ETH).\n        uint256 received = a0 > 0 ? uint256(uint128(a0)) : 0;\n        uint256 leg = received + fee;\n        assertLe(fee, hook.feeOnLeg(leg), \"fee must be 0.5% of the partial leg\");\n    }\n\n    /// Sell with exact 1000 ETH out when the pool can pay ~3 ETH: the pool pays 2.995 ETH, the hook keeps\n    /// 5.025 ETH (0.5% of the 1000 asked), so the seller's ETH delta is -2.03 ETH: the seller pays ETH\n    /// *and* PRIO. (Without ETH attached the router cannot settle and the sell reverts outright.)\n    function test_sellExactOutput_partialFill_sellerPaysEthNet() public {\n        uint256 wantOut = 1_000 ether;\n        uint256 prioBefore = token.balanceOf(trader);\n        BalanceDelta d = swap(false, int256(wantOut), TickMath.MAX_SQRT_PRICE - 1, 10 ether);\n        assertLt(token.balanceOf(trader), prioBefore, \"PRIO was sold\");\n        assertGe(d.amount0(), 0, \"a seller must never pay ETH\");\n    }\n}","reproduction":"Pool ETH/PRIO at 1:1 with liquidity 1e21 only in ticks [-60, 60] (about 3 ETH of depth each side), treasury bound. (a) Trader buys exact input 100 ETH with limit MIN_SQRT_PRICE+1: pool exchanges ~3.04 ETH, fee charged = ceil(100e18*50/10050) = 497512437810945274 wei (treasury.totalIncome()); expected <= feeOnLeg(3.04e18) = 15211919305022409. (b) Sell exact output 100 ETH with limit MAX_SQRT_PRICE-1: pool pays ~2.995 ETH, fee 502512562814070352 wei; expected <= 14976774779553905. (c) Sell exact output 1000 ETH with 10 ETH attached: swapper's delta.amount0() = -2029770672229922581 (the seller pays 2.03 ETH and PRIO). Same with a deep full-range pool (10_000e18) and a price limit 0.5% from spot: buy 100 ETH charges 497512437810945274 vs feeOnLeg(actual leg) 254436740665352078. Run: forge test --match-path test/scratch/Proof_c57b3e4bc143.t.sol (3 failing tests).","severity":"high","snippet":"            (fee, leg) = quoteBuyExactInput(uint256(-params.amountSpecified));","title":"Hook fee on ETH-specified swaps is 0.5% of the requested amount, not of the ETH leg the pool exchanged; a partial fill overcharges and an exact-output sell can make the seller pay ETH"},{"citation":"resolved","description":"rewardPerToken() does not advance while totalStaked == 0, but _update() still moves lastUpdateTime forward and periodFinish is fixed at notification, so every second of a stream with no stakers pays rewardRate to nobody. Those PRIO stay in the vault, are still counted in rewardsOwed (claim() is the only decrement), and no function (owner or funder) can recover or re-stream them: notifyReward only re-spreads the remaining seconds of the current stream (leftover = remaining * rewardRate), never the skipped amount. In this launch the sequence is the normal one: FeeTreasury.buyPrio() calls notifyReward() on the vault from the first fee income, before any PRIO holder has staked, and every later window in which all stakers exit loses the same way. The brief's 30% PRIO-for-rewards budget is destroyed in proportion to zero-stake time instead of distributed. Merged from audit_economics, audit_permissions, audit_math and write_foundry_tests. Fix: pause the stream while totalStaked == 0 (do not advance lastUpdateTime; extend periodFinish by the idle time) or accumulate the undistributed amount and fold it into the next notification's total; keep rewardsOwed <= rewardReserve() as the funding check.","line":81,"path":"src/StakingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\n\n/// @dev Rewards stream by wall clock whether or not anyone is staked. With `totalStaked == 0` the\n/// `rewardPerToken` accumulator does not move, so every wei streamed in that window belongs to nobody,\n/// stays counted in `rewardsOwed` forever and has no recovery path. Fails now; passes once streaming\n/// pauses while nothing is staked, or the unearned part is re-queued or recoverable by the funder.\ncontract StakingStrandedTest is Test {\n    PrismRiotToken token;\n    StakingVault vault;\n    address owner = makeAddr(\"owner\");\n    address funder = makeAddr(\"funder\");\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        token = new PrismRiotToken();\n        vault = new StakingVault(owner, address(token));\n        vm.prank(owner);\n        vault.setRewardFunder(funder);\n        token.transfer(alice, 1_000 ether);\n        token.transfer(funder, 10_000 ether);\n        vm.prank(alice);\n        token.approve(address(vault), type(uint256).max);\n        vm.prank(funder);\n        token.approve(address(vault), type(uint256).max);\n    }\n\n    function test_rewardsStreamedWithNoStakersAreLostForever() public {\n        // The treasury's first PRIO purchase funds the vault before anyone has staked.\n        vm.prank(funder);\n        vault.notifyReward(3_000 ether);\n        skip(15 days); // half the stream passes with totalStaked == 0\n\n        vm.prank(alice);\n        vault.stake(100 ether);\n        skip(30 days); // well past periodFinish\n        vm.prank(alice);\n        vault.exit();\n\n        uint256 paid = token.balanceOf(alice) - 1_000 ether;\n        uint256 stranded = token.balanceOf(address(vault)); // nothing is staked any more\n        // Every funded reward must be claimable by someone or recoverable; nothing may be stranded\n        // (allow dust from fixed-point rounding).\n        assertLe(stranded, 1e9, \"half the funded rewards are stranded with no stakers and no recovery path\");\n        assertGe(paid, 3_000 ether - 1e9);\n    }\n}","reproduction":"Funder calls notifyReward(3000e18) with totalStaked == 0 (rewardsDuration 30 days). skip(15 days). alice stakes 100e18, skip(30 days), alice exit(). alice receives ~1500e18; the vault still holds 1500000000000000000100 wei with totalStaked == 0, earned(alice) == 0, rewardsOwed ~1500e18, periodFinish passed. Expected: the full 3000e18 is distributed to stakers or recoverable by the funder; actual: no call can ever move the remaining 1500e18. Run: forge test --match-path test/scratch/Proof_ba4a1eee86c0.t.sol.","severity":"high","snippet":"        if (totalStaked == 0) return rewardPerTokenStored;","title":"StakingVault streams rewards by wall clock while totalStaked == 0; everything streamed in that window is credited to nobody, stays counted in rewardsOwed and is locked in the vault forever"},{"citation":"resolved","description":"_accept only requires issuedAt + 5 min >= notBefore (the round's commit deadline) and _verifyAttestation only requires issuedAt <= now + 5 min; nothing in _accept, the permissionless submitAttestation(), onOracleResult() or request() compares block.timestamp with p.notBefore. An attestation with issuedAt in [commitDeadline - 5 min, commitDeadline) is therefore stored at once, resultOf(roundId).answer is public, and Arena.enter still accepts commitments until commitDeadline. Anyone reading the result enters (answer % choiceCount) + 1, reveals and takes an equal share (against few honest players, the whole) of the prize while honest wrong guesses pay 10 PRIO. request() also has no lower bound, so the executor (operator.py has no timing guard) can pay for the answer while commits are open; an answer earlier than notBefore - 5 min is simply wasted IMD (IssuedTooEarly on the callback). The brief asks for clock tolerance consistent with commit/reveal boundaries; the tolerance was meant for signer clock skew, not as a window in which the answer may be public. Merged from audit_economics, audit_permissions, audit_flow and audit_math. Fix: in _accept require block.timestamp >= p.notBefore (store nothing before the commit boundary on the chain's own clock) and in request() refuse before p.notBefore; keep the issuedAt tolerance as defence in depth.","line":248,"path":"src/OracleAdapter.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {OracleAdapter} from \"src/OracleAdapter.sol\";\nimport {Arena, IRoundOracle} from \"src/Arena.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\n\n/// @dev A validly signed panel answer issued inside the 5-minute tolerance before the round's commit deadline\n/// is stored by the adapter (through the permissionless relay or the intake callback) while Arena.enter still\n/// accepts commitments, so anyone who reads it commits the winning choice. Fails on the current code; passes\n/// once the adapter refuses to store (or expose) a result before `pinned.notBefore` on the chain's own clock.\ncontract ProofEarlyResultTest is Test {\n    address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    bytes32 constant QUESTION = keccak256(\"which vault holds the prism?\");\n\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    address sniper = makeAddr(\"sniper\");\n\n    PrismRiotToken prio;\n    OracleAdapter adapter;\n    Arena arena;\n    uint64 T0;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        T0 = uint64(block.timestamp);\n        prio = new PrismRiotToken();\n        adapter = new OracleAdapter(owner, SIGNER);\n        arena = new Arena(owner, address(prio));\n        vm.prank(owner);\n        arena.setOracle(IRoundOracle(address(adapter)));\n        prio.approve(address(arena), type(uint256).max);\n        arena.fundPrizes(1_000 ether);\n        prio.transfer(alice, 1_000 ether);\n        prio.transfer(sniper, 1_000 ether);\n        vm.prank(alice);\n        prio.approve(address(arena), type(uint256).max);\n        vm.prank(sniper);\n        prio.approve(address(arena), type(uint256).max);\n    }\n\n    function att(uint256 answer, uint64 issuedAt) internal pure returns (OracleAttestation.Attestation memory a) {\n        a = OracleAttestation.Attestation({\n            requestId: keccak256(abi.encode(\"round\", answer, issuedAt)),\n            chainId: 1,\n            questionHash: QUESTION,\n            answerType: OracleAttestation.ANSWER_UINT256,\n            answer: abi.encode(answer),\n            figure: 0,\n            fromBlock: 100,\n            toBlock: 200,\n            blockHash: bytes32(uint256(7)),\n            panelJobId: bytes32(uint256(9)),\n            panelSize: 5,\n            quorum: 4,\n            agreed: 5,\n            issuedAt: issuedAt,\n            expiresAt: issuedAt + 30 days\n        });\n    }\n\n    function sign(OracleAttestation.Attestation memory a) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, adapter.attestationDigest(a));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function enter(address who, uint256 id, uint8 choice) internal returns (bool ok) {\n        bytes32 c = arena.commitmentOf(id, who, choice, bytes32(\"s\"));\n        vm.prank(who);\n        try arena.enter(id, c) {\n            ok = true;\n        } catch {}\n    }\n\n    function test_resultIsNotStoredWhileCommitsAreOpen() public {\n        uint64 commitDeadline = T0 + 1 hours;\n        vm.prank(owner);\n        adapter.pinQuestion(1, QUESTION, 1, 5, 4, commitDeadline, \"\");\n        vm.prank(owner);\n        uint256 id = arena.createRound(\n            Arena.Mode.VaultRaid, 4, commitDeadline, T0 + 2 hours, T0 + 3 hours, 300 ether, 0, bytes32(\"r\")\n        );\n        assertTrue(enter(alice, id, 1));\n\n        // Four minutes before the commit deadline a correctly signed answer appears; anyone may relay it.\n        vm.warp(commitDeadline - 4 minutes);\n        OracleAttestation.Attestation memory a = att(6, uint64(block.timestamp));\n        bytes memory sig = sign(a);\n        try adapter.submitAttestation(id, a, sig) {} catch {}\n\n        // Nothing about the answer may be on chain while Arena.enter still accepts commitments.\n        assertFalse(adapter.resultOf(id).settled, \"answer stored and readable while entries are still open\");\n\n        // And, in consequence, a late entrant must not be able to turn it into a guaranteed win.\n        uint8 winning = uint8(adapter.resultOf(id).answer % 4) + 1;\n        bool entered = enter(sniper, id, winning);\n        if (entered) {\n            vm.warp(commitDeadline + 1);\n            vm.prank(sniper);\n            arena.reveal(id, winning, bytes32(\"s\"));\n            vm.warp(T0 + 2 hours);\n            try arena.settle(id) {} catch {}\n            assertLt(arena.payoutOf(id, sniper), 100 ether + 300 ether, \"late entrant took the whole prize\");\n        }\n    }\n}","reproduction":"Round id 1: createRound(VaultRaid, 4 choices, commitDeadline = T0+1h, reveal T0+2h, result T0+3h, prize 300e18); pinQuestion(1, Q, 1, 5, 4, notBefore = commitDeadline, ''). alice enters choice 1 at T0. At commitDeadline - 4 min anyone calls submitAttestation(1, a, sig) with a valid signer signature, a.issuedAt = now, answer = 6. Checks: issuedAt <= now+5min ok; issuedAt+5min < notBefore is false; stored. Expected: refused or not readable until commitDeadline. Actual: resultOf(1).settled == true, answer 6 -> winning choice 3; a sniper enters choice 3 at commitDeadline - 4 min (enter passes: block.timestamp < commitDeadline), reveals, settle(1), claim(1): sniper balance 1000e18 -> 1298e18 (+298), alice payoutOf == 90e18. Run: forge test --match-path test/scratch/ProofEarlyResult.t.sol.","severity":"medium","snippet":"        if (a.issuedAt + ISSUED_AT_TOLERANCE < p.notBefore) revert IssuedTooEarly(a.issuedAt, p.notBefore);","title":"OracleAdapter stores and publishes a round's answer up to 5 minutes before the commit deadline while Arena.enter still accepts commitments; a late entrant commits the winning choice with certainty"},{"citation":"resolved","description":"buyPrio does prioBudget -= ethIn (line 225) and buyImd imdBudget -= ethIn (line 243) before the swap; unlockCallback swaps exact input to MIN_SQRT_PRICE + 1 and settles only owed = -delta.amount0(), the ETH the pool actually consumed, rejecting only owed > ethIn. When the pool's liquidity below the price is thinner than the spend (a token-only launch seed range that a large buy has exhausted, an LP withdrawal, or simply a thin pool) the swap stops at the limit with owed < ethIn. The unspent ethIn - owed stays in the contract's ETH balance but belongs to no line: it is not unallocated, reserve, imdBudget, prioBudget or ownerBudget, and the only ETH outlets (withdrawOwner, withdrawReserve, the two buys) read those lines. The invariant balance == unallocated + reserve + imdBudget + prioBudget + ownerBudget, which holds on every other path, breaks and that fee income is permanently unspendable. minPrioOut does not prevent it because a partial fill can still exceed a loose minimum. Merged from audit_economics, audit_permissions, audit_flow and audit_math. Fix: return owed from unlockCallback and credit ethIn - owed back to the originating budget, or revert when owed != ethIn.","line":270,"path":"src/FeeTreasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\nimport {Arena} from \"src/Arena.sol\";\n\n/// @dev `buyPrio` debits the whole `ethIn` from `prioBudget` but `unlockCallback` settles only what the pool\n/// consumed (it swaps to MIN_SQRT_PRICE + 1 and only rejects `owed > ethIn`). When the pool's PRIO liquidity\n/// runs out the unspent ETH stays in the contract but on no budget line, and no function can ever move it.\n/// Fails on the current code; passes once the unspent part is credited back (or a partial fill is refused).\ncontract ProofTreasuryStrandedTest is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint24 constant POOL_FEE = 12500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n    address executor = makeAddr(\"executor\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    StakingVault vault;\n    Arena arena;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode, abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vault = new StakingVault(owner, address(token));\n        arena = new Arena(owner, address(token));\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        treasury.setPrio(address(token));\n        treasury.setSinks(address(vault), address(arena), owner);\n        treasury.setExecutor(executor);\n        vault.setRewardFunder(address(treasury));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), POOL_FEE, TICK_SPACING, IHooks(at));\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        token.transfer(trader, 10_000_000 ether);\n        // PRIO-only seed in a bounded range below the price, as a token-only launch seed is.\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(-600, 0, 2e20, bytes32(0)), \"\");\n        vm.stopPrank();\n        vm.deal(trader, 1_000 ether);\n    }\n\n    function swap(bool zeroForOne, int256 amountSpecified, uint256 value) internal returns (BalanceDelta d) {\n        vm.startPrank(trader);\n        token.approve(address(swapRouter), type(uint256).max);\n        d = swapRouter.swap{value: value}(\n            key,\n            SwapParams({\n                zeroForOne: zeroForOne,\n                amountSpecified: amountSpecified,\n                sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1\n            }),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        vm.stopPrank();\n    }\n\n    function tracked() internal view returns (uint256) {\n        return treasury.unallocated() + treasury.reserve() + treasury.imdBudget() + treasury.prioBudget()\n            + treasury.ownerBudget();\n    }\n\n    function test_partialFillKeepsEveryWeiOnABudgetLine() public {\n        // A buyer exhausts the range; the hook's fee on the whole request reaches the treasury as a claim.\n        swap(true, -100 ether, 100 ether);\n        uint256 claims = hook.pendingClaims();\n        if (claims > 0) hook.redeemClaims(claims);\n        hook.flush();\n        treasury.allocate();\n        uint256 budget = treasury.prioBudget();\n        assertGt(budget, 0, \"treasury earned a PRIO budget\");\n        // A small sell restores a little buy-side capacity, far below the budget.\n        swap(false, -0.02 ether, 0);\n        vm.prank(owner);\n        treasury.setMaxSpendPerSwap(budget);\n        assertEq(address(treasury).balance, tracked(), \"consistent before the buy\");\n\n        vm.prank(executor);\n        try treasury.buyPrio(budget, 1) {} catch {}\n\n        // Whether the buy fills, fills partly or is refused, every wei of ETH must stay on a budget line.\n        assertEq(address(treasury).balance, tracked(), \"ETH left every budget line without leaving the contract\");\n    }\n}","reproduction":"Hooked pool seeded PRIO-only in ticks [-600, 0] with liquidity 2e20. Trader buys 100 ETH exact-in (range absorbs ~6 ETH; the hook's fee on the whole request is minted as a claim), redeemClaims + flush deliver it, allocate() gives prioBudget = 134328358208955224 wei. Trader sells 0.02 PRIO to restore ~0.02 ETH of buy-side capacity. Owner setMaxSpendPerSwap(prioBudget); executor buyPrio(prioBudget, 1): out > 0, prioBudget becomes 0, but address(treasury).balance == 476382776726149822 while unallocated+reserve+imdBudget+prioBudget+ownerBudget == 363957225267235285; the 112425551458914537 wei difference is reachable by no function. Expected: balance equals the sum of budget lines after any buy. Run: forge test --match-path test/scratch/ProofTreasuryStranded.t.sol.","severity":"medium","snippet":"        if (owed > ethIn) revert ExceedsBudget();","title":"FeeTreasury.buyPrio/buyImd debit the full ethIn from the budget but unlockCallback settles only what the pool consumed; on a partial fill the difference is stranded outside every budget line with no r"},{"citation":"resolved","description":"createRound() freezes mode, choices, deadlines, prize and rulesHash but records neither the oracle address nor a pinned question; settle() reads the live `oracle` storage variable. Arena.setOracle (line 147) is onlyOwner with no open-round guard, OracleAdapter.setSigner (line 149) rotates the trusted signer at any time, and createRound() does not require OracleAdapter.pinQuestion(roundId) to have happened, so the question can be pinned after reveals. Reveals are public from commitDeadline on, so after the reveal window the owner knows every player's choice and can choose the winning choice: point setOracle at a contract whose resultOf returns the wanted answer (or rotate the signer and sign one), then settle. Wrong players lose 10 PRIO each and the prize goes to the owner's own entries, or returns to the pool the owner allocates. This is an owner power, reported against the brief's explicit 'Active rules ... cannot change' and 'Freeze rules, deadlines, maximum loss and funded prizes before entry' and 'rounds settle only on verified IMD attestations'; there is no unprivileged amplifier. Merged from audit_economics, audit_permissions, audit_flow and write_foundry_tests. Fix: snapshot the oracle address (and require the question to be pinned with notBefore == commitDeadline) into the Round at createRound and settle against that snapshot; refuse setOracle/setSigner while any round is Open, or make the adapter record the signer per pinned question.","line":260,"path":"src/Arena.sol","reproduction":"Owner setOracle(adapter); createRound(FactionDuel, 2, T0+1h, T0+2h, T0+3h, 300e18, 0, rules) -> id. alice enters and reveals 2, bob enters and reveals 1. At T0+2h the owner deploys FakeOracle whose resultOf returns answer 0 (settled, issuedAt = now) -> winning choice 1, calls arena.setOracle(fake) and settle(id). Expected: settle reverts NoResult from the adapter the round opened with, or setOracle is refused while id is Open. Actual: round Settled with winningChoice 1, payoutOf(id, bob) == 400e18, payoutOf(id, alice) == 90e18. Verified in test/scratch/JudgeArenaOracle.t.sol test_ownerSwapsOracleMidRound.","severity":"medium","snippet":"        IRoundOracle.Result memory res = oracle.resultOf(roundId);","title":"A round's result source is not frozen at creation: the owner can swap the Arena's oracle, rotate the adapter's signer or pin the question after players have committed and revealed"},{"citation":"resolved","description":"cancel() checks only state == Open and the time; it never asks oracle.resultOf(roundId).settled. settle() requires only state Open, block.timestamp >= revealDeadline and a stored result, and never checks resultDeadline. So once the grace has passed and a valid result exists (relayed late, or stored earlier but never settled) two mutually exclusive permissionless transitions are available at once: a wrong or non-revealing player calls cancel() to be refunded 102 PRIO instead of 90/80 and to erase every winner's prize share (prize returns to the game pool), while a winner calls settle(). Whoever is mined first decides and a searcher can order it. The brief says to cancel 'unresolved' rounds 72 hours after the result deadline; a round whose result is on file is resolved, and resultDeadline otherwise has no effect in the code. Merged from audit_economics, audit_permissions, audit_flow, audit_math and write_foundry_tests. Fix: in cancel() revert when oracle.resultOf(roundId).settled is true and the result passes settle's issuedAt check (or settle instead), and/or in settle() require block.timestamp < resultDeadline + CANCEL_GRACE so cancellation is the only outcome after the grace.","line":284,"path":"src/Arena.sol","reproduction":"FactionDuel round (2 choices, prize 300e18), commitDeadline T0+1h, revealDeadline T0+2h, resultDeadline T0+3h. alice reveals 2 (correct), bob reveals 1. At T0+3h+72h a valid attestation with answer 1 (winning 2) is submitted; resultOf(id).settled == true. bob calls cancel(id): succeeds, state Cancelled, payoutOf(id, bob) == 102e18 instead of 90e18 and payoutOf(id, alice) == 102e18 instead of 400e18; settle(id) then reverts NotOpen. In the mirror case (settle first, 10 minutes after the grace) settle succeeds and pays alice 400e18, bob 90e18, so both orders are valid today. Expected: one deterministic outcome. Verified in test/scratch/JudgeArenaOracle.t.sol test_cancelAfterGraceWithResultStored and test_settleStillLiveAfterGrace.","severity":"medium","snippet":"        if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();","title":"Arena.cancel ignores a stored oracle result and settle has no upper time bound: after resultDeadline + 72h both are live and transaction ordering decides whether winners are paid or losers refunded in"},{"citation":"resolved","description":"maxSpendPerSwap bounds one call; nothing bounds calls per block or per period (nonReentrant only prevents nesting), and minPrioOut/minImdOut are supplied by the executor with sqrtPriceLimitX96 = MIN_SQRT_PRICE + 1 inside unlockCallback, so minOut = 0 is accepted. OracleAdapter, by contrast, has a rolling BUDGET_WINDOW. The brief asks for a budget-limited operator so that a server-side key compromise is bounded; here the executor (or whoever steals the key) can push the pool price with its own wallet, loop buyPrio(min(prioBudget, maxSpendPerSwap), 0) until the budget is zero (same for buyImd on the IMD pool), then sell back, capturing most of the budget's value. Reported as a trust assumption on the executor key with a weaker-than-intended bound, not as an unprivileged bypass. Fix: add a rolling-window spend cap like OracleAdapter's and an owner-set price floor (minimum PRIO per ETH or a maximum deviation from the pool price of a previous block) so minOut cannot be zero.","line":223,"path":"src/FeeTreasury.sol","reproduction":"Deep 1:1 pool. Trader buys 1000 ETH -> allocate(): prioBudget = 1343283582089552239 wei, maxSpendPerSwap = 1e18. Executor wallet buys 30000 ETH of PRIO (price pumped), then in the same block calls buyPrio(1e18, 0) and buyPrio(343283582089552239, 0): both succeed (each ethIn <= maxSpendPerSwap, each <= prioBudget, minOut 0), total spent in one block = 1343283582089552239 wei, the whole budget, at the manipulated price; the executor then sells its PRIO back. Expected: spend per block/window bounded by an owner-set cap and a price floor.","severity":"low","snippet":"        if (ethIn > maxSpendPerSwap) revert ExceedsMaxSpend();","title":"FeeTreasury executor budget is per call only and minOut is executor-chosen: a compromised operator key can spend the whole PRIO and IMD budgets in one block at a self-manipulated price"},{"citation":"resolved","description":"setImdPool freezes currency1 = address(imd) into _imdPoolKey at call time and sets imdPoolSet; setImd may be called again afterwards and only rewrites `imd`. buyImd then swaps on the stale key (old token arrives in the treasury via poolManager.take(key.currency1, ...)) but executes imd.safeTransfer(oracleAdapter, out) on the new token. With no balance of the new token the call reverts (IMD purchases disabled until the owner re-calls setImdPool, which nothing tells them to do); if the treasury holds enough of the new token (anyone can send some) the adapter receives the wrong token and the old IMD stays in FeeTreasury, which has no ERC-20 sweep. Fix: have setImd clear imdPoolSet (or make setImdPool the only writer of imd) and check Currency.unwrap(key.currency1) == address(imd) in buyImd.","line":133,"path":"src/FeeTreasury.sol","reproduction":"Owner: setImd(A); setImdPool(3000, 60, address(0)); later setImd(B). Executor: buyImd(1e17, 0) with imdBudget >= 1e17. Expected: refuse until the pool matches, or buy B. Actual: the swap executes on the ETH/A pool, `out` tokens of A are taken into FeeTreasury, then IERC20(B).safeTransfer(oracleAdapter, out) reverts with insufficient balance; if someone first sends >= out of B to the treasury, B is forwarded and the A stays stuck.","severity":"low","snippet":"        imd = IERC20(imd_);","title":"setImd and setImdPool are not coupled: calling setImd again after setImdPool makes buyImd swap one token on the old pool and transfer another"},{"citation":"resolved","description":"FeeTreasury.receive accepts ETH only from its bound hook, but TreasuryFeeHook.bindTreasury accepts any non-zero address once and never checks that the target can receive from this hook. If the owner binds an address that is not the FeeTreasury bound to this hook (a typo, another deployment's treasury, a contract without a payable receive), flush() fails silently on every swap (pendingEth is restored and the function returns), pendingEth and claims grow for the life of the pool, and there is no re-bind, rescue or alternative recipient: the fee is collected but can never be delivered, which the brief forbids ('never silently remove the fee'). Fix: require FeeTreasury(treasury_).hook() == address(this) in bindTreasury, or allow the owner to re-bind while pendingEth is undeliverable.","line":167,"path":"src/TreasuryFeeHook.sol","reproduction":"Owner calls hook.bindTreasury(X) where X is a FeeTreasury whose bindHook was called with a different address (receive() reverts NotHook). Trader buys 10 ETH exact-in on a funded pool: FeeCharged emitted, hook.pendingEth() == fee, FeeDelivered never emitted; flush() returns with pendingEth unchanged; bindTreasury(correct) reverts TreasuryAlreadyBound. Expected: the binding is verified or correctable; actual: the ETH is unreachable by any function.","severity":"low","snippet":"        treasury = treasury_;","title":"bindTreasury is one-shot and unverified: a wrong or non-receiving address strands every fee forever"},{"citation":"resolved","description":"receive() is unrestricted so PoolManager.take can pay native ETH to the hook, but any other sender's ETH is accepted too. It is not added to pendingEth, so flush() never forwards it and no other function moves ETH out of the hook: it is permanently stuck. Fix: restrict receive() to msg.sender == address(poolManager), or have flush() forward address(this).balance (every wei the hook holds is fee ETH by design) with pendingEth as the accounting mirror.","line":207,"path":"src/TreasuryFeeHook.sol","reproduction":"vm.deal(alice, 1 ether); alice sends 1 ether to the hook with a plain call: succeeds; hook.pendingEth() unchanged; address(hook).balance == pendingEth + 1 ether; flush() forwards only pendingEth. Expected: refused or forwarded to the treasury; actual: stuck.","severity":"low","snippet":"    receive() external payable {}","title":"TreasuryFeeHook.receive() accepts ETH from anyone; such ETH is never accounted in pendingEth and cannot be recovered"},{"citation":"resolved","description":"The brief says the paying wallet owns the project, so this is documented as a trust assumption rather than a bypass. The README and contract notices present the 30/30/40 split as a property of the system, but only hook, prio and treasury bindings are one-shot: setSinks is re-settable with no zero or interface check (Slither missing-zero-check), so purchased PRIO can be sent to any contract exposing notifyReward/fundPrizes; setImd/setImdPool accept any token and hooks address, so the IMD budget can be routed through an owner-controlled pool; OracleAdapter.withdrawToken (src/OracleAdapter.sol:266) lets the owner pull all IMD bought 'for agent work' at any time. Combined with the 40% owner budget the owner can take 100% of fee income after launch. If the split is meant as a guarantee, make the sinks one-shot like setPrio/bindHook, restrict withdrawToken to tokens other than the configured asset, and reject zero addresses in setSinks.","line":138,"path":"src/FeeTreasury.sol","reproduction":"After launch the owner deploys Drain with notifyReward(uint256 a){prio.transferFrom(msg.sender, owner, a);} and an identical fundPrizes, calls setSinks(Drain, Drain, owner); the executor's next buyPrio(x, 1) forwards all purchased PRIO to the owner. Owner also calls adapter.withdrawToken(IMD, owner, balance). No revert; the 30/30 allocations end in the owner's wallet.","severity":"low","snippet":"        stakingVault = stakingVault_;","title":"Trust assumption: the owner can redirect the 30% PRIO and 30% IMD allocations to itself through the re-settable sinks, IMD venue and OracleAdapter.withdrawToken"},{"citation":"resolved","description":"PoolManager.swap checks amountSpecified == 0 on the swapper's original params (PoolManager.sol:193), before beforeSwap; Pool.swap with the hook-adjusted amountToSwap == 0 returns a zero delta (Pool.sol:320) instead of reverting. For grossIn = 1 wei quoteBuyExactInput gives fee 1, poolLeg 0, so the swap completes with amount0 = -1, amount1 = 0: the whole input is fee. Dust-level, but the README's wei-level rounding text (which the brief asked to be documented) is wrong; inputs up to 200 wei pay a 1-wei fee of >= 0.5%. The manifest notes repeat the claim. Fix: correct the documentation (or revert in beforeSwap when poolLeg == 0).","line":113,"path":"README.md","reproduction":"Full-range funded pool. PoolSwapTest.swap{value: 1}(key, {zeroForOne: true, amountSpecified: -1, sqrtPriceLimitX96: MIN_SQRT_PRICE+1}). Expected per README: revert. Actual: success; delta (-1, 0); hook.totalFeeCharged() == 1. Verified in test/scratch/JudgeTreasury.t.sol test_oneWeiBuyExecutesAllFee.","severity":"info","snippet":"Views for quoting: `feeOnLeg`, `quoteBuyExactInput`, `quoteSellExactOutput`. A 1-wei buy cannot","title":"README says a 1-wei buy cannot execute because the pool refuses a zero swap; it executes, the swapper pays the wei as fee and receives nothing"}],"hash":"ec0da0bcab7542a17392b40337aba5b109999805a6588f2e36b840c0df8e0719","nodeId":"c8234b0a-239e-4496-a946-4c0a50e330b8","outcome":"completed","summary":"The review is complete. The findings file is in place and validated.\n\n**Outcome:** `.imd-findings.json` holds 12 findings and a 66-row coverage record answering all 65 listed entry points plus a manifest invariant. Every snippet and line was checked against the tree, and the four attached proofs each fail on the current code under `forge test`.\n\n**Findings kept, after reproducing each against the code and merging duplicates across the five reports:**\n\n- **High, hook partial fills.** For ETH-specified swaps the fee is fixed in `beforeSwap` on the requested amount, so a partial fill overcharges and an exact-output sell can make the seller pay ETH net. Reported by all five specialists; merged into one. Proof attached (three failing tests).\n- **High, stranded staking rewards.** Rewards streamed while nobody is staked are credited to nobody and locked forever, and the launch's first `buyPrio` triggers exactly this. Four reports merged. Proof attached.\n- **Medium, early oracle result.** A signed answer issued within the 5-minute tolerance before the commit deadline is stored and public while entries are open; a late entrant takes the prize. Proof attached.\n- **Medium, treasury partial-fill stranding.** `buyPrio`/`buyImd` debit the whole spend but settle only what the pool took; the remainder sits on no budget line. Proof attached.\n- **Medium, unfrozen result source.** Owner can swap the oracle, rotate the signer or pin the question for an open round. Reported as a violation of the brief's \"active rules cannot change\".\n- **Medium, cancel versus settle race.** After the 72-hour grace both calls are live when a result exists, so ordering decides who is paid.\n- **Low:** per-call-only executor cap with executor-chosen slippage, `setImd`/`setImdPool` decoupling, unverified one-shot `bindTreasury`, unrestricted hook `receive()`, and the owner's ability to redirect the 30/30 allocations as a trust assumption.\n- **Info:** the README's claim that a 1-wei buy cannot execute is wrong. Confirmed by test and by reading v4-core.\n\n**Dropped or folded:** the one-wei quote deviation and the `exit()` usability edge were left out as non-defects. The manifest checks out: constructor order, permissions, fee tier, notes length and bytecode size all hold.\n\nNo project files were changed. Scratch tests live only under `test/scratch/`.","treeHash":null,"usage":{"cachedInputTokens":2109041,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":49275,"runtime":"claude","turns":38,"wallClockMs":612384}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"72b617d4b615473a","findings":[{"citation":"resolved","description":"For the two ETH-specified shapes (buy with exact ETH in, sell with exact ETH out) the fee is fixed in beforeSwap from params.amountSpecified, before the pool runs. The pool then fills only as much as its liquidity and the swapper's sqrtPriceLimitX96 allow, so whenever the fill is partial the fee stays at 0.5% of the full request while the actual ETH leg is smaller. The immutable guarantee the brief and README state (0.5% of the ETH leg; README 'Quote tick limits': 'a partially filled swap pays 0.5% of the partial leg') is broken in the treasury's favour and at the swapper's expense. On a sell with exact ETH out the hook's fee is deducted from whatever the pool actually paid, so once the request exceeds the pool's ETH depth the seller's ETH delta goes negative: they hand over PRIO and pay ETH on top (and if their router attaches no ETH the sell reverts). Partial fills are ordinary v4 behaviour on a thin launch pool (the pool is seeded with PRIO only, so its ETH depth is only what buyers have put in) and with any router-set price limit; no attacker is needed, the loss is the swapper's and the gain is the treasury's. The PRIO-specified shapes are computed in afterSwap from the real delta and are correct. Fix: in afterSwap for ETH-specified swaps, recompute the leg from delta.amount0() (|amount0| minus the fee for buys, |amount0| plus the fee for sells) and revert if feeOnLeg(actualLeg) is below the fee already taken, so a partially filled ETH-specified swap reverts instead of overpaying (the specified-currency delta cannot be corrected in afterSwap); update the README accordingly. A variant that keeps partial fills must take the fee in afterSwap, which v4 only allows on the unspecified currency, so it would change the fee currency and is not compatible with the brief.","line":240,"path":"src/TreasuryFeeHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\n\n/// @dev ETH-specified swaps (buy exact ETH in, sell exact ETH out) have their 0.5% fixed in beforeSwap on\n/// the *requested* amount. When the pool fills only part of it (price limit or liquidity exhausted) the\n/// swapper still pays 0.5% of the full request: far more than 0.5% of the ETH leg the pool exchanged, and\n/// on a sell the swapper can end up paying ETH net. README \"Quote tick limits\" promises the opposite.\ncontract HookPartialFillTest is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint24 constant POOL_FEE = 12500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode,\n            abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: POOL_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        // A thin pool: liquidity only in [-60, +60] around 1:1, both sides.\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        vm.deal(factory, 1_000 ether);\n        lpRouter.modifyLiquidity{value: 10 ether}(\n            key, ModifyLiquidityParams(-60, 60, int256(1e21), bytes32(0)), \"\"\n        );\n        vm.stopPrank();\n\n        vm.deal(trader, 1_000 ether);\n        vm.prank(factory);\n        token.transfer(trader, 10_000_000 ether);\n        vm.prank(trader);\n        token.approve(address(swapRouter), type(uint256).max);\n    }\n\n    function swap(bool zeroForOne, int256 amountSpecified, uint160 limit, uint256 value)\n        internal\n        returns (BalanceDelta d)\n    {\n        vm.prank(trader);\n        d = swapRouter.swap{value: value}(\n            key,\n            SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: limit}),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n    }\n\n    /// Buy with exact 100 ETH in: liquidity ends at tick -60, so the pool exchanges ~3 ETH, but the\n    /// hook charged 0.5% of 100 ETH. README: \"a partially filled swap pays 0.5% of the partial leg\".\n    function test_buyExactInput_partialFill_feeExceedsHalfPercentOfLeg() public {\n        uint256 grossIn = 100 ether;\n        BalanceDelta d = swap(true, -int256(grossIn), TickMath.MIN_SQRT_PRICE + 1, grossIn);\n        uint256 paid = uint256(uint128(-d.amount0()));\n        uint256 fee = treasury.totalIncome();\n        uint256 leg = paid - fee;\n        assertLt(leg, 10 ether, \"the pool filled only a small part of the request\");\n        // What the brief and README promise: 0.5% of the ETH leg actually exchanged, rounded up.\n        assertLe(fee, hook.feeOnLeg(leg), \"fee must be 0.5% of the partial leg\");\n    }\n\n    /// Sell with exact 100 ETH out: the pool can only pay ~3 ETH before tick +60, yet the hook asked for\n    /// 100 ETH * 50 / 9950 ~ 0.5025 ETH of it. The seller still receives at most the pool's ETH minus\n    /// the full fee, i.e. an effective fee far above 0.5%.\n    function test_sellExactOutput_partialFill_feeExceedsHalfPercentOfLeg() public {\n        uint256 wantOut = 100 ether;\n        BalanceDelta d = swap(false, int256(wantOut), TickMath.MAX_SQRT_PRICE - 1, 0);\n        int128 a0 = d.amount0();\n        uint256 fee = treasury.totalIncome();\n        assertGt(fee, 0);\n        // The pool's ETH leg is what the swapper received plus the fee (when the swapper received ETH).\n        uint256 received = a0 > 0 ? uint256(uint128(a0)) : 0;\n        uint256 leg = received + fee;\n        assertLe(fee, hook.feeOnLeg(leg), \"fee must be 0.5% of the partial leg\");\n    }\n\n    /// Sell with exact 1000 ETH out when the pool can pay ~3 ETH: the pool pays 2.995 ETH, the hook keeps\n    /// 5.025 ETH (0.5% of the 1000 asked), so the seller's ETH delta is -2.03 ETH: the seller pays ETH\n    /// *and* PRIO. (Without ETH attached the router cannot settle and the sell reverts outright.)\n    function test_sellExactOutput_partialFill_sellerPaysEthNet() public {\n        uint256 wantOut = 1_000 ether;\n        uint256 prioBefore = token.balanceOf(trader);\n        BalanceDelta d = swap(false, int256(wantOut), TickMath.MAX_SQRT_PRICE - 1, 10 ether);\n        assertLt(token.balanceOf(trader), prioBefore, \"PRIO was sold\");\n        assertGe(d.amount0(), 0, \"a seller must never pay ETH\");\n    }\n}","reproduction":"Pool ETH/PRIO at 1:1 with liquidity 1e21 only in ticks [-60, 60] (about 3 ETH of depth each side). (a) Buy exact input 100 ETH with the default MIN price limit: pool exchanges 3.04 ETH, treasury receives 0.4975 ETH (ceil(100e18*50/10050)), i.e. 16% of the leg; expected ceil(3.04e18*50/10000) = 0.0152 ETH. (b) Sell exact output 100 ETH with the default MAX limit: pool pays 2.995 ETH, fee 0.5025 ETH; expected 0.015 ETH. (c) Sell exact output 1000 ETH: pool pays 2.995 ETH, fee 5.025 ETH, swapper's amount0 = -2.03 ETH: the seller pays 2.03 ETH and 3.04 PRIO. Proof: test/scratch/HookPartialFill.t.sol (3 failing tests).","severity":"high","snippet":"        if (exactInput) {\n            // Buy with exact ETH in: the pool gets the input minus the fee.\n            (fee, leg) = quoteBuyExactInput(uint256(-params.amountSpecified));\n        } else {\n            // Sell with exact ETH out: the pool pays out the requested ETH plus the fee.\n            (fee, leg) = quoteSellExactOutput(uint256(params.amountSpecified));\n        }","title":"ETH-specified swaps are charged 0.5% of the requested amount, not of the ETH leg the pool exchanged; a partial fill overcharges and can make a seller pay ETH"},{"citation":"resolved","description":"Synthetix-style accounting skips every second in which totalStaked is zero: rewardPerToken() returns the stored value and _update() still advances lastUpdateTime, so the reward for that interval is paid to no one. notifyReward() only re-spreads the remaining time of the current stream (leftover = remaining seconds * rewardRate), never the skipped amount, and rewardsOwed only falls on claim(), so the skipped PRIO is counted as owed forever and no function (no sweep, no re-notify of undistributed rewards) can move it. This is the launch's normal order of events: FeeTreasury.buyPrio() notifies the vault from the first fee income, before any PRIO holder has staked, and every interval with zero stakers afterwards has the same effect. The brief's 30% PRIO-for-rewards budget is partly destroyed rather than distributed. Fix: track undistributed reward (accumulate (dt * rewardRate)/PRECISION whenever totalStaked == 0 in _update) and fold it into the next notifyReward() as extra 'leftover', or let notifyReward re-stream the difference rewardReserve() - rewardsOwed; keep rewardsOwed <= rewardReserve() as the funding check.","line":81,"path":"src/StakingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\n\n/// @dev Rewards that stream while `totalStaked == 0` are credited to nobody, stay counted in `rewardsOwed`\n/// and can never be claimed, re-streamed or recovered: PRIO bought with fee income is locked forever.\ncontract VaultStrandedTest is Test {\n    PrismRiotToken token;\n    StakingVault vault;\n    address owner = makeAddr(\"owner\");\n    address funder = makeAddr(\"funder\");\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        token = new PrismRiotToken();\n        vault = new StakingVault(owner, address(token));\n        vm.prank(owner);\n        vault.setRewardFunder(funder);\n        token.transfer(funder, 10_000 ether);\n        token.transfer(alice, 1_000 ether);\n        vm.prank(funder);\n        token.approve(address(vault), type(uint256).max);\n        vm.prank(alice);\n        token.approve(address(vault), type(uint256).max);\n    }\n\n    /// The treasury funds 3000 PRIO over 30 days before anyone stakes (the launch order: fees come in,\n    /// the executor buys PRIO, stakers arrive later). Alice stakes after 15 days and is the only staker\n    /// for the rest of the period: she earns ~1500, the other ~1500 are stranded in the vault.\n    function test_rewardsDuringZeroStakeAreStrandedForever() public {\n        vm.prank(funder);\n        vault.notifyReward(3_000 ether);\n        skip(15 days);\n        vm.prank(alice);\n        vault.stake(100 ether);\n        skip(30 days);\n        vm.prank(alice);\n        vault.exit();\n        // Nobody holds a stake, nobody has an unclaimed reward, the stream is over.\n        assertEq(vault.totalStaked(), 0);\n        assertEq(vault.earned(alice), 0);\n        assertGe(block.timestamp, vault.periodFinish());\n        uint256 locked = token.balanceOf(address(vault));\n        // Expected: every funded reward is either distributed or still distributable. Actual: ~1500 PRIO\n        // sit in the vault, counted as owed, with no path that will ever pay or re-stream them.\n        assertLt(locked, 1 ether, \"funded rewards are stranded in the vault\");\n    }\n}","reproduction":"Treasury (rewardFunder) calls notifyReward(3000e18) at T with nobody staked; alice stakes 100e18 at T+15d; at T+45d alice exit()s. She received ~1500e18; totalStaked == 0, earned(alice) == 0, periodFinish passed, yet the vault still holds 1500e18 PRIO (rewardsOwed == 1500e18) that no call can ever pay out or re-stream. Proof: test/scratch/VaultStranded.t.sol.","severity":"medium","snippet":"    function rewardPerToken() public view returns (uint256) {\n        if (totalStaked == 0) return rewardPerTokenStored;\n        return rewardPerTokenStored + ((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate) / totalStaked;\n    }","title":"Rewards that stream while totalStaked == 0 are credited to nobody, stay counted in rewardsOwed and are locked in the vault forever"},{"citation":"resolved","description":"buyPrio()/buyImd() subtract the full ethIn from prioBudget/imdBudget before the swap, while unlockCallback() settles only owed = -delta.amount0(), the amount the pool actually consumed up to the MIN_SQRT_PRICE limit. When liquidity near the price is thinner than the spend (an LP withdrew, or the pool is simply thin), owed < ethIn and the difference stays in the contract's balance but in no accounting line: it is not unallocated, reserve, imdBudget, prioBudget or ownerBudget, and the only ETH outlets (withdrawOwner, withdrawReserve, the two buys) read those lines. The invariant 'balance == unallocated + reserve + imdBudget + prioBudget + ownerBudget' that the rest of the contract relies on breaks and the ETH is lost to the project permanently. Fix: in unlockCallback return owed alongside out, and in buyPrio/buyImd credit ethIn - owed back to the budget it came from (or revert when owed != ethIn, since a partially filled treasury buy is also a bad execution).","line":270,"path":"src/FeeTreasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {PoolIdLibrary} from \"v4-core/src/types/PoolId.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {TreasuryFeeHook} from \"src/TreasuryFeeHook.sol\";\nimport {FeeTreasury} from \"src/FeeTreasury.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\nimport {Arena} from \"src/Arena.sol\";\n\n/// @dev `FeeTreasury.buyPrio(ethIn, ...)` deducts `ethIn` from `prioBudget` but settles only what the pool\n/// actually took (`owed`). When the pool fills the order partially, `ethIn - owed` stays in the contract\n/// outside every budget line and no function can ever move it.\ncontract TreasuryStrandedTest is Test {\n    using PoolIdLibrary for PoolKey;\n\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint160 constant HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG\n        | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG;\n\n    address owner = makeAddr(\"owner\");\n    address factory = makeAddr(\"factory\");\n    address trader = makeAddr(\"trader\");\n    address executor = makeAddr(\"executor\");\n\n    PoolManager manager;\n    PrismRiotToken token;\n    TreasuryFeeHook hook;\n    FeeTreasury treasury;\n    StakingVault vault;\n    Arena arena;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        vm.prank(factory);\n        token = new PrismRiotToken();\n        address at = address((uint160(uint256(keccak256(\"hook\"))) & ~uint160(0x3FFF)) | HOOK_FLAGS);\n        bytes memory creation = abi.encodePacked(\n            type(TreasuryFeeHook).creationCode,\n            abi.encode(IPoolManager(address(manager)), address(token), factory, owner)\n        );\n        vm.etch(at, creation);\n        (bool ok, bytes memory runtime) = at.call(\"\");\n        require(ok, \"hook constructor reverted\");\n        vm.etch(at, runtime);\n        hook = TreasuryFeeHook(payable(at));\n        treasury = new FeeTreasury(IPoolManager(address(manager)), owner);\n        vault = new StakingVault(owner, address(token));\n        arena = new Arena(owner, address(token));\n        vm.startPrank(owner);\n        hook.bindTreasury(payable(address(treasury)));\n        treasury.bindHook(address(hook));\n        treasury.setPrio(address(token));\n        treasury.setSinks(address(vault), address(arena), makeAddr(\"adapter\"));\n        treasury.setExecutor(executor);\n        vault.setRewardFunder(address(treasury));\n        vm.stopPrank();\n        swapRouter = new PoolSwapTest(IPoolManager(address(manager)));\n        lpRouter = new PoolModifyLiquidityTest(IPoolManager(address(manager)));\n        key = PoolKey({\n            currency0: CurrencyLibrary.ADDRESS_ZERO,\n            currency1: Currency.wrap(address(token)),\n            fee: 12500,\n            tickSpacing: 60,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n        // Launch liquidity: PRIO only, below the current price, over the full range.\n        int24 minTick = TickMath.minUsableTick(60);\n        vm.startPrank(factory);\n        token.approve(address(lpRouter), type(uint256).max);\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(minTick, 0, int256(1e22), bytes32(0)), \"\");\n        vm.stopPrank();\n        // Earn fees: a buyer sends 100 ETH; the hook collects 0.5% as a claim (manager held no ETH).\n        vm.deal(trader, 1_000 ether);\n        vm.startPrank(trader);\n        swapRouter.swap{value: 100 ether}(\n            key,\n            SwapParams({zeroForOne: true, amountSpecified: -100 ether, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        vm.stopPrank();\n        hook.redeemClaims(hook.pendingClaims());\n        treasury.allocate();\n        // The LP withdraws the wide position and leaves only a thin PRIO-only range right below the price:\n        // the state of a pool whose liquidity is mostly gone when the executor buys.\n        (, int24 tick,,) = _slot0();\n        int24 upper = tick >= 0 ? (tick / 60) * 60 : -((-tick + 59) / 60) * 60;\n        vm.startPrank(factory);\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(minTick, 0, -int256(1e22), bytes32(0)), \"\");\n        lpRouter.modifyLiquidity(key, ModifyLiquidityParams(upper - 60, upper, int256(1e19), bytes32(0)), \"\");\n        vm.stopPrank();\n    }\n\n    function _slot0() internal view returns (uint160 sqrtPriceX96, int24 tick, uint24 pf, uint24 lpf) {\n        return StateLibrary.getSlot0(IPoolManager(address(manager)), key.toId());\n    }\n\n    function tracked() internal view returns (uint256) {\n        return treasury.unallocated() + treasury.reserve() + treasury.imdBudget() + treasury.prioBudget()\n            + treasury.ownerBudget();\n    }\n\n    function test_partialFillStrandsEthOutsideEveryBudget() public {\n        uint256 budget = treasury.prioBudget();\n        assertGt(budget, 0.01 ether, \"there is PRIO budget to spend\");\n        assertEq(address(treasury).balance, tracked(), \"fully accounted before the buy\");\n        vm.prank(owner);\n        treasury.setMaxSpendPerSwap(budget);\n        vm.prank(executor);\n        treasury.buyPrio(budget, 1);\n        assertEq(treasury.prioBudget(), 0, \"the whole budget line was consumed\");\n        // Expected: every wei the treasury holds is in some budget line. Actual: the unfilled part of the\n        // spend is held by the contract with no line and no withdrawal path.\n        assertEq(address(treasury).balance, tracked(), \"ETH outside every budget is stranded\");\n    }\n}","reproduction":"Pool seeded PRIO-only full range (1e22), a buyer swaps 100 ETH in, hook claim redeemed, allocate() gives prioBudget = 0.1343 ETH; the LP removes its wide position and leaves 1e19 liquidity in the 60-tick range just below the price. owner setMaxSpendPerSwap(prioBudget); executor buyPrio(0.1343e18, 1): the swap consumes 0.0315 ETH (price runs to MIN), prioBudget becomes 0, and treasury balance (0.466 ETH) exceeds the sum of all lines (0.363 ETH) by 0.103 ETH with no function able to move it. Proof: test/scratch/TreasuryStranded.t.sol.","severity":"medium","snippet":"        uint256 owed = uint256(uint128(-delta.amount0()));\n        uint256 out = uint256(uint128(delta.amount1()));\n        // The hook's 0.5% is inside `owed`; a bounded spend never exceeds the budget line.\n        if (owed > ethIn) revert ExceedsBudget();\n        poolManager.settle{value: owed}();","title":"FeeTreasury deducts ethIn from the budget but settles only what the pool took; a partial fill leaves ETH outside every budget line with no recovery path"},{"citation":"resolved","description":"The pinned notBefore (the round's commit deadline) is only applied to the attestation's issuedAt, with the 5-minute clock tolerance, and never to when request() may be sent. The executor can therefore pay for the panel answer while commits are open. If the panel answers earlier than notBefore - 5 min the price is simply wasted (the result is refused, a second request is needed: budget loss, no income means no retry). If it answers inside the last 5 minutes before the commit deadline the result is accepted, stored in _results and readable by anyone through resultOf() while enter() is still open, so any wallet commits the winning choice and takes an equal (or, against few honest players, the whole) share of the prize; the same holds for a submitAttestation relay of such an attestation. The commit-reveal guarantee the brief asks for ('specify clock tolerance consistently with commit/reveal boundaries') is defeated by the tolerance being open on the early side at the moment that matters. Fix: in request() require block.timestamp >= p.notBefore (and, since the Arena already re-checks issuedAt at settle, keep the tolerance there only); optionally require issuedAt >= notBefore with no tolerance in _accept, which also stops a relayed early attestation.","line":190,"path":"src/OracleAdapter.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 {PrismRiotToken} from \"src/PrismRiotToken.sol\";\nimport {Arena, IRoundOracle} from \"src/Arena.sol\";\nimport {OracleAdapter, IIntake} from \"src/OracleAdapter.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\n\ncontract Intake is IIntake {\n    Callback public lastCallback;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function request(bytes32, bytes calldata, Callback calldata callback, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 requestId)\n    {\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        lastCallback = callback;\n        requestId = keccak256(\"intake-1\");\n    }\n\n    function deliver(bytes memory data) external returns (bool ok) {\n        (ok,) = lastCallback.target.call{gas: 200_000}(abi.encodePacked(lastCallback.selector, data));\n    }\n}\n\n/// @dev `OracleAdapter.request` has no lower time bound. The executor may buy the answer before the\n/// round's commit deadline; an answer issued inside the 5-minute tolerance is accepted and stored, and\n/// anyone can read it and enter the still-open round with the winning choice.\ncontract OracleEarlyAnswerTest is Test {\n    uint256 constant SIGNER_KEY = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;\n    address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n    bytes32 constant QUESTION = keccak256(\"round question\");\n    uint64 constant T0 = 1_800_000_000;\n\n    PrismRiotToken token;\n    PrismRiotToken imd;\n    Arena arena;\n    OracleAdapter adapter;\n    Intake intake;\n    address owner = makeAddr(\"owner\");\n    address executor = makeAddr(\"executor\");\n    address alice = makeAddr(\"alice\");\n    address attacker = makeAddr(\"attacker\");\n\n    function setUp() public {\n        vm.warp(T0);\n        token = new PrismRiotToken();\n        imd = new PrismRiotToken();\n        arena = new Arena(owner, address(token));\n        adapter = new OracleAdapter(owner, SIGNER);\n        intake = new Intake();\n        imd.transfer(address(adapter), 10 ether);\n        vm.startPrank(owner);\n        arena.setOracle(IRoundOracle(address(adapter)));\n        adapter.setIntake(IIntake(address(intake)));\n        adapter.setAction(keccak256(\"oracle.request\"));\n        adapter.setPayment(address(imd), 0.5 ether);\n        adapter.setCallbackConfigured(true);\n        adapter.setExecutor(executor);\n        adapter.setBudget(10 ether);\n        vm.stopPrank();\n        token.transfer(alice, 1_000 ether);\n        token.transfer(attacker, 1_000 ether);\n        vm.prank(alice);\n        token.approve(address(arena), type(uint256).max);\n        vm.prank(attacker);\n        token.approve(address(arena), type(uint256).max);\n        token.approve(address(arena), type(uint256).max);\n        arena.fundPrizes(1_000 ether);\n    }\n\n    function attestation(uint256 answer, uint64 issuedAt) internal pure returns (OracleAttestation.Attestation memory a) {\n        a = OracleAttestation.Attestation({\n            requestId: keccak256(\"req-1\"),\n            chainId: 1,\n            questionHash: QUESTION,\n            answerType: OracleAttestation.ANSWER_UINT256,\n            answer: abi.encode(answer),\n            figure: 0,\n            fromBlock: 1,\n            toBlock: 2,\n            blockHash: bytes32(uint256(1)),\n            panelJobId: keccak256(\"job-1\"),\n            panelSize: 5,\n            quorum: 4,\n            agreed: 5,\n            issuedAt: issuedAt,\n            expiresAt: issuedAt + 1 days\n        });\n    }\n\n    function test_answerBoughtBeforeCommitDeadlineIsPublicWhileCommitsAreOpen() public {\n        uint64 commitDeadline = T0 + 1 hours;\n        vm.startPrank(owner);\n        uint256 id = arena.createRound(\n            Arena.Mode.VaultRaid, 4, commitDeadline, commitDeadline + 1 hours, commitDeadline + 2 hours, 1_000 ether, 0, bytes32(0)\n        );\n        adapter.pinQuestion(id, QUESTION, 1, 5, 4, commitDeadline, \"\");\n        vm.stopPrank();\n\n        // Alice commits honestly to vault 1 early in the window.\n        bytes32 saltA = keccak256(\"alice\");\n        bytes32 cA = arena.commitmentOf(id, alice, 1, saltA);\n        vm.prank(alice);\n        arena.enter(id, cA);\n\n        // The operator requests the answer 10 minutes before commits close. Expected: refused until the\n        // commit boundary. Actual: accepted and paid.\n        vm.warp(commitDeadline - 10 minutes);\n        vm.prank(executor);\n        adapter.request(id);\n\n        // The panel answers 4 minutes before the deadline; within tolerance, so the adapter stores it.\n        vm.warp(commitDeadline - 4 minutes);\n        OracleAttestation.Attestation memory a = attestation(2, uint64(block.timestamp)); // winning choice 3\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, adapter.attestationDigest(a));\n        bool delivered = intake.deliver(abi.encode(keccak256(\"intake-1\"), a, abi.encodePacked(r, s, v)));\n        assertTrue(delivered);\n        assertTrue(adapter.resultOf(id).settled, \"the answer is on chain while commits are still open\");\n\n        // Anyone reads it and enters with the winning choice before the deadline.\n        uint8 winning = uint8(adapter.resultOf(id).answer % 4) + 1;\n        bytes32 saltX = keccak256(\"attacker\");\n        bytes32 cX = arena.commitmentOf(id, attacker, winning, saltX);\n        vm.prank(attacker);\n        arena.enter(id, cX);\n\n        vm.warp(commitDeadline + 1);\n        vm.prank(alice);\n        arena.reveal(id, 1, saltA);\n        vm.prank(attacker);\n        arena.reveal(id, winning, saltX);\n        vm.warp(commitDeadline + 1 hours);\n        arena.settle(id);\n        vm.prank(attacker);\n        arena.claim(id);\n        // Expected: an answer cannot exist before the commit boundary, so the late entrant could not\n        // know it. Actual: the attacker takes the whole 1000 PRIO prize.\n        assertLt(token.balanceOf(attacker), 1_000 ether + 500 ether, \"late entrant wins with a known answer\");\n    }\n}","reproduction":"Round with commitDeadline = T+1h, pinQuestion(id, Q, 1, 5, 4, T+1h). alice enters choice 1 at T. At T+50min executor calls request(id): succeeds, 0.5 IMD spent. At T+56min the intake delivers a valid attestation (answer 2, issuedAt = T+56min): accepted, resultOf(id).settled == true. attacker reads the answer, enters choice (2 % 4)+1 = 3 at T+57min (commits still open), reveals after T+1h, settle(), claim(): the attacker is the only correct player, so they get 100 escrow + the entire 1000 PRIO prize back for 102 paid; balance goes from 1000e18 to 1998e18 (+998 PRIO) while alice, who committed honestly, loses 12 PRIO. Expected: request() refused before T+1h, no result can exist while commits are open. Proof: test/scratch/OracleEarlyAnswer.t.sol.","severity":"medium","snippet":"        Pinned storage p = _pinned[roundId];\n        if (p.questionHash == bytes32(0)) revert QuestionNotPinned(roundId);\n        if (_results[roundId].settled) revert AlreadySettled(roundId);","title":"OracleAdapter.request has no lower time bound: an answer bought before the commit deadline is stored on chain inside the 5-minute tolerance and lets late entrants commit the winning choice"},{"citation":"resolved","description":"createRound() freezes mode, choices, deadlines, prize and rulesHash but records neither the oracle address nor a pinned question. settle() reads the current `oracle` and the adapter's current pinned question for the round; setOracle() has no 'no open rounds' guard and createRound() does not require the question to be pinned first, so the owner can change which adapter (and which signer) decides an open round, or pin the question after reveals. This is an owner power, reported as a trust assumption against the brief's explicit 'active rules ... cannot change' and 'freeze rules ... before entry'; there is no unprivileged amplifier. Fix: store the oracle address in the Round at creation and use it in settle(); require OracleAdapter.pinned(roundId).questionHash != 0 (and its notBefore == commitDeadline) in createRound(), or refuse setOracle() while any round is Open.","line":149,"path":"src/Arena.sol","reproduction":"Owner createRound(...) (id 1), players enter and reveal; owner deploys a second OracleAdapter with a signer they control, setOracle(adapter2), pinQuestion on adapter2 for round 1, submits an attestation choosing a winning choice held by their own wallet; settle(1) pays the prize to that wallet. Expected: the oracle and question the players entered under decide the round.","severity":"low","snippet":"    function setOracle(IRoundOracle to) external onlyOwner {\n        if (address(to) == address(0)) revert ZeroAddress();\n        oracle = to;\n        emit OracleSet(address(to));","title":"Settlement inputs of an open round are not frozen: the owner can swap the Arena's oracle or pin the round's question after entries are in"},{"citation":"resolved","description":"settle() only requires state Open and timestamp >= revealDeadline; cancel() requires timestamp >= resultDeadline + 72h. Both are permissionless, so once the grace has passed a result that arrives late (or was stored just before the grace ended but not yet settled) makes the round's outcome depend on which call lands first: wrong-choice and no-reveal players prefer cancel (102 PRIO back instead of 90 or 80), winners prefer settle (prize). The brief's 'cancel unresolved rounds 72 hours after their result deadline' reads as a deterministic rule; here resultDeadline otherwise has no effect. Fix: refuse settle() once block.timestamp >= resultDeadline + CANCEL_GRACE (or once the result's issuedAt is past resultDeadline), so cancellation is the only outcome after the grace.","line":259,"path":"src/Arena.sol","reproduction":"Round 1 with resultDeadline = D; no result until D + 72h + 10 min when a valid attestation is relayed. In the same block bob (wrong choice) sends cancel(1) and alice (correct) sends settle(1); if cancel is first, alice gets 102 and bob 102; if settle is first, alice gets 100 + prize and bob 90. Both orders are valid today.","severity":"low","snippet":"        if (r.state != RoundState.Open) revert NotOpen();\n        if (block.timestamp < r.revealDeadline) revert RevealNotOver();\n        IRoundOracle.Result memory res = oracle.resultOf(roundId);\n        if (!res.settled) revert NoResult();","title":"settle() has no upper time bound, so after resultDeadline + 72h a settle and a cancel race and transaction ordering decides who gets paid"}],"hash":"fb2523231cf87ff72d7adaddaf909a6784d79777637ef1e26272ad170dc64295","nodeId":"9e8b0343-07b1-4044-96ba-4a4df149530d","outcome":"completed","summary":"The review is complete. The findings file is at the repository root, no tracked file was modified, and the only other writes are under test/scratch/, which the harness discards. The file is excluded from git by the harness's own exclude entry, which is expected.\n\n**What I found (6 findings, 4 with failing Foundry proofs under test/scratch/):**\n\n1. **High, TreasuryFeeHook.beforeSwap.** For ETH-specified swaps the 0.5% is fixed on the requested amount before the pool runs. On a partial fill the swapper pays 0.5% of the full request, not of the ETH leg. Measured on a thin pool: a 100 ETH buy that filled 3.04 ETH paid 0.4975 ETH of fee, which is 16% of the leg. A 1000 ETH exact-output sell left the seller with an ETH delta of -2.03 ETH, so they paid ETH and PRIO. The README states the opposite guarantee. Proposed fix is to recompute the leg in afterSwap and revert partially filled ETH-specified swaps.\n\n2. **Medium, StakingVault.** Rewards streamed while nobody is staked are credited to no one, stay in rewardsOwed, and no function can ever pay or re-stream them. Funding before the first staker is the launch's normal order. Proof strands 1500 of 3000 PRIO.\n\n3. **Medium, FeeTreasury.unlockCallback.** Buys deduct the full ethIn from the budget but settle only what the pool took. The difference sits outside every budget line with no withdrawal path. Proof strands 0.103 ETH of a 0.134 ETH buy.\n\n4. **Medium, OracleAdapter.request.** No lower time bound against the pinned notBefore. An answer bought and delivered inside the 5-minute tolerance before the commit deadline is stored and public while entries are open. Proof: a late entrant reads it and takes the whole 1000 PRIO prize.\n\n5. **Low, Arena.** The oracle address and the pinned question are not frozen at round creation. Owner-only, reported as a trust assumption against the brief's frozen-rules requirement.\n\n6. **Low, Arena.settle.** No upper time bound, so after the 72-hour grace a settle and a cancel race on transaction ordering.\n\n**Coverage:** all 65 listed entry points plus 3 invariant rows are answered. 54 hold and 14 point at a finding. None are unreached.\n\n**Verification:** the existing suite passes on the unmodified tree, and the four proof files fail on it for the stated reasons. The two low findings carry call-sequence reproductions but no test.\n\n**Not covered:** the server operator's HTTP flows and the manifest were outside the economics scope and I did not audit them beyond reading when the operator requests an oracle answer.","treeHash":null,"usage":{"cachedInputTokens":2611496,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":68110,"runtime":"claude","turns":57,"wallClockMs":859403}}],"verification":[{"checks":[{"durationMs":3804,"exitCode":0,"name":"build","output":"Compiling 118 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.65s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> script/Deploy.s.sol:63:5:\n   |\n63 |     function run() external {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/StakingVault.sol:62:30\n   │\n62 │     function setRewardFunder(address funder) external onlyOwner {\n   │                              ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/StakingVault.sol:77:16\n   │\n77 │         return block.timestamp < periodFinish ? block.timestamp : periodFinish;\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/OracleAdapter.sol:138:26\n    │\n138 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/TreasuryFeeHook.sol:179:22\n    │\n179 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:46\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                                              ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/TreasuryFeeHook.sol:179:22\n    │\n179 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:62\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                                                              ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:185:9\n    │\n185 │         emit FeeDelivered(treasury, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:23\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                       ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:144:26\n    │\n144 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/TreasuryFeeHook.sol:193:9\n    │\n193 │         poolManager.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:277:22\n    │\n277 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/FeeTreasury.sol:200:83\n    │\n200 │     function withdrawOwner(address payable to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                                   ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:204:9\n    │\n204 │         emit OwnerWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:193:13\n    │\n193 │         if (block.timestamp >= windowStart + BUDGET_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:204:9\n    │\n204 │         emit Requested(roundId, intakeId, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:209:44\n    │\n209 │         if (pendingSince[intakeId] == 0 || block.timestamp < pendingSince[intakeId] + REQUEST_TIMEOUT) {\n    │                                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:168:15\n    │\n168 │         if (!(block.timestamp < commitDeadline && commitDeadline < revealDeadline && revealDeadline < resultDeadline)) {\n    │               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:228:13\n    │\n228 │         if (block.timestamp >= r.commitDeadline) revert CommitClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:242:13\n    │\n242 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:242:51\n    │\n242 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/StakingVault.sol:139:13\n    │\n139 │         if (block.timestamp < periodFinish) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:259:13\n    │\n259 │         if (block.timestamp < r.revealDeadline) revert RevealNotOver();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Arena.sol:264:25\n    │\n264 │         uint8 winning = uint8(res.answer % r.choiceCount) + 1;\n    │                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:203:9\n    │\n203 │         emit ClaimsRedeemed(amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:284:13\n    │\n284 │         if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:213:9\n    │\n213 │         emit ReserveWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:230:9\n    │\n230 │         IRewardSink(stakingVault).notifyReward(toStaking);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:232:9\n    │\n232 │         IPrizeSink(arena).fundPrizes(toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:252:31\n    │\n252 │         bytes memory result = poolManager.unlock(abi.encode(key, ethIn));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:233:9\n    │\n233 │         emit PrioBought(ethIn, out, toStaking, toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:240:45\n    │\n240 │             (fee, leg) = quoteBuyExactInput(uint256(-params.amountSpecified));\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:243:47\n    │\n243 │             (fee, leg) = quoteSellExactOutput(uint256(params.amountSpecified));\n    │                                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:246:63\n    │\n246 │         return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);\n    │                                                               ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:246:70\n    │\n246 │         return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);\n    │                                                                      ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:246:9\n    │\n246 │         emit ImdBought(ethIn, out);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:262:9\n    │\n262 │         emit ResultStored(roundId, answer, a.requestId, a.panelJobId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:38\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:46\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                              ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:68\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:76\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                            ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:266:44\n    │\n266 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:266:51\n    │\n266 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                                   ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:263:53\n    │\n263 │                 zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:267:24\n    │\n267 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:267:32\n    │\n267 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:268:23\n    │\n268 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:268:31\n    │\n268 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/FeeTreasury.sol:271:9\n    │\n271 │         poolManager.settle{value: owed}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:341:9\n    │\n341 │         emit FeeCharged(poolId, zeroForOne, exactInput, leg, fee, asClaim);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/FeeTreasury.sol:277:22\n    │\n277 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/Arena.t.sol:49:40\n    │\n 49 │         uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n    │                                        ━━━━━━━━━━━━━━━\n    │\n    ⸬  lib/forge-std/src/StdCheats.sol:677:9\n    │\n677 │         vm.warp(vm.getBlockTimestamp() + time);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":174,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/PrismRiotToken.t.sol:PrismRiotTokenTest\n[PASS] test_metadataAndSupply() (gas: 44137)\n[PASS] test_noMintOrOwnerFunctions() (gas: 51960)\n[PASS] test_transferMovesExactly() (gas: 78817)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.19ms (316.78µs CPU time)\n\nRan 6 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] test_durationBounds() (gas: 67910)\n[PASS] test_leftoverRollsIntoNextStream() (gas: 431893)\n[PASS] test_noRewardsWithoutFunding() (gas: 209489)\n[PASS] test_onlyFunderOrOwnerNotifies() (gas: 41455)\n[PASS] test_rewardsProRataByStakeAndTime_andStopAtPeriodEnd() (gas: 646737)\n[PASS] test_stakeWithdrawKeepsPrincipalExact() (gas: 251959)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 1.40ms (929.23µs CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployAllPlacesHookOnFlaggedAddress() (gas: 23245771)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.69ms (1.05ms CPU time)\n\nRan 3 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookClaimsTest\n[PASS] test_buyOnTokenOnlyPoolMintsClaimThenRedeems() (gas: 651315)\n[PASS] test_redeemRefusesMoreThanHeld() (gas: 69782)\n[PASS] test_secondBuyPaysDirectlyOnceManagerHoldsEth() (gas: 740396)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 3.00ms (1.03ms CPU time)\n\nRan 10 tests for test/OracleAdapter.t.sol:OracleAdapterTest\n[PASS] test_budgetAndExecutorEnforced() (gas: 952569)\n[PASS] test_callbackRefusesWrongSenderAndUnknownId() (gas: 683201)\n[PASS] test_callbackSelectorIsCanonical() (gas: 507)\n[PASS] test_clearStaleAfterTimeout() (gas: 657965)\n[PASS] test_digestMatchesTheProtocol() (gas: 10880)\n[PASS] test_manualRelayStoresResultAndEvidence() (gas: 225809)\n[PASS] test_paidRequestDisabledUntilConfigured() (gas: 99204)\n[PASS] test_paidRequestThenCallbackUnder200kGas() (gas: 854250)\n[PASS] test_relayRejectsReplayAndSecondResult() (gas: 393159)\n[PASS] test_relayRejectsWrongQuestionChainQuorumPanelSignerExpiryAndType() (gas: 610171)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 8.03ms (23.88ms CPU time)\n\nRan 9 tests for test/Arena.t.sol:ArenaTest\n[PASS] test_bossChallengeNeedsThreshold() (gas: 1011509)\n[PASS] test_cancelAfter72hRefundsEverything() (gas: 1095210)\n[PASS] test_constantsPublished() (gas: 16377)\n[PASS] test_entryRules() (gas: 653340)\n[PASS] test_noWinnerReturnsPrizeToPool() (gas: 828434)\n[PASS] test_oldRoundStaysClaimableAfterNewRounds() (gas: 1279521)\n[PASS] test_resultIssuedBeforeCommitBoundaryCannotSettle() (gas: 429691)\n[PASS] test_roundCreationRules() (gas: 132179)\n[PASS] test_vaultRaidFullLifecycle() (gas: 1620787)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 8.13ms (38.25ms CPU time)\n\nRan 14 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookTest\n[PASS] testFuzz_feeChargedMatchesTreasuryIncome(uint96,bool,bool) (runs: 256, μ: 372273, ~: 366767)\n[PASS] testFuzz_quotesAreConsistentWeiLevel(uint128) (runs: 256, μ: 22585, ~: 22585)\n[PASS] test_buyExactInput_feeFromEthInput() (gas: 374625)\n[PASS] test_buyExactOutput_feeOnTopOfEthCharged() (gas: 376464)\n[PASS] test_callbacksRefuseNonManager() (gas: 123093)\n[PASS] test_constructorRefusesMismatchedAddress() (gas: 71766)\n[PASS] test_feeOnLegRoundsUpAtWeiLevel() (gas: 22980)\n[PASS] test_initializeOnFreshHookRefusesWrongPair() (gas: 340433)\n[PASS] test_initializeRefusesNonFactoryAndOtherPools() (gas: 47998)\n[PASS] test_otherDirectionsLeaveTokenTransfersUntaxed() (gas: 74556)\n[PASS] test_permissionsAndAddressAgree() (gas: 31486)\n[PASS] test_sellExactInput_feeOutOfEthOutput() (gas: 343952)\n[PASS] test_sellExactOutput_swapperGetsExactlyTheEthAsked() (gas: 350366)\n[PASS] test_treasuryBindsOnce() (gas: 63780)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 33.13ms (37.12ms CPU time)\n\nRan 7 tests for test/FeeTreasury.t.sol:FeeTreasuryTest\n[PASS] test_allocationSplitAndReserveCap() (gas: 543549)\n[PASS] test_buyPrioGuards() (gas: 842894)\n[PASS] test_buyPrioSplitsBetweenStakingAndArena() (gas: 953462)\n[PASS] test_imdPurchaseWaitsForConfiguration_prioIndependent() (gas: 904523)\n[PASS] test_onlyHookCanFund_noOwnerAdvances() (gas: 43633)\n[PASS] test_ownerAndReserveWithdrawalsAreCapped() (gas: 673836)\n[PASS] test_reserveStopsAtTarget() (gas: 840621)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 33.18ms (3.29ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:InvariantsTest\n[PASS]\nInvariantsTest invariants:\n[PASS] invariant_arenaBalanceIsFullyAccounted\n[PASS] invariant_vaultCoversPrincipalAndOwedRewards\n InvariantsTest invariants (runs: 32, calls: 1024, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 272   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | enter    | 153   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | fund     | 151   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | stake    | 145   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | warp     | 145   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 158   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 71.95ms (70.66ms CPU time)\n\nRan 9 test suites in 73.15ms (161.70ms CPU time): 54 tests passed, 0 failed, 0 skipped (54 total tests)\n","passed":true},{"durationMs":66,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Arena.acceptOwnership()\",\"Arena.cancel(uint256)\",\"Arena.claim(uint256)\",\"Arena.createRound(uint8,uint8,uint64,uint64,uint64,uint256,uint16,bytes32)\",\"Arena.enter(uint256,bytes32)\",\"Arena.fundPrizes(uint256)\",\"Arena.refund(uint256)\",\"Arena.reveal(uint256,uint8,bytes32)\",\"Arena.setOracle(address)\",\"Arena.settle(uint256)\",\"Arena.transferOwnership(address)\",\"FeeTreasury.acceptOwnership()\",\"FeeTreasury.allocate()\",\"FeeTreasury.bindHook(address)\",\"FeeTreasury.buyImd(uint256,uint256)\",\"FeeTreasury.buyPrio(uint256,uint256)\",\"FeeTreasury.receive()\",\"FeeTreasury.setExecutor(address)\",\"FeeTreasury.setImd(address)\",\"FeeTreasury.setImdPool(uint24,int24,address)\",\"FeeTreasury.setMaxSpendPerSwap(uint256)\",\"FeeTreasury.setPrio(address)\",\"FeeTreasury.setReserveTarget(uint256)\",\"FeeTreasury.setSinks(address,address,address)\",\"FeeTreasury.transferOwnership(address)\",\"FeeTreasury.unlockCallback(bytes)\",\"FeeTreasury.withdrawOwner(address,uint256)\",\"FeeTreasury.withdrawReserve(address,uint256)\",\"OracleAdapter.acceptOwnership()\",\"OracleAdapter.clearStale(bytes32)\",\"OracleAdapter.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.pinQuestion(uint256,bytes32,uint256,uint16,uint16,uint64,bytes)\",\"OracleAdapter.request(uint256)\",\"OracleAdapter.setAction(bytes32)\",\"OracleAdapter.setBudget(uint256)\",\"OracleAdapter.setCallbackConfigured(bool)\",\"OracleAdapter.setExecutor(address)\",\"OracleAdapter.setIntake(address)\",\"OracleAdapter.setPayment(address,uint256)\",\"OracleAdapter.setSigner(address)\",\"OracleAdapter.submitAttestation(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.transferOwnership(address)\",\"OracleAdapter.withdrawToken(address,address,uint256)\",\"PrismRiotToken.approve(address,uint256)\",\"PrismRiotToken.transfer(address,uint256)\",\"PrismRiotToken.transferFrom(address,address,uint256)\",\"StakingVault.acceptOwnership()\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.notifyReward(uint256)\",\"StakingVault.setRewardFunder(address)\",\"StakingVault.setRewardsDuration(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.transferOwnership(address)\",\"StakingVault.withdraw(uint256)\",\"TreasuryFeeHook.acceptOwnership()\",\"TreasuryFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"TreasuryFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"TreasuryFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"TreasuryFeeHook.bindTreasury(address)\",\"TreasuryFeeHook.flush()\",\"TreasuryFeeHook.receive()\",\"TreasuryFeeHook.redeemClaims(uint256)\",\"TreasuryFeeHook.transferOwnership(address)\",\"TreasuryFeeHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":9,\"LICENSE\":9,\"README.md\":254,\"docs/OPERATOR.md\":49,\"docs/REVIEW.md\":51,\"docs/abi/Arena.json\":1016,\"docs/abi/FeeTreasury.json\":1073,\"docs/abi/OracleAdapter.json\":1429,\"docs/abi/PrismRiotToken.json\":328,\"docs/abi/StakingVault.json\":627,\"docs/abi/TreasuryFeeHook.json\":1505,\"foundry.toml\":27,\"operator/operator.example.json\":19,\"operator/operator.py\":279,\"operator/test_operator.py\":94,\"remappings.txt\":6,\"script/Deploy.s.sol\":66,\"src/Arena.sol\":321,\"src/FeeTreasury.sol\":280,\"src/OracleAdapter.sol\":269,\"src/OracleAttestation.sol\":125,\"src/PrismRiotToken.sol\":16,\"src/StakingVault.sol\":160,\"src/TreasuryFeeHook.sol\":343,\"src/TwoStepOwned.sol\":19,\"test/Arena.t.sol\":360,\"test/Deploy.t.sol\":23,\"test/FeeTreasury.t.sol\":133,\"test/Invariants.t.sol\":142,\"test/OracleAdapter.t.sol\":265,\"test/PrismRiotToken.t.sol\":37,\"test/StakingVault.t.sol\":104,\"test/TreasuryFeeHook.t.sol\":260,\"test/utils/Fixture.sol\":117,\"test/utils/MockIntake.sol\":36},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3543,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/FeeTreasury.sol:276: FeeTreasury._send(address,uint256) (src/FeeTreasury.sol#276-279) sends eth to arbitrary user\n[high/medium] arbitrary-send-eth at src/TreasuryFeeHook.sol:174: TreasuryFeeHook.flush() (src/TreasuryFeeHook.sol#174-186) sends eth to arbitrary user\n[high/medium] reentrancy-eth at src/TreasuryFeeHook.sol:174: Reentrancy in TreasuryFeeHook.flush() (src/TreasuryFeeHook.sol#174-186):\n[medium/medium] divide-before-multiply at src/StakingVault.sol:133: StakingVault.notifyReward(uint256) (src/StakingVault.sol#133-150) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/StakingVault.sol:115: StakingVault.claim() (src/StakingVault.sol#115-123) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/OracleAdapter.sol:208: OracleAdapter.clearStale(bytes32) (src/OracleAdapter.sol#208-215) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/OracleAdapter.sol:220: OracleAdapter.onOracleResult(bytes32,OracleAttestation.Attestation,bytes) (src/OracleAdapter.sol#220-229) uses a dangerous strict equality:\n[medium/medium] unused-return at src/TreasuryFeeHook.sol:190: TreasuryFeeHook.redeemClaims(uint256) (src/TreasuryFeeHook.sol#190-195) ignores return value by poolManager.unlock(abi.encode(amount)) (src/TreasuryFeeHook.sol#193)\n[medium/medium] unused-return at src/FeeTreasury.sol:257: FeeTreasury.unlockCallback(bytes) (src/FeeTreasury.sol#257-274) ignores return value by poolManager.settle{value: owed}() (src/FeeTreasury.sol#271)\n[low/medium] missing-zero-check at src/FeeTreasury.sol:137: FeeTreasury.setSinks(address,address,address).oracleAdapter_ (src/FeeTreasury.sol#137) lacks a zero-check on :\n[low/medium] missing-zero-check at src/FeeTreasury.sol:144: FeeTreasury.setExecutor(address).to (src/FeeTreasury.sol#144) lacks a zero-check on :\n[low/medium] missing-zero-check at src/FeeTreasury.sol:137: FeeTreasury.setSinks(address,address,address).stakingVault_ (src/FeeTreasury.sol#137) lacks a zero-check on :\n[low/medium] missing-zero-check at src/FeeTreasury.sol:137: FeeTreasury.setSinks(address,address,address).arena_ (src/FeeTreasury.sol#137) lacks a zero-check on :\n[low/medium] missing-zero-check at src/StakingVault.sol:62: StakingVault.setRewardFunder(address).funder (src/StakingVault.sol#62) lacks a zero-check on :\n[low/medium] missing-zero-check at src/OracleAdapter.sol:138: OracleAdapter.setExecutor(address).to (src/OracleAdapter.sol#138) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/TreasuryFeeHook.sol:330: Reentrancy in TreasuryFeeHook._collect(uint256,bool,bool,uint256) (src/TreasuryFeeHook.sol#330-342):\n[low/medium] reentrancy-benign at src/TreasuryFeeHook.sol:197: Reentrancy in TreasuryFeeHook.unlockCallback(bytes) (src/TreasuryFeeHook.sol#197-205):\n[low/medium] reentrancy-benign at src/OracleAdapter.sol:187: Reentrancy in OracleAdapter.request(uint256) (src/OracleAdapter.sol#187-205):\n[low/medium] reentrancy-benign at src/TreasuryFeeHook.sol:330: Reentrancy in TreasuryFeeHook._collect(uint256,bool,bool,uint256) (src/TreasuryFeeHook.sol#330-342):\n[low/medium] reentrancy-benign at src/TreasuryFeeHook.sol:174: Reentrancy in TreasuryFeeHook.flush() (src/TreasuryFeeHook.sol#174-186):\n[low/medium] reentrancy-events at src/OracleAdapter.sol:187: Reentrancy in OracleAdapter.request(uint256) (src/OracleAdapter.sol#187-205):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:190: Reentrancy in TreasuryFeeHook.redeemClaims(uint256) (src/TreasuryFeeHook.sol#190-195):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:197: Reentrancy in TreasuryFeeHook.unlockCallback(bytes) (src/TreasuryFeeHook.sol#197-205):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:330: Reentrancy in TreasuryFeeHook._collect(uint256,bool,bool,uint256) (src/TreasuryFeeHook.sol#330-342):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:330: Reentrancy in TreasuryFeeHook._collect(uint256,bool,bool,uint256) (src/TreasuryFeeHook.sol#330-342):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:174: Reentrancy in TreasuryFeeHook.flush() (src/TreasuryFeeHook.sol#174-186):\n[low/medium] timestamp at src/OracleAdapter.sol:208: OracleAdapter.clearStale(bytes32) (src/OracleAdapter.sol#208-215) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:115: StakingVault.claim() (src/StakingVault.sol#115-123) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:225: Arena.enter(uint256,bytes32) (src/Arena.sol#225-237) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:154: Arena.createRound(Arena.Mode,uint8,uint64,uint64,uint64,uint256,uint16,bytes32) (src/Arena.sol#154-186) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:76: StakingVault.lastTimeRewardApplicable() (src/StakingVault.sol#76-78) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:256: Arena.settle(uint256) (src/Arena.sol#256-278) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAttestation.sol:91: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/OracleAttestation.sol#91-99) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:239: Arena.reveal(uint256,uint8,bytes32) (src/Arena.sol#239-251) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:133: StakingVault.notifyReward(uint256) (src/StakingVault.sol#133-150) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAdapter.sol:187: OracleAdapter.request(uint256) (src/OracleAdapter.sol#187-205) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:281: Arena.cancel(uint256) (src/Arena.sol#281-289) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAdapter.sol:220: OracleAdapter.onOracleResult(bytes32,OracleAttestation.Attestation,bytes) (src/OracleAdapter.sol#220-229) uses timestamp for comparisons","passed":true},{"durationMs":704,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/Arena.sol:260: Reentrancy: State change after external call (6 places)\n[high] storage-array-memory-edit at src/FeeTreasury.sol:244: Storage Array Edited with Memory\n[high] unsafe-casting at src/TreasuryFeeHook.sol:246: Unsafe Casting of integers\n[low] centralization-risk at src/Arena.sol:147: Centralization Risk (25 places)\n[low] internal-function-used-once at src/OracleAttestation.sol:117: Internal Function Used Only Once\n[low] large-numeric-literal at src/FeeTreasury.sol:49: Large Numeric Literal (3 places)\n[low] non-reentrant-not-first at src/FeeTreasury.sol:200: `nonReentrant` is Not the First Modifier\n[low] state-no-address-check at src/FeeTreasury.sol:138: Address State Variable Set Without Checks (6 places)\n[low] unchecked-return at src/TreasuryFeeHook.sol:193: Unchecked Return\n[low] unused-error at src/Arena.sol:120: Unused Error","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"149eef22732c75304416dbed2311b69a0a4875d1c2597f218aef64ebbe77c3b7","verifiedTreeHash":"a605f168a067921e6a3981dfad4ec930bd04f921","verifierVersion":"0.1.0+c4d32abc"},{"checks":[{"durationMs":7801,"exitCode":0,"name":"build","output":"Compiling 125 files with Solc 0.8.26\nSolc 0.8.26 finished in 7.62s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> script/Deploy.s.sol:63:5:\n   |\n63 |     function run() external {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/StakingVault.sol:66:30\n   │\n66 │     function setRewardFunder(address funder) external onlyOwner {\n   │                              ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/StakingVault.sol:81:16\n   │\n81 │         return block.timestamp < periodFinish ? block.timestamp : periodFinish;\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:189:15\n    │\n189 │         if (!(block.timestamp < commitDeadline && commitDeadline < revealDeadline && revealDeadline < resultDeadline)) {\n    │               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:254:13\n    │\n254 │         if (block.timestamp >= r.commitDeadline) revert CommitClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/OracleAdapter.sol:145:26\n    │\n145 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:268:13\n    │\n268 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:268:51\n    │\n268 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:296:13\n    │\n296 │         if (block.timestamp < r.revealDeadline) revert RevealNotOver();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Arena.sol:300:25\n    │\n300 │         uint8 winning = uint8(res.answer % r.choiceCount) + 1;\n    │                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:321:13\n    │\n321 │         if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/TreasuryFeeHook.sol:200:22\n    │\n200 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/TreasuryFeeHook.sol:200:22\n    │\n200 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:167:26\n    │\n167 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:206:9\n    │\n206 │         emit FeeDelivered(treasury, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/TreasuryFeeHook.sol:214:9\n    │\n214 │         poolManager.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/StakingVault.sol:144:13\n    │\n144 │         if (block.timestamp < periodFinish) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:204:13\n    │\n204 │         if (block.timestamp >= windowStart + BUDGET_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:215:9\n    │\n215 │         emit Requested(roundId, intakeId, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:331:22\n    │\n331 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:220:44\n    │\n220 │         if (pendingSince[intakeId] == 0 || block.timestamp < pendingSince[intakeId] + REQUEST_TIMEOUT) {\n    │                                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/FeeTreasury.sol:236:83\n    │\n236 │     function withdrawOwner(address payable to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                                   ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:240:9\n    │\n240 │         emit OwnerWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:224:9\n    │\n224 │         emit ClaimsRedeemed(amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:249:9\n    │\n249 │         emit ReserveWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-no-eth]: external call can be reentered before `spentInWindow` is updated\n    ╭▸ src/FeeTreasury.sol:305:31\n    │\n305 │         bytes memory result = poolManager.unlock(abi.encode(key, ethIn));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:269:9\n    │\n269 │         IRewardSink(stakingVault).notifyReward(toStaking);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:271:9\n    │\n271 │         IPrizeSink(arena).fundPrizes(toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:272:9\n    │\n272 │         emit PrioBought(spent, out, toStaking, toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:288:9\n    │\n288 │         emit ImdBought(spent, out);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:276:9\n    │\n276 │         emit ResultStored(roundId, answer, a.requestId, a.panelJobId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/FeeTreasury.sol:300:13\n    │\n300 │         if (block.timestamp >= windowStart + SPEND_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:267:30\n    │\n267 │             ? _buyExactInput(uint256(-params.amountSpecified), params.sqrtPriceLimitX96)\n    │                              ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:268:32\n    │\n268 │             : _sellExactOutput(uint256(params.amountSpecified), params.sqrtPriceLimitX96);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:317:53\n    │\n317 │                 zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:321:24\n    │\n321 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:321:32\n    │\n321 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:322:23\n    │\n322 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:322:31\n    │\n322 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/FeeTreasury.sol:325:9\n    │\n325 │         poolManager.settle{value: owed}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/FeeTreasury.sol:331:22\n    │\n331 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:289:13\n    │\n289 │         if (filled != leg) fee = feeOnLeg(filled);\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:292:62\n    │\n292 │         return toBeforeSwapDelta((filled + fee).toInt128(), -prioOut.toInt128());\n    │                                                              ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:420:9\n    │\n420 │         emit FeeCharged(poolId, zeroForOne, exactInput, leg, fee, asClaim);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:286:22\n    │\n286 │             filled = uint256(uint128(-d.amount0()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:286:30\n    │\n286 │             filled = uint256(uint128(-d.amount0()));\n    │                              ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:287:23\n    │\n287 │             prioOut = uint256(uint128(d.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:287:31\n    │\n287 │             prioOut = uint256(uint128(d.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:324:51\n    │\n324 │         return toBeforeSwapDelta(-pay.toInt128(), prioIn.toInt128());\n    │                                                   ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:312:13\n    │\n312 │         if (filled != leg) {\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:308:22\n    │\n308 │             filled = uint256(uint128(d.amount0()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:308:30\n    │\n308 │             filled = uint256(uint128(d.amount0()));\n    │                              ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:309:22\n    │\n309 │             prioIn = uint256(uint128(-d.amount1()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:309:30\n    │\n309 │             prioIn = uint256(uint128(-d.amount1()));\n    │                              ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:38\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:46\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                              ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:68\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:76\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                            ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:345:44\n    │\n345 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:345:51\n    │\n345 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                                   ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/Arena.t.sol:49:40\n    │\n 49 │         uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n    │                                        ━━━━━━━━━━━━━━━\n    │\n    ⸬  lib/forge-std/src/StdCheats.sol:677:9\n    │\n677 │         vm.warp(vm.getBlockTimestamp() + time);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ArenaEdge.t.sol:486:30\n    │\n486 │         attest(id, 0, uint64(block.timestamp));\n    │                              ━━━━━━━━━━━━━━━\n    ‡\n497 │         vm.warp(uint256(r.resultDeadline) + 72 hours);\n    │         ───────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ArenaEdge.t.sol:88:31\n    │\n 88 │             expiresAt: uint64(block.timestamp + 1 days)\n    │                               ━━━━━━━━━━━━━━━\n    ‡\n497 │         vm.warp(uint256(r.resultDeadline) + 72 hours);\n    │         ───────────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":972,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] test_durationBounds() (gas: 67910)\n[PASS] test_leftoverRollsIntoNextStream() (gas: 432247)\n[PASS] test_noRewardsWithoutFunding() (gas: 204391)\n[PASS] test_notifyWhileIdleKeepsRemainderAndRestartsSchedule() (gas: 391149)\n[PASS] test_onlyFunderOrOwnerNotifies() (gas: 41455)\n[PASS] test_rewardsProRataByStakeAndTime_andStopAtPeriodEnd() (gas: 643629)\n[PASS] test_stakeWithdrawKeepsPrincipalExact() (gas: 249915)\n[PASS] test_streamPausesAgainWhenEveryoneLeaves() (gas: 675018)\n[PASS] test_streamPausesWhileNothingIsStaked_nothingStranded() (gas: 473558)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 2.89ms (1.49ms CPU time)\n\nRan 4 tests for test/TreasuryFeeHookEdge.t.sol:TreasuryFeeHookUnboundTest\n[PASS] test_feesBeforeBindingWaitThenDeliver() (gas: 1050855)\n[PASS] test_flushRevertsWhenNoTreasuryBound() (gas: 31569)\n[PASS] test_redeemClaimsRevertsWhenManagerHoldsLessEthThanAsked() (gas: 1082363)\n[PASS] test_treasuryRefusingEthNeverRevertsTheSwapAndFeeIsKept() (gas: 978912)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 3.08ms (1.92ms CPU time)\n\nRan 1 test for test/FeeTreasuryEdge.t.sol:FeeTreasuryThinPoolTest\n[PASS] test_rollingWindowCountsOnlyTheEthActuallySpent() (gas: 1036796)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.23ms (885.17µs CPU time)\n\nRan 3 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookClaimsTest\n[PASS] test_buyOnTokenOnlyPoolMintsClaimThenRedeems() (gas: 673609)\n[PASS] test_redeemRefusesMoreThanHeld() (gas: 75515)\n[PASS] test_secondBuyPaysDirectlyOnceManagerHoldsEth() (gas: 781711)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 3.75ms (2.03ms CPU time)\n\nRan 13 tests for test/OracleAdapter.t.sol:OracleAdapterTest\n[PASS] test_budgetAndExecutorEnforced() (gas: 961255)\n[PASS] test_callbackRefusesWrongSenderAndUnknownId() (gas: 685444)\n[PASS] test_callbackSelectorIsCanonical() (gas: 529)\n[PASS] test_clearStaleAfterTimeout() (gas: 660165)\n[PASS] test_digestMatchesTheProtocol() (gas: 10836)\n[PASS] test_manualRelayStoresResultAndEvidence() (gas: 224019)\n[PASS] test_nothingIsStoredOrRequestedBeforeTheBoundary() (gas: 999103)\n[PASS] test_paidRequestDisabledUntilConfigured() (gas: 99249)\n[PASS] test_paidRequestThenCallbackUnder200kGas() (gas: 854626)\n[PASS] test_pinnedSignerOutlivesRotation() (gas: 420903)\n[PASS] test_relayRejectsReplayAndSecondResult() (gas: 391853)\n[PASS] test_relayRejectsWrongQuestionChainQuorumPanelSignerExpiryAndType() (gas: 604372)\n[PASS] test_withdrawTokenRefusesTheConfiguredAsset() (gas: 444917)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 4.28ms (4.73ms CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployAllPlacesHookOnFlaggedAddress() (gas: 27273735)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.29ms (4.80ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:InvariantsTest\n[PASS]\nInvariantsTest invariants:\n[PASS] invariant_arenaBalanceIsFullyAccounted\n[PASS] invariant_vaultCoversPrincipalAndOwedRewards\n InvariantsTest invariants (runs: 32, calls: 1024, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 305   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | enter    | 144   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | fund     | 141   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | stake    | 139   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | warp     | 138   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 157   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 107.71ms (105.67ms CPU time)\n\nRan 11 tests for test/FeeTreasury.t.sol:FeeTreasuryTest\n[PASS] test_allocationSplitAndReserveCap() (gas: 562896)\n[PASS] test_buyPrioGuards() (gas: 994911)\n[PASS] test_buyPrioSplitsBetweenStakingAndArena() (gas: 1013065)\n[PASS] test_imdPurchaseWaitsForConfiguration_prioIndependent() (gas: 964127)\n[PASS] test_onlyHookCanFund_noOwnerAdvances() (gas: 43633)\n[PASS] test_ownerAndReserveWithdrawalsAreCapped() (gas: 692965)\n[PASS] test_partialFillCreditsUnspentEthBackToTheBudget() (gas: 1833934)\n[PASS] test_reserveStopsAtTarget() (gas: 878659)\n[PASS] test_rollingWindowAndPriceFloorBoundTheExecutor() (gas: 2247675)\n[PASS] test_setImdUnsetsThePoolKey() (gas: 751231)\n[PASS] test_sinksAreSetOnceAndNonZero() (gas: 165567)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 116.32ms (9.03ms CPU time)\n\nRan 14 tests for test/Arena.t.sol:ArenaTest\n[PASS] test_bossChallengeNeedsThreshold() (gas: 1138514)\n[PASS] test_cancelAfter72hRefundsEverything() (gas: 1240175)\n[PASS] test_constantsPublished() (gas: 16224)\n[PASS] test_createRoundNeedsPinnedQuestionAtTheCommitBoundary() (gas: 361560)\n[PASS] test_entryRules() (gas: 777445)\n[PASS] test_noWinnerReturnsPrizeToPool() (gas: 959998)\n[PASS] test_oldRoundStaysClaimableAfterNewRounds() (gas: 1535638)\n[PASS] test_ownerCannotSwapOracleForAnOpenRound() (gas: 1856152)\n[PASS] test_resolvedRoundCannotBeCancelled_onlySettled() (gas: 1199743)\n[PASS] test_resultIssuedBeforeCommitBoundaryCannotSettle() (gas: 556639)\n[PASS] test_roundCreationRules() (gas: 132179)\n[PASS] test_signerRotationDoesNotReachPinnedRounds() (gas: 803200)\n[PASS] test_unresolvedRoundCancelsAndALateResultCannotSettleIt() (gas: 858249)\n[PASS] test_vaultRaidFullLifecycle() (gas: 1752356)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 189.65ms (7.41ms CPU time)\n\nRan 3 tests for test/PrismRiotToken.t.sol:PrismRiotTokenTest\n[PASS] test_metadataAndSupply() (gas: 44137)\n[PASS] test_noMintOrOwnerFunctions() (gas: 51960)\n[PASS] test_transferMovesExactly() (gas: 78817)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 200.91ms (824.70µs CPU time)\n\nRan 17 tests for test/OracleAdapterEdge.t.sol:OracleAdapterEdgeTest\n[PASS] test_boundaryIsInclusiveOnTheChainClock() (gas: 337947)\n[PASS] test_budgetWindowBoundary() (gas: 988622)\n[PASS] test_callbackAfterManualRelayCannotOverwrite() (gas: 903609)\n[PASS] test_callbackWithBadAttestationLeavesRequestPending() (gas: 960439)\n[PASS] test_configurationIsOwnerOnly() (gas: 239040)\n[PASS] test_everyConfigurationPieceIsRequired() (gas: 725212)\n[PASS] test_expiryBoundaryIsInclusive() (gas: 175907)\n[PASS] test_issuedInTheFutureBeyondToleranceIsRefused_andWithinToleranceAccepted() (gas: 225032)\n[PASS] test_oneAttestationSettlesOneRoundOnly() (gas: 320119)\n[PASS] test_ownerCanWithdrawTokens() (gas: 125453)\n[PASS] test_pinningRules() (gas: 268522)\n[PASS] test_priceChangeAppliesToNextRequest() (gas: 414166)\n[PASS] test_requestRefusedBeforeBoundarySpendsNothing() (gas: 753776)\n[PASS] test_requestRefusesUnpinnedAndSettledRounds() (gas: 552651)\n[PASS] test_requestWithoutImdBalanceReverts_noIncomeNoOperations() (gas: 402607)\n[PASS] test_signerRotationRevokesOldSigner_andZeroRefused() (gas: 701643)\n[PASS] test_wrongKeyAndMalformedSignaturesRefused() (gas: 217120)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 201.03ms (22.45ms CPU time)\n\nRan 13 tests for test/StakingVaultEdge.t.sol:StakingVaultEdgeTest\n[PASS] testFuzz_claimsNeverExceedFunding(uint96,uint8) (runs: 512, μ: 1868601, ~: 1443410)\n[PASS] testFuzz_principalIsExactUnderAnActiveStream(uint96,uint96,uint96,uint32) (runs: 512, μ: 628037, ~: 648927)\n[PASS] testFuzz_proRataByStake(uint96,uint96,uint96) (runs: 512, μ: 661215, ~: 661505)\n[PASS] test_claimWithNothingEarnedIsANoOp() (gas: 237151)\n[PASS] test_constructorRefusesZeroToken() (gas: 6040)\n[PASS] test_exitReturnsPrincipalAndRewards() (gas: 414125)\n[PASS] test_funderWithoutBalanceCannotFund() (gas: 152237)\n[PASS] test_noAccrualBetweenStreams() (gas: 448706)\n[PASS] test_ownershipAndConfigurationGuards() (gas: 282061)\n[PASS] test_proRataByTime() (gas: 517729)\n[PASS] test_strangerCannotFund_andFundingPullsFromFunderOnly() (gas: 522961)\n[PASS] test_withdrawMoreThanStakedRefused_evenWithOthersPrincipalPresent() (gas: 319975)\n[PASS] test_zeroAmountsRefused() (gas: 97272)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 201.86ms (598.63ms CPU time)\n\nRan 16 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookTest\n[PASS] testFuzz_feeChargedMatchesTreasuryIncome(uint96,bool,bool) (runs: 256, μ: 382259, ~: 384928)\n[PASS] testFuzz_quotesAreConsistentWeiLevel(uint128) (runs: 256, μ: 22607, ~: 22607)\n[PASS] test_buyExactInput_feeFromEthInput() (gas: 393752)\n[PASS] test_buyExactOutput_feeOnTopOfEthCharged() (gas: 376548)\n[PASS] test_callbacksRefuseNonManager() (gas: 123049)\n[PASS] test_constructorRefusesMismatchedAddress() (gas: 74525)\n[PASS] test_feeOnLegRoundsUpAtWeiLevel() (gas: 22980)\n[PASS] test_initializeOnFreshHookRefusesWrongPair() (gas: 344774)\n[PASS] test_initializeRefusesNonFactoryAndOtherPools() (gas: 48021)\n[PASS] test_oneWeiBuyIsRefusedNotSwallowed() (gas: 439366)\n[PASS] test_otherDirectionsLeaveTokenTransfersUntaxed() (gas: 74600)\n[PASS] test_permissionsAndAddressAgree() (gas: 31553)\n[PASS] test_receiveRefusesEveryoneButThePoolManager() (gas: 38301)\n[PASS] test_sellExactInput_feeOutOfEthOutput() (gas: 343970)\n[PASS] test_sellExactOutput_swapperGetsExactlyTheEthAsked() (gas: 369304)\n[PASS] test_treasuryBindingIsCorrectableUntilFirstDelivery_thenImmutable() (gas: 593116)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 208.38ms (407.96ms CPU time)\n\nRan 12 tests for test/TreasuryFeeHookEdge.t.sol:TreasuryFeeHookEdgeTest\n[PASS] testFuzz_dustBuysChargeOneWei(uint256) (runs: 512, μ: 380868, ~: 381725)\n[PASS] testFuzz_everyShapeSucceedsAcrossMagnitudes(uint256,uint8) (runs: 512, μ: 371130, ~: 373167)\n[PASS] testFuzz_feeIsHalfPercentOfExecutedLeg(uint96,uint8) (runs: 512, μ: 371123, ~: 375165)\n[PASS] test_constructorRefusesZeroAddresses() (gas: 209390)\n[PASS] test_hooklessEthPrioPoolPaysNoHookFee() (gas: 723677)\n[PASS] test_noFeeOrTreasurySetterExists() (gas: 157574)\n[PASS] test_oneWeiBuyIsAllFeeAndDoesNotRevert() (gas: 126911)\n[PASS] test_ownershipIsTwoStepAndNotRenounceable() (gas: 185755)\n[PASS] test_partialFill_afterSwapShapesChargeOnExecutedLegOnly() (gas: 687140)\n[PASS] test_redeemZeroRefused() (gas: 31767)\n[PASS] test_swapOnForeignKeyNamingTheHookIsRefused() (gas: 126722)\n[PASS] test_tinyExactOutputsBothDirections() (gas: 633682)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 208.43ms (586.71ms CPU time)\n\nRan 18 tests for test/ArenaEdge.t.sol:ArenaEdgeTest\n[PASS] testFuzz_payoutBounds(uint8,uint8,bool,uint256,uint96) (runs: 512, μ: 943363, ~: 967857)\n[PASS] testFuzz_prizeSplitNeverExceedsPrize(uint8,uint96) (runs: 512, μ: 3381210, ~: 3139912)\n[PASS] test_bossThresholdMetPaysPrize() (gas: 1102657)\n[PASS] test_cancelAndSettleAreExclusiveAfterTheGrace() (gas: 1726188)\n[PASS] test_createRoundRefusesMissingOrMisalignedPin() (gas: 499113)\n[PASS] test_deadlineBoundariesAreHalfOpen() (gas: 831683)\n[PASS] test_entryPullsExactly102AndNothingLater() (gas: 952168)\n[PASS] test_entryWithoutApprovalReverts_andWithShortApprovalReverts() (gas: 553695)\n[PASS] test_factionDuelSplitsPrizeAmongAllCorrect() (gas: 1171268)\n[PASS] test_fundPrizesZeroIsHarmless() (gas: 92684)\n[PASS] test_issuedAtToleranceIsConsistentWithTheAdapter() (gas: 816396)\n[PASS] test_nonEntrantCannotClaimOrRefund() (gas: 1382435)\n[PASS] test_onlyOwnerCreatesAndSetsOracle_andOracleMustBeSetFirst() (gas: 188400)\n[PASS] test_ownershipTwoStepNoRenounce() (gas: 135533)\n[PASS] test_prizeDustReturnsToPool() (gas: 1293259)\n[PASS] test_settledRoundCannotBeCancelledAndCancelledCannotSettle() (gas: 857912)\n[PASS] test_unknownRoundRefusesEverything() (gas: 187624)\n[PASS] test_zeroCommitmentRefused() (gas: 355375)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 218.87ms (413.38ms CPU time)\n\nRan 7 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookPartialFillTest\n[PASS] testFuzz_partialFillFeeIsAlwaysOnTheFilledLeg(uint96,bool) (runs: 256, μ: 651314, ~: 745558)\n[PASS] test_buyExactInput_liquidityExhausted_feeIsHalfPercentOfFilledLeg() (gas: 766277)\n[PASS] test_buyExactInput_priceLimit_feeIsHalfPercentOfFilledLeg() (gas: 410658)\n[PASS] test_fullFillsStillCostExactlyWhatWasSpecified() (gas: 634061)\n[PASS] test_limitNextToSpotMovesNothingAndChargesNothing() (gas: 233336)\n[PASS] test_sellExactOutput_liquidityExhausted_sellerNeverPaysEth() (gas: 763001)\n[PASS] test_sellExactOutput_priceLimit_feeIsHalfPercentOfFilledLeg() (gas: 381614)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 218.77ms (222.06ms CPU time)\n\nRan 19 tests for test/FeeTreasuryEdge.t.sol:FeeTreasuryEdgeTest\n[PASS] testFuzz_allocationArithmetic(uint96,uint256) (runs: 512, μ: 607127, ~: 607359)\n[PASS] testFuzz_repeatedBuysNeverOverspend(uint8) (runs: 512, μ: 1947300, ~: 2055524)\n[PASS] test_allocateIsIdempotentUntilNewIncome() (gas: 578517)\n[PASS] test_allocateWithNothingIsANoOp() (gas: 76559)\n[PASS] test_buyPrioFailsWithoutFunderRoleAndRollsBack() (gas: 895016)\n[PASS] test_buyPrioOnUnboundTreasuryRefused() (gas: 91009)\n[PASS] test_buyPrioRefusesZeroAndUnsetSinks() (gas: 962999)\n[PASS] test_configurationIsOwnerOnlyAndOneShotWhereStated() (gas: 324946)\n[PASS] test_executorCannotTakeOwnerBudget_ownerCannotExceedReserve() (gas: 670170)\n[PASS] test_ownershipTwoStepNoRenounce() (gas: 156187)\n[PASS] test_priceFloorBoundaryIsExactOnTheSpentLeg() (gas: 1779485)\n[PASS] test_reserveAboveLoweredTargetGetsNothingMore() (gas: 909568)\n[PASS] test_rollingWindowBoundaries() (gas: 1919932)\n[PASS] test_setImdPoolRequiresImdFirst_thenBuyImdDeliversToAdapter() (gas: 2319514)\n[PASS] test_strangerEthRefusedEvenAfterBinding() (gas: 69941)\n[PASS] test_unlockCallbackOnlyFromManager() (gas: 40214)\n[PASS] test_withdrawToRejectingRecipientRevertsAndKeepsBudget() (gas: 740923)\n[PASS] test_withdrawZeroIsHarmless() (gas: 83365)\n[PASS] test_zeroFloorDisablesPurchasesAgain() (gas: 565942)\nSuite result: ok. 19 passed; 0 failed; 0 skipped; finished in 218.94ms (288.72ms CPU time)\n\nRan 1 test for test/TreasuryInvariants.t.sol:TreasuryInvariantsTest\n[PASS]\nTreasuryInvariantsTest invariants:\n[PASS] invariant_hookFeeConservation\n[PASS] invariant_incomeConservation\n[PASS] invariant_managerSolventForClaims\n[PASS] invariant_purchasedPrioIsSplitAndForwarded\n[PASS] invariant_reserveCapped\n[PASS] invariant_treasuryEthIsFullyBucketed\n TreasuryInvariantsTest invariants (runs: 64, calls: 2560, reverts: 3)\n\n╭-----------------+------------------+-------+---------+----------╮\n| Contract        | Selector         | Calls | Reverts | Discards |\n+=================================================================+\n| TreasuryHandler | allocate         | 228   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | buyExactIn       | 184   | 3       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | buyExactOut      | 229   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | buyPrio          | 254   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | flush            | 198   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | redeemClaims     | 204   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | sellExactIn      | 237   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | sellExactOut     | 199   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | setReserveTarget | 222   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | warp             | 186   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | withdrawOwner    | 223   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | withdrawReserve  | 196   | 0       | 0        |\n╰-----------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 610.04ms (606.33ms CPU time)\n\nRan 1 test for test/ArenaInvariants.t.sol:ArenaInvariantsTest\n[PASS]\nArenaInvariantsTest invariants:\n[PASS] invariant_balanceFullyAccounted\n[PASS] invariant_playersNeverExtractMoreThanFunded\n[PASS] invariant_roundsAreFrozenOnceFinished\n[PASS] invariant_tokenConservation\n ArenaInvariantsTest invariants (runs: 64, calls: 3840, reverts: 0)\n\n╭--------------+---------------+-------+---------+----------╮\n| Contract     | Selector      | Calls | Reverts | Discards |\n+===========================================================+\n| ArenaHandler | cancel        | 453   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | claimOrRefund | 491   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | createRound   | 488   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | enter         | 506   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | fund          | 489   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | reveal        | 500   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | settle        | 441   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | warp          | 472   | 0       | 0        |\n╰--------------+---------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 848.03ms (841.92ms CPU time)\n\nRan 19 test suites in 851.74ms (3.57s CPU time): 164 tests passed, 0 failed, 0 skipped (164 total tests)\n","passed":true},{"durationMs":71,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Arena.acceptOwnership()\",\"Arena.cancel(uint256)\",\"Arena.claim(uint256)\",\"Arena.createRound(uint8,uint8,uint64,uint64,uint64,uint256,uint16,bytes32)\",\"Arena.enter(uint256,bytes32)\",\"Arena.fundPrizes(uint256)\",\"Arena.refund(uint256)\",\"Arena.reveal(uint256,uint8,bytes32)\",\"Arena.setOracle(address)\",\"Arena.settle(uint256)\",\"Arena.transferOwnership(address)\",\"FeeTreasury.acceptOwnership()\",\"FeeTreasury.allocate()\",\"FeeTreasury.bindHook(address)\",\"FeeTreasury.buyImd(uint256,uint256)\",\"FeeTreasury.buyPrio(uint256,uint256)\",\"FeeTreasury.receive()\",\"FeeTreasury.setExecutor(address)\",\"FeeTreasury.setImd(address)\",\"FeeTreasury.setImdPool(uint24,int24,address)\",\"FeeTreasury.setMaxSpendPerSwap(uint256)\",\"FeeTreasury.setPriceFloors(uint256,uint256)\",\"FeeTreasury.setPrio(address)\",\"FeeTreasury.setReserveTarget(uint256)\",\"FeeTreasury.setSinks(address,address,address)\",\"FeeTreasury.setSpendPerWindow(uint256)\",\"FeeTreasury.transferOwnership(address)\",\"FeeTreasury.unlockCallback(bytes)\",\"FeeTreasury.withdrawOwner(address,uint256)\",\"FeeTreasury.withdrawReserve(address,uint256)\",\"OracleAdapter.acceptOwnership()\",\"OracleAdapter.clearStale(bytes32)\",\"OracleAdapter.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.pinQuestion(uint256,bytes32,uint256,uint16,uint16,uint64,bytes)\",\"OracleAdapter.request(uint256)\",\"OracleAdapter.setAction(bytes32)\",\"OracleAdapter.setBudget(uint256)\",\"OracleAdapter.setCallbackConfigured(bool)\",\"OracleAdapter.setExecutor(address)\",\"OracleAdapter.setIntake(address)\",\"OracleAdapter.setPayment(address,uint256)\",\"OracleAdapter.setSigner(address)\",\"OracleAdapter.submitAttestation(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.transferOwnership(address)\",\"OracleAdapter.withdrawToken(address,address,uint256)\",\"PrismRiotToken.approve(address,uint256)\",\"PrismRiotToken.transfer(address,uint256)\",\"PrismRiotToken.transferFrom(address,address,uint256)\",\"StakingVault.acceptOwnership()\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.notifyReward(uint256)\",\"StakingVault.setRewardFunder(address)\",\"StakingVault.setRewardsDuration(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.transferOwnership(address)\",\"StakingVault.withdraw(uint256)\",\"TreasuryFeeHook.acceptOwnership()\",\"TreasuryFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"TreasuryFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"TreasuryFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"TreasuryFeeHook.bindTreasury(address)\",\"TreasuryFeeHook.flush()\",\"TreasuryFeeHook.receive()\",\"TreasuryFeeHook.redeemClaims(uint256)\",\"TreasuryFeeHook.transferOwnership(address)\",\"TreasuryFeeHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":9,\"LICENSE\":9,\"README.md\":308,\"docs/OPERATOR.md\":55,\"docs/REVIEW.md\":79,\"docs/abi/Arena.json\":1055,\"docs/abi/FeeTreasury.json\":1224,\"docs/abi/OracleAdapter.json\":1450,\"docs/abi/PrismRiotToken.json\":328,\"docs/abi/StakingVault.json\":627,\"docs/abi/TreasuryFeeHook.json\":1515,\"foundry.toml\":27,\"operator/operator.example.json\":19,\"operator/operator.py\":294,\"operator/test_operator.py\":110,\"remappings.txt\":6,\"script/Deploy.s.sol\":66,\"src/Arena.sol\":359,\"src/FeeTreasury.sol\":334,\"src/OracleAdapter.sol\":298,\"src/OracleAttestation.sol\":125,\"src/PrismRiotToken.sol\":16,\"src/StakingVault.sol\":177,\"src/TreasuryFeeHook.sol\":422,\"src/TwoStepOwned.sol\":19,\"test/Arena.t.sol\":464,\"test/ArenaEdge.t.sol\":514,\"test/ArenaInvariants.t.sol\":288,\"test/Deploy.t.sol\":23,\"test/FeeTreasury.t.sol\":235,\"test/FeeTreasuryEdge.t.sol\":544,\"test/Invariants.t.sol\":155,\"test/OracleAdapter.t.sol\":320,\"test/OracleAdapterEdge.t.sol\":374,\"test/PrismRiotToken.t.sol\":37,\"test/StakingVault.t.sol\":156,\"test/StakingVaultEdge.t.sol\":268,\"test/TreasuryFeeHook.t.sol\":415,\"test/TreasuryFeeHookEdge.t.sol\":384,\"test/TreasuryInvariants.t.sol\":253,\"test/utils/Fixture.sol\":117,\"test/utils/MockIntake.sol\":36},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"2238a04c42e12f01006a365c64a9af6322ab54c26b1d1a03137778ce0767e84c","verifiedTreeHash":"ecc210284230012a781800baa955efd6bcb0960d","verifierVersion":"0.1.0+c4d32abc"},{"checks":[{"durationMs":6009,"exitCode":0,"name":"build","output":"Compiling 125 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.86s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> script/Deploy.s.sol:63:5:\n   |\n63 |     function run() external {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:168:15\n    │\n168 │         if (!(block.timestamp < commitDeadline && commitDeadline < revealDeadline && revealDeadline < resultDeadline)) {\n    │               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/OracleAdapter.sol:138:26\n    │\n138 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/StakingVault.sol:62:30\n   │\n62 │     function setRewardFunder(address funder) external onlyOwner {\n   │                              ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:228:13\n    │\n228 │         if (block.timestamp >= r.commitDeadline) revert CommitClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/StakingVault.sol:77:16\n   │\n77 │         return block.timestamp < periodFinish ? block.timestamp : periodFinish;\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:242:13\n    │\n242 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:242:51\n    │\n242 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:259:13\n    │\n259 │         if (block.timestamp < r.revealDeadline) revert RevealNotOver();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Arena.sol:264:25\n    │\n264 │         uint8 winning = uint8(res.answer % r.choiceCount) + 1;\n    │                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:284:13\n    │\n284 │         if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/TreasuryFeeHook.sol:179:22\n    │\n179 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/TreasuryFeeHook.sol:179:22\n    │\n179 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:185:9\n    │\n185 │         emit FeeDelivered(treasury, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/TreasuryFeeHook.sol:193:9\n    │\n193 │         poolManager.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:23\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                       ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:46\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                                              ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:62\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                                                              ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:144:26\n    │\n144 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:193:13\n    │\n193 │         if (block.timestamp >= windowStart + BUDGET_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:204:9\n    │\n204 │         emit Requested(roundId, intakeId, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:209:44\n    │\n209 │         if (pendingSince[intakeId] == 0 || block.timestamp < pendingSince[intakeId] + REQUEST_TIMEOUT) {\n    │                                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:277:22\n    │\n277 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/FeeTreasury.sol:200:83\n    │\n200 │     function withdrawOwner(address payable to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                                   ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:204:9\n    │\n204 │         emit OwnerWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/StakingVault.sol:139:13\n    │\n139 │         if (block.timestamp < periodFinish) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:203:9\n    │\n203 │         emit ClaimsRedeemed(amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:213:9\n    │\n213 │         emit ReserveWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:230:9\n    │\n230 │         IRewardSink(stakingVault).notifyReward(toStaking);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:232:9\n    │\n232 │         IPrizeSink(arena).fundPrizes(toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:252:31\n    │\n252 │         bytes memory result = poolManager.unlock(abi.encode(key, ethIn));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:233:9\n    │\n233 │         emit PrioBought(ethIn, out, toStaking, toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:262:9\n    │\n262 │         emit ResultStored(roundId, answer, a.requestId, a.panelJobId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:240:45\n    │\n240 │             (fee, leg) = quoteBuyExactInput(uint256(-params.amountSpecified));\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:243:47\n    │\n243 │             (fee, leg) = quoteSellExactOutput(uint256(params.amountSpecified));\n    │                                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:246:63\n    │\n246 │         return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);\n    │                                                               ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:246:70\n    │\n246 │         return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);\n    │                                                                      ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:246:9\n    │\n246 │         emit ImdBought(ethIn, out);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:38\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:46\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                              ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:68\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:76\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                            ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:266:44\n    │\n266 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:266:51\n    │\n266 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                                   ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:341:9\n    │\n341 │         emit FeeCharged(poolId, zeroForOne, exactInput, leg, fee, asClaim);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:263:53\n    │\n263 │                 zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:267:24\n    │\n267 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:267:32\n    │\n267 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:268:23\n    │\n268 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:268:31\n    │\n268 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/FeeTreasury.sol:271:9\n    │\n271 │         poolManager.settle{value: owed}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/FeeTreasury.sol:277:22\n    │\n277 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/Arena.t.sol:49:40\n    │\n 49 │         uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n    │                                        ━━━━━━━━━━━━━━━\n    │\n    ⸬  lib/forge-std/src/StdCheats.sol:677:9\n    │\n677 │         vm.warp(vm.getBlockTimestamp() + time);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/ArenaEdge.t.sol:49:40\n    │\n 49 │         uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n    │                                        ━━━━━━━━━━━━━━━\n    │\n    ⸬  lib/forge-std/src/StdCheats.sol:677:9\n    │\n677 │         vm.warp(vm.getBlockTimestamp() + time);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":747,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/PrismRiotToken.t.sol:PrismRiotTokenTest\n[PASS] test_metadataAndSupply() (gas: 44137)\n[PASS] test_noMintOrOwnerFunctions() (gas: 51960)\n[PASS] test_transferMovesExactly() (gas: 78817)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 454.74µs (172.29µs CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployAllPlacesHookOnFlaggedAddress() (gas: 23245771)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.81ms (1.35ms CPU time)\n\nRan 3 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookClaimsTest\n[PASS] test_buyOnTokenOnlyPoolMintsClaimThenRedeems() (gas: 651315)\n[PASS] test_redeemRefusesMoreThanHeld() (gas: 69782)\n[PASS] test_secondBuyPaysDirectlyOnceManagerHoldsEth() (gas: 740396)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 40.57ms (1.05ms CPU time)\n\nRan 10 tests for test/OracleAdapter.t.sol:OracleAdapterTest\n[PASS] test_budgetAndExecutorEnforced() (gas: 952569)\n[PASS] test_callbackRefusesWrongSenderAndUnknownId() (gas: 683201)\n[PASS] test_callbackSelectorIsCanonical() (gas: 507)\n[PASS] test_clearStaleAfterTimeout() (gas: 657965)\n[PASS] test_digestMatchesTheProtocol() (gas: 10880)\n[PASS] test_manualRelayStoresResultAndEvidence() (gas: 225809)\n[PASS] test_paidRequestDisabledUntilConfigured() (gas: 99204)\n[PASS] test_paidRequestThenCallbackUnder200kGas() (gas: 854250)\n[PASS] test_relayRejectsReplayAndSecondResult() (gas: 393159)\n[PASS] test_relayRejectsWrongQuestionChainQuorumPanelSignerExpiryAndType() (gas: 610171)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 40.64ms (4.21ms CPU time)\n\nRan 9 tests for test/Arena.t.sol:ArenaTest\n[PASS] test_bossChallengeNeedsThreshold() (gas: 1011509)\n[PASS] test_cancelAfter72hRefundsEverything() (gas: 1095210)\n[PASS] test_constantsPublished() (gas: 16377)\n[PASS] test_entryRules() (gas: 653340)\n[PASS] test_noWinnerReturnsPrizeToPool() (gas: 828434)\n[PASS] test_oldRoundStaysClaimableAfterNewRounds() (gas: 1279521)\n[PASS] test_resultIssuedBeforeCommitBoundaryCannotSettle() (gas: 429691)\n[PASS] test_roundCreationRules() (gas: 132179)\n[PASS] test_vaultRaidFullLifecycle() (gas: 1620787)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 41.17ms (3.70ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:InvariantsTest\n[PASS]\nInvariantsTest invariants:\n[PASS] invariant_arenaBalanceIsFullyAccounted\n[PASS] invariant_vaultCoversPrincipalAndOwedRewards\n InvariantsTest invariants (runs: 32, calls: 1024, reverts: 1)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 307   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | enter    | 125   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | fund     | 154   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | stake    | 142   | 1       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | warp     | 160   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 136   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 95.90ms (94.55ms CPU time)\n\nRan 4 tests for test/TreasuryFeeHookEdge.t.sol:TreasuryFeeHookUnboundTest\n[PASS] test_feesBeforeBindingWaitThenDeliver() (gas: 1028521)\n[PASS] test_flushRevertsWhenNoTreasuryBound() (gas: 31569)\n[PASS] test_redeemClaimsRevertsWhenManagerHoldsLessEthThanAsked() (gas: 1060037)\n[PASS] test_treasuryRefusingEthNeverRevertsTheSwapAndFeeIsKept() (gas: 954102)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 130.66ms (2.25ms CPU time)\n\nRan 13 tests for test/StakingVaultEdge.t.sol:StakingVaultEdgeTest\n[PASS] testFuzz_claimsNeverExceedFunding(uint96,uint8) (runs: 512, μ: 1823117, ~: 1459106)\n[PASS] testFuzz_principalIsExactUnderAnActiveStream(uint96,uint96,uint96,uint32) (runs: 512, μ: 633165, ~: 657282)\n[PASS] testFuzz_proRataByStake(uint96,uint96,uint96) (runs: 512, μ: 666219, ~: 666546)\n[PASS] test_claimWithNothingEarnedIsANoOp() (gas: 239594)\n[PASS] test_constructorRefusesZeroToken() (gas: 6040)\n[PASS] test_exitReturnsPrincipalAndRewards() (gas: 414118)\n[PASS] test_funderWithoutBalanceCannotFund() (gas: 154336)\n[PASS] test_noAccrualBetweenStreams() (gas: 448067)\n[PASS] test_ownershipAndConfigurationGuards() (gas: 282061)\n[PASS] test_proRataByTime() (gas: 517415)\n[PASS] test_strangerCannotFund_andFundingPullsFromFunderOnly() (gas: 523367)\n[PASS] test_withdrawMoreThanStakedRefused_evenWithOthersPrincipalPresent() (gas: 325419)\n[PASS] test_zeroAmountsRefused() (gas: 97272)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 132.51ms (264.09ms CPU time)\n\nRan 16 tests for test/ArenaEdge.t.sol:ArenaEdgeTest\n[PASS] testFuzz_payoutBounds(uint8,uint8,bool,uint256,uint96) (runs: 512, μ: 868814, ~: 838840)\n[PASS] testFuzz_prizeSplitNeverExceedsPrize(uint8,uint96) (runs: 512, μ: 3185427, ~: 2722783)\n[PASS] test_bossThresholdMetPaysPrize() (gas: 1028368)\n[PASS] test_deadlineBoundariesAreHalfOpen() (gas: 733013)\n[PASS] test_entryPullsExactly102AndNothingLater() (gas: 882381)\n[PASS] test_entryWithoutApprovalReverts_andWithShortApprovalReverts() (gas: 482220)\n[PASS] test_factionDuelSplitsPrizeAmongAllCorrect() (gas: 1096935)\n[PASS] test_fundPrizesZeroIsHarmless() (gas: 92662)\n[PASS] test_issuedAtToleranceIsConsistentWithTheAdapter() (gas: 739231)\n[PASS] test_nonEntrantCannotClaimOrRefund() (gas: 1223041)\n[PASS] test_onlyOwnerCreatesAndSetsOracle_andOracleMustBeSetFirst() (gas: 188305)\n[PASS] test_ownershipTwoStepNoRenounce() (gas: 135482)\n[PASS] test_prizeDustReturnsToPool() (gas: 1218838)\n[PASS] test_settledRoundCannotBeCancelledAndCancelledCannotSettle() (gas: 783489)\n[PASS] test_unknownRoundRefusesEverything() (gas: 187624)\n[PASS] test_zeroCommitmentRefused() (gas: 283880)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 150.32ms (287.10ms CPU time)\n\nRan 6 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] test_durationBounds() (gas: 67910)\n[PASS] test_leftoverRollsIntoNextStream() (gas: 431893)\n[PASS] test_noRewardsWithoutFunding() (gas: 209489)\n[PASS] test_onlyFunderOrOwnerNotifies() (gas: 41455)\n[PASS] test_rewardsProRataByStakeAndTime_andStopAtPeriodEnd() (gas: 646737)\n[PASS] test_stakeWithdrawKeepsPrincipalExact() (gas: 251959)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 150.32ms (884.56µs CPU time)\n\nRan 14 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookTest\n[PASS] testFuzz_feeChargedMatchesTreasuryIncome(uint96,bool,bool) (runs: 256, μ: 373520, ~: 377852)\n[PASS] testFuzz_quotesAreConsistentWeiLevel(uint128) (runs: 256, μ: 22585, ~: 22585)\n[PASS] test_buyExactInput_feeFromEthInput() (gas: 374625)\n[PASS] test_buyExactOutput_feeOnTopOfEthCharged() (gas: 376464)\n[PASS] test_callbacksRefuseNonManager() (gas: 123093)\n[PASS] test_constructorRefusesMismatchedAddress() (gas: 71766)\n[PASS] test_feeOnLegRoundsUpAtWeiLevel() (gas: 22980)\n[PASS] test_initializeOnFreshHookRefusesWrongPair() (gas: 340433)\n[PASS] test_initializeRefusesNonFactoryAndOtherPools() (gas: 47998)\n[PASS] test_otherDirectionsLeaveTokenTransfersUntaxed() (gas: 74556)\n[PASS] test_permissionsAndAddressAgree() (gas: 31486)\n[PASS] test_sellExactInput_feeOutOfEthOutput() (gas: 343952)\n[PASS] test_sellExactOutput_swapperGetsExactlyTheEthAsked() (gas: 350366)\n[PASS] test_treasuryBindsOnce() (gas: 63780)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 150.31ms (200.22ms CPU time)\n\nRan 15 tests for test/OracleAdapterEdge.t.sol:OracleAdapterEdgeTest\n[PASS] test_budgetWindowBoundary() (gas: 977770)\n[PASS] test_callbackAfterManualRelayCannotOverwrite() (gas: 903221)\n[PASS] test_callbackWithBadAttestationLeavesRequestPending() (gas: 959687)\n[PASS] test_configurationIsOwnerOnly() (gas: 239040)\n[PASS] test_everyConfigurationPieceIsRequired() (gas: 725212)\n[PASS] test_expiryBoundaryIsInclusive() (gas: 177707)\n[PASS] test_issuedInTheFutureBeyondToleranceIsRefused_andWithinToleranceAccepted() (gas: 224508)\n[PASS] test_oneAttestationSettlesOneRoundOnly() (gas: 321469)\n[PASS] test_ownerCanWithdrawTokens() (gas: 123278)\n[PASS] test_pinningRules() (gas: 266026)\n[PASS] test_priceChangeAppliesToNextRequest() (gas: 412045)\n[PASS] test_requestRefusesUnpinnedAndSettledRounds() (gas: 554429)\n[PASS] test_requestWithoutImdBalanceReverts_noIncomeNoOperations() (gas: 400441)\n[PASS] test_signerRotationRevokesOldSigner_andZeroRefused() (gas: 324034)\n[PASS] test_wrongKeyAndMalformedSignaturesRefused() (gas: 216319)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 150.41ms (5.88ms CPU time)\n\nRan 7 tests for test/FeeTreasury.t.sol:FeeTreasuryTest\n[PASS] test_allocationSplitAndReserveCap() (gas: 543549)\n[PASS] test_buyPrioGuards() (gas: 842894)\n[PASS] test_buyPrioSplitsBetweenStakingAndArena() (gas: 953462)\n[PASS] test_imdPurchaseWaitsForConfiguration_prioIndependent() (gas: 904523)\n[PASS] test_onlyHookCanFund_noOwnerAdvances() (gas: 43633)\n[PASS] test_ownerAndReserveWithdrawalsAreCapped() (gas: 673836)\n[PASS] test_reserveStopsAtTarget() (gas: 840621)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 150.42ms (3.35ms CPU time)\n\nRan 16 tests for test/FeeTreasuryEdge.t.sol:FeeTreasuryEdgeTest\n[PASS] testFuzz_allocationArithmetic(uint96,uint256) (runs: 512, μ: 587477, ~: 588035)\n[PASS] testFuzz_repeatedBuysNeverOverspend(uint8) (runs: 512, μ: 1766655, ~: 1608639)\n[PASS] test_allocateIsIdempotentUntilNewIncome() (gas: 559259)\n[PASS] test_allocateWithNothingIsANoOp() (gas: 76406)\n[PASS] test_buyPrioFailsWithoutFunderRoleAndRollsBack() (gas: 833273)\n[PASS] test_buyPrioOnUnboundTreasuryRefused() (gas: 91008)\n[PASS] test_buyPrioRefusesZeroAndUnsetSinks() (gas: 592527)\n[PASS] test_configurationIsOwnerOnlyAndOneShotWhereStated() (gas: 324655)\n[PASS] test_executorCannotTakeOwnerBudget_ownerCannotExceedReserve() (gas: 651086)\n[PASS] test_ownershipTwoStepNoRenounce() (gas: 156274)\n[PASS] test_reserveAboveLoweredTargetGetsNothingMore() (gas: 871225)\n[PASS] test_setImdPoolRequiresImdFirst_thenBuyImdDeliversToAdapter() (gas: 1672612)\n[PASS] test_strangerEthRefusedEvenAfterBinding() (gas: 69941)\n[PASS] test_unlockCallbackOnlyFromManager() (gas: 40191)\n[PASS] test_withdrawToRejectingRecipientRevertsAndKeepsBudget() (gas: 721621)\n[PASS] test_withdrawZeroIsHarmless() (gas: 83386)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 150.33ms (219.71ms CPU time)\n\nRan 12 tests for test/TreasuryFeeHookEdge.t.sol:TreasuryFeeHookEdgeTest\n[PASS] testFuzz_dustBuysChargeOneWei(uint256) (runs: 512, μ: 361942, ~: 362664)\n[PASS] testFuzz_everyShapeSucceedsAcrossMagnitudes(uint256,uint8) (runs: 512, μ: 359883, ~: 366722)\n[PASS] testFuzz_feeIsHalfPercentOfExecutedLeg(uint96,uint8) (runs: 512, μ: 361575, ~: 362876)\n[PASS] test_constructorRefusesZeroAddresses() (gas: 197485)\n[PASS] test_hooklessEthPrioPoolPaysNoHookFee() (gas: 723633)\n[PASS] test_noFeeOrTreasurySetterExists() (gas: 157574)\n[PASS] test_oneWeiBuyIsAllFeeAndDoesNotRevert() (gas: 314343)\n[PASS] test_ownershipIsTwoStepAndNotRenounceable() (gas: 185743)\n[PASS] test_partialFill_afterSwapShapesChargeOnExecutedLegOnly() (gas: 686994)\n[PASS] test_redeemZeroRefused() (gas: 31767)\n[PASS] test_swapOnForeignKeyNamingTheHookIsRefused() (gas: 126722)\n[PASS] test_tinyExactOutputsBothDirections() (gas: 614660)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 150.58ms (444.89ms CPU time)\n\nRan 1 test for test/TreasuryInvariants.t.sol:TreasuryInvariantsTest\n[PASS]\nTreasuryInvariantsTest invariants:\n[PASS] invariant_hookFeeConservation\n[PASS] invariant_incomeConservation\n[PASS] invariant_managerSolventForClaims\n[PASS] invariant_purchasedPrioIsSplitAndForwarded\n[PASS] invariant_reserveCapped\n[PASS] invariant_treasuryEthIsFullyBucketed\n TreasuryInvariantsTest invariants (runs: 64, calls: 2560, reverts: 0)\n\n╭-----------------+------------------+-------+---------+----------╮\n| Contract        | Selector         | Calls | Reverts | Discards |\n+=================================================================+\n| TreasuryHandler | allocate         | 196   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | buyExactIn       | 235   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | buyExactOut      | 219   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | buyPrio          | 238   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | flush            | 204   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | redeemClaims     | 187   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | sellExactIn      | 205   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | sellExactOut     | 220   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | setReserveTarget | 238   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | warp             | 221   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | withdrawOwner    | 200   | 0       | 0        |\n|-----------------+------------------+-------+---------+----------|\n| TreasuryHandler | withdrawReserve  | 197   | 0       | 0        |\n╰-----------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 518.59ms (515.61ms CPU time)\n\nRan 1 test for test/ArenaInvariants.t.sol:ArenaInvariantsTest\n[PASS]\nArenaInvariantsTest invariants:\n[PASS] invariant_balanceFullyAccounted\n[PASS] invariant_playersNeverExtractMoreThanFunded\n[PASS] invariant_roundsAreFrozenOnceFinished\n[PASS] invariant_tokenConservation\n ArenaInvariantsTest invariants (runs: 64, calls: 3840, reverts: 198)\n\n╭--------------+---------------+-------+---------+----------╮\n| Contract     | Selector      | Calls | Reverts | Discards |\n+===========================================================+\n| ArenaHandler | cancel        | 484   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | claimOrRefund | 495   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | createRound   | 484   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | enter         | 462   | 198     | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | fund          | 460   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | reveal        | 483   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | settle        | 490   | 0       | 0        |\n|--------------+---------------+-------+---------+----------|\n| ArenaHandler | warp          | 482   | 0       | 0        |\n╰--------------+---------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 646.27ms (643.99ms CPU time)\n\nRan 17 test suites in 648.85ms (2.70s CPU time): 132 tests passed, 0 failed, 0 skipped (132 total tests)\n","passed":true},{"durationMs":56,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Arena.acceptOwnership()\",\"Arena.cancel(uint256)\",\"Arena.claim(uint256)\",\"Arena.createRound(uint8,uint8,uint64,uint64,uint64,uint256,uint16,bytes32)\",\"Arena.enter(uint256,bytes32)\",\"Arena.fundPrizes(uint256)\",\"Arena.refund(uint256)\",\"Arena.reveal(uint256,uint8,bytes32)\",\"Arena.setOracle(address)\",\"Arena.settle(uint256)\",\"Arena.transferOwnership(address)\",\"FeeTreasury.acceptOwnership()\",\"FeeTreasury.allocate()\",\"FeeTreasury.bindHook(address)\",\"FeeTreasury.buyImd(uint256,uint256)\",\"FeeTreasury.buyPrio(uint256,uint256)\",\"FeeTreasury.receive()\",\"FeeTreasury.setExecutor(address)\",\"FeeTreasury.setImd(address)\",\"FeeTreasury.setImdPool(uint24,int24,address)\",\"FeeTreasury.setMaxSpendPerSwap(uint256)\",\"FeeTreasury.setPrio(address)\",\"FeeTreasury.setReserveTarget(uint256)\",\"FeeTreasury.setSinks(address,address,address)\",\"FeeTreasury.transferOwnership(address)\",\"FeeTreasury.unlockCallback(bytes)\",\"FeeTreasury.withdrawOwner(address,uint256)\",\"FeeTreasury.withdrawReserve(address,uint256)\",\"OracleAdapter.acceptOwnership()\",\"OracleAdapter.clearStale(bytes32)\",\"OracleAdapter.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.pinQuestion(uint256,bytes32,uint256,uint16,uint16,uint64,bytes)\",\"OracleAdapter.request(uint256)\",\"OracleAdapter.setAction(bytes32)\",\"OracleAdapter.setBudget(uint256)\",\"OracleAdapter.setCallbackConfigured(bool)\",\"OracleAdapter.setExecutor(address)\",\"OracleAdapter.setIntake(address)\",\"OracleAdapter.setPayment(address,uint256)\",\"OracleAdapter.setSigner(address)\",\"OracleAdapter.submitAttestation(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.transferOwnership(address)\",\"OracleAdapter.withdrawToken(address,address,uint256)\",\"PrismRiotToken.approve(address,uint256)\",\"PrismRiotToken.transfer(address,uint256)\",\"PrismRiotToken.transferFrom(address,address,uint256)\",\"StakingVault.acceptOwnership()\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.notifyReward(uint256)\",\"StakingVault.setRewardFunder(address)\",\"StakingVault.setRewardsDuration(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.transferOwnership(address)\",\"StakingVault.withdraw(uint256)\",\"TreasuryFeeHook.acceptOwnership()\",\"TreasuryFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"TreasuryFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"TreasuryFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"TreasuryFeeHook.bindTreasury(address)\",\"TreasuryFeeHook.flush()\",\"TreasuryFeeHook.receive()\",\"TreasuryFeeHook.redeemClaims(uint256)\",\"TreasuryFeeHook.transferOwnership(address)\",\"TreasuryFeeHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":9,\"LICENSE\":9,\"README.md\":254,\"docs/OPERATOR.md\":49,\"docs/REVIEW.md\":51,\"docs/abi/Arena.json\":1016,\"docs/abi/FeeTreasury.json\":1073,\"docs/abi/OracleAdapter.json\":1429,\"docs/abi/PrismRiotToken.json\":328,\"docs/abi/StakingVault.json\":627,\"docs/abi/TreasuryFeeHook.json\":1505,\"foundry.toml\":27,\"operator/operator.example.json\":19,\"operator/operator.py\":279,\"operator/test_operator.py\":94,\"remappings.txt\":6,\"script/Deploy.s.sol\":66,\"src/Arena.sol\":321,\"src/FeeTreasury.sol\":280,\"src/OracleAdapter.sol\":269,\"src/OracleAttestation.sol\":125,\"src/PrismRiotToken.sol\":16,\"src/StakingVault.sol\":160,\"src/TreasuryFeeHook.sol\":343,\"src/TwoStepOwned.sol\":19,\"test/Arena.t.sol\":360,\"test/ArenaEdge.t.sol\":445,\"test/ArenaInvariants.t.sol\":286,\"test/Deploy.t.sol\":23,\"test/FeeTreasury.t.sol\":133,\"test/FeeTreasuryEdge.t.sol\":354,\"test/Invariants.t.sol\":142,\"test/OracleAdapter.t.sol\":265,\"test/OracleAdapterEdge.t.sol\":318,\"test/PrismRiotToken.t.sol\":37,\"test/StakingVault.t.sol\":104,\"test/StakingVaultEdge.t.sol\":268,\"test/TreasuryFeeHook.t.sol\":260,\"test/TreasuryFeeHookEdge.t.sol\":370,\"test/TreasuryInvariants.t.sol\":250,\"test/utils/Fixture.sol\":117,\"test/utils/MockIntake.sol\":36},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"25eda4ce6dce5fc5384770670bca58f4b1ba847e9e384b5103be4eb84284d4b4","verifiedTreeHash":"a7727b698de04b98d9f6207af97fc8c9b46c5829","verifierVersion":"0.1.0+c4d32abc"},{"checks":[{"durationMs":5188,"exitCode":0,"name":"build","output":"Compiling 118 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.89s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> script/Deploy.s.sol:63:5:\n   |\n63 |     function run() external {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/StakingVault.sol:62:30\n   │\n62 │     function setRewardFunder(address funder) external onlyOwner {\n   │                              ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/OracleAdapter.sol:138:26\n    │\n138 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:62\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                                                              ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:46\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                                              ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:137:23\n    │\n137 │     function setSinks(address stakingVault_, address arena_, address oracleAdapter_) external onlyOwner {\n    │                       ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/TreasuryFeeHook.sol:179:22\n    │\n179 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:144:26\n    │\n144 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/StakingVault.sol:77:16\n   │\n77 │         return block.timestamp < periodFinish ? block.timestamp : periodFinish;\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/TreasuryFeeHook.sol:179:22\n    │\n179 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:168:15\n    │\n168 │         if (!(block.timestamp < commitDeadline && commitDeadline < revealDeadline && revealDeadline < resultDeadline)) {\n    │               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:185:9\n    │\n185 │         emit FeeDelivered(treasury, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/TreasuryFeeHook.sol:193:9\n    │\n193 │         poolManager.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:277:22\n    │\n277 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:228:13\n    │\n228 │         if (block.timestamp >= r.commitDeadline) revert CommitClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/FeeTreasury.sol:200:83\n    │\n200 │     function withdrawOwner(address payable to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                                   ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:204:9\n    │\n204 │         emit OwnerWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:242:13\n    │\n242 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:242:51\n    │\n242 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:259:13\n    │\n259 │         if (block.timestamp < r.revealDeadline) revert RevealNotOver();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Arena.sol:264:25\n    │\n264 │         uint8 winning = uint8(res.answer % r.choiceCount) + 1;\n    │                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:284:13\n    │\n284 │         if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:203:9\n    │\n203 │         emit ClaimsRedeemed(amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:193:13\n    │\n193 │         if (block.timestamp >= windowStart + BUDGET_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:204:9\n    │\n204 │         emit Requested(roundId, intakeId, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:209:44\n    │\n209 │         if (pendingSince[intakeId] == 0 || block.timestamp < pendingSince[intakeId] + REQUEST_TIMEOUT) {\n    │                                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:213:9\n    │\n213 │         emit ReserveWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/StakingVault.sol:139:13\n    │\n139 │         if (block.timestamp < periodFinish) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:230:9\n    │\n230 │         IRewardSink(stakingVault).notifyReward(toStaking);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:232:9\n    │\n232 │         IPrizeSink(arena).fundPrizes(toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:252:31\n    │\n252 │         bytes memory result = poolManager.unlock(abi.encode(key, ethIn));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:233:9\n    │\n233 │         emit PrioBought(ethIn, out, toStaking, toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:240:45\n    │\n240 │             (fee, leg) = quoteBuyExactInput(uint256(-params.amountSpecified));\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:243:47\n    │\n243 │             (fee, leg) = quoteSellExactOutput(uint256(params.amountSpecified));\n    │                                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:246:63\n    │\n246 │         return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);\n    │                                                               ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:246:70\n    │\n246 │         return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);\n    │                                                                      ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:246:9\n    │\n246 │         emit ImdBought(ethIn, out);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:262:9\n    │\n262 │         emit ResultStored(roundId, answer, a.requestId, a.panelJobId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:38\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:46\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                              ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:68\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:262:76\n    │\n262 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                            ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:266:44\n    │\n266 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:266:51\n    │\n266 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                                   ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:341:9\n    │\n341 │         emit FeeCharged(poolId, zeroForOne, exactInput, leg, fee, asClaim);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:263:53\n    │\n263 │                 zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:267:24\n    │\n267 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:267:32\n    │\n267 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:268:23\n    │\n268 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:268:31\n    │\n268 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/FeeTreasury.sol:271:9\n    │\n271 │         poolManager.settle{value: owed}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/FeeTreasury.sol:277:22\n    │\n277 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/Arena.t.sol:49:40\n    │\n 49 │         uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n    │                                        ━━━━━━━━━━━━━━━\n    │\n    ⸬  lib/forge-std/src/StdCheats.sol:677:9\n    │\n677 │         vm.warp(vm.getBlockTimestamp() + time);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":238,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployAllPlacesHookOnFlaggedAddress() (gas: 23245771)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.21ms (9.30ms CPU time)\n\nRan 3 tests for test/PrismRiotToken.t.sol:PrismRiotTokenTest\n[PASS] test_metadataAndSupply() (gas: 44137)\n[PASS] test_noMintOrOwnerFunctions() (gas: 51960)\n[PASS] test_transferMovesExactly() (gas: 78817)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 15.72ms (1.31ms CPU time)\n\nRan 6 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] test_durationBounds() (gas: 67910)\n[PASS] test_leftoverRollsIntoNextStream() (gas: 431893)\n[PASS] test_noRewardsWithoutFunding() (gas: 209489)\n[PASS] test_onlyFunderOrOwnerNotifies() (gas: 41455)\n[PASS] test_rewardsProRataByStakeAndTime_andStopAtPeriodEnd() (gas: 646737)\n[PASS] test_stakeWithdrawKeepsPrincipalExact() (gas: 251959)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 18.76ms (10.86ms CPU time)\n\nRan 3 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookClaimsTest\n[PASS] test_buyOnTokenOnlyPoolMintsClaimThenRedeems() (gas: 651315)\n[PASS] test_redeemRefusesMoreThanHeld() (gas: 69782)\n[PASS] test_secondBuyPaysDirectlyOnceManagerHoldsEth() (gas: 740396)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 19.22ms (1.94ms CPU time)\n\nRan 7 tests for test/FeeTreasury.t.sol:FeeTreasuryTest\n[PASS] test_allocationSplitAndReserveCap() (gas: 543549)\n[PASS] test_buyPrioGuards() (gas: 842894)\n[PASS] test_buyPrioSplitsBetweenStakingAndArena() (gas: 953462)\n[PASS] test_imdPurchaseWaitsForConfiguration_prioIndependent() (gas: 904523)\n[PASS] test_onlyHookCanFund_noOwnerAdvances() (gas: 43633)\n[PASS] test_ownerAndReserveWithdrawalsAreCapped() (gas: 673836)\n[PASS] test_reserveStopsAtTarget() (gas: 840621)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 20.12ms (4.83ms CPU time)\n\nRan 9 tests for test/Arena.t.sol:ArenaTest\n[PASS] test_bossChallengeNeedsThreshold() (gas: 1011509)\n[PASS] test_cancelAfter72hRefundsEverything() (gas: 1095210)\n[PASS] test_constantsPublished() (gas: 16377)\n[PASS] test_entryRules() (gas: 653340)\n[PASS] test_noWinnerReturnsPrizeToPool() (gas: 828434)\n[PASS] test_oldRoundStaysClaimableAfterNewRounds() (gas: 1279521)\n[PASS] test_resultIssuedBeforeCommitBoundaryCannotSettle() (gas: 429691)\n[PASS] test_roundCreationRules() (gas: 132179)\n[PASS] test_vaultRaidFullLifecycle() (gas: 1620787)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 24.05ms (47.89ms CPU time)\n\nRan 10 tests for test/OracleAdapter.t.sol:OracleAdapterTest\n[PASS] test_budgetAndExecutorEnforced() (gas: 952569)\n[PASS] test_callbackRefusesWrongSenderAndUnknownId() (gas: 683201)\n[PASS] test_callbackSelectorIsCanonical() (gas: 507)\n[PASS] test_clearStaleAfterTimeout() (gas: 657965)\n[PASS] test_digestMatchesTheProtocol() (gas: 10880)\n[PASS] test_manualRelayStoresResultAndEvidence() (gas: 225809)\n[PASS] test_paidRequestDisabledUntilConfigured() (gas: 99204)\n[PASS] test_paidRequestThenCallbackUnder200kGas() (gas: 854250)\n[PASS] test_relayRejectsReplayAndSecondResult() (gas: 393159)\n[PASS] test_relayRejectsWrongQuestionChainQuorumPanelSignerExpiryAndType() (gas: 610171)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 42.39ms (36.05ms CPU time)\n\nRan 14 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookTest\n[PASS] testFuzz_feeChargedMatchesTreasuryIncome(uint96,bool,bool) (runs: 256, μ: 372987, ~: 377833)\n[PASS] testFuzz_quotesAreConsistentWeiLevel(uint128) (runs: 256, μ: 22585, ~: 22585)\n[PASS] test_buyExactInput_feeFromEthInput() (gas: 374625)\n[PASS] test_buyExactOutput_feeOnTopOfEthCharged() (gas: 376464)\n[PASS] test_callbacksRefuseNonManager() (gas: 123093)\n[PASS] test_constructorRefusesMismatchedAddress() (gas: 71766)\n[PASS] test_feeOnLegRoundsUpAtWeiLevel() (gas: 22980)\n[PASS] test_initializeOnFreshHookRefusesWrongPair() (gas: 340433)\n[PASS] test_initializeRefusesNonFactoryAndOtherPools() (gas: 47998)\n[PASS] test_otherDirectionsLeaveTokenTransfersUntaxed() (gas: 74556)\n[PASS] test_permissionsAndAddressAgree() (gas: 31486)\n[PASS] test_sellExactInput_feeOutOfEthOutput() (gas: 343952)\n[PASS] test_sellExactOutput_swapperGetsExactlyTheEthAsked() (gas: 350366)\n[PASS] test_treasuryBindsOnce() (gas: 63780)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 42.39ms (32.74ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:InvariantsTest\n[PASS]\nInvariantsTest invariants:\n[PASS] invariant_arenaBalanceIsFullyAccounted\n[PASS] invariant_vaultCoversPrincipalAndOwedRewards\n InvariantsTest invariants (runs: 32, calls: 1024, reverts: 0)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 277   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | enter    | 151   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | fund     | 156   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | stake    | 154   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | warp     | 148   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 138   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 93.95ms (77.63ms CPU time)\n\nRan 9 test suites in 103.24ms (291.81ms CPU time): 54 tests passed, 0 failed, 0 skipped (54 total tests)\n","passed":true},{"durationMs":83,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Arena.acceptOwnership()\",\"Arena.cancel(uint256)\",\"Arena.claim(uint256)\",\"Arena.createRound(uint8,uint8,uint64,uint64,uint64,uint256,uint16,bytes32)\",\"Arena.enter(uint256,bytes32)\",\"Arena.fundPrizes(uint256)\",\"Arena.refund(uint256)\",\"Arena.reveal(uint256,uint8,bytes32)\",\"Arena.setOracle(address)\",\"Arena.settle(uint256)\",\"Arena.transferOwnership(address)\",\"FeeTreasury.acceptOwnership()\",\"FeeTreasury.allocate()\",\"FeeTreasury.bindHook(address)\",\"FeeTreasury.buyImd(uint256,uint256)\",\"FeeTreasury.buyPrio(uint256,uint256)\",\"FeeTreasury.receive()\",\"FeeTreasury.setExecutor(address)\",\"FeeTreasury.setImd(address)\",\"FeeTreasury.setImdPool(uint24,int24,address)\",\"FeeTreasury.setMaxSpendPerSwap(uint256)\",\"FeeTreasury.setPrio(address)\",\"FeeTreasury.setReserveTarget(uint256)\",\"FeeTreasury.setSinks(address,address,address)\",\"FeeTreasury.transferOwnership(address)\",\"FeeTreasury.unlockCallback(bytes)\",\"FeeTreasury.withdrawOwner(address,uint256)\",\"FeeTreasury.withdrawReserve(address,uint256)\",\"OracleAdapter.acceptOwnership()\",\"OracleAdapter.clearStale(bytes32)\",\"OracleAdapter.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.pinQuestion(uint256,bytes32,uint256,uint16,uint16,uint64,bytes)\",\"OracleAdapter.request(uint256)\",\"OracleAdapter.setAction(bytes32)\",\"OracleAdapter.setBudget(uint256)\",\"OracleAdapter.setCallbackConfigured(bool)\",\"OracleAdapter.setExecutor(address)\",\"OracleAdapter.setIntake(address)\",\"OracleAdapter.setPayment(address,uint256)\",\"OracleAdapter.setSigner(address)\",\"OracleAdapter.submitAttestation(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.transferOwnership(address)\",\"OracleAdapter.withdrawToken(address,address,uint256)\",\"PrismRiotToken.approve(address,uint256)\",\"PrismRiotToken.transfer(address,uint256)\",\"PrismRiotToken.transferFrom(address,address,uint256)\",\"StakingVault.acceptOwnership()\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.notifyReward(uint256)\",\"StakingVault.setRewardFunder(address)\",\"StakingVault.setRewardsDuration(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.transferOwnership(address)\",\"StakingVault.withdraw(uint256)\",\"TreasuryFeeHook.acceptOwnership()\",\"TreasuryFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"TreasuryFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"TreasuryFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"TreasuryFeeHook.bindTreasury(address)\",\"TreasuryFeeHook.flush()\",\"TreasuryFeeHook.receive()\",\"TreasuryFeeHook.redeemClaims(uint256)\",\"TreasuryFeeHook.transferOwnership(address)\",\"TreasuryFeeHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":9,\"LICENSE\":9,\"README.md\":254,\"docs/OPERATOR.md\":49,\"docs/REVIEW.md\":51,\"docs/abi/Arena.json\":1016,\"docs/abi/FeeTreasury.json\":1073,\"docs/abi/OracleAdapter.json\":1429,\"docs/abi/PrismRiotToken.json\":328,\"docs/abi/StakingVault.json\":627,\"docs/abi/TreasuryFeeHook.json\":1505,\"foundry.toml\":27,\"launch.json\":32,\"operator/operator.example.json\":19,\"operator/operator.py\":279,\"operator/test_operator.py\":94,\"remappings.txt\":6,\"script/Deploy.s.sol\":66,\"src/Arena.sol\":321,\"src/FeeTreasury.sol\":280,\"src/OracleAdapter.sol\":269,\"src/OracleAttestation.sol\":125,\"src/PrismRiotToken.sol\":16,\"src/StakingVault.sol\":160,\"src/TreasuryFeeHook.sol\":343,\"src/TwoStepOwned.sol\":19,\"test/Arena.t.sol\":360,\"test/Deploy.t.sol\":23,\"test/FeeTreasury.t.sol\":133,\"test/Invariants.t.sol\":142,\"test/OracleAdapter.t.sol\":265,\"test/PrismRiotToken.t.sol\":37,\"test/StakingVault.t.sol\":104,\"test/TreasuryFeeHook.t.sol\":260,\"test/utils/Fixture.sol\":117,\"test/utils/MockIntake.sol\":36},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"bd519b8139a9d7f52b2cae5561559ae28ab633de388a5cb245fc2a4525c641f6","verifiedTreeHash":"3402b02fc8c2cd0635b86cd196071658e45c50c0","verifierVersion":"0.1.0+c4d32abc"},{"checks":[{"durationMs":5610,"exitCode":0,"name":"build","output":"Compiling 118 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.42s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> script/Deploy.s.sol:63:5:\n   |\n63 |     function run() external {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/OracleAdapter.sol:145:26\n    │\n145 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:189:15\n    │\n189 │         if (!(block.timestamp < commitDeadline && commitDeadline < revealDeadline && revealDeadline < resultDeadline)) {\n    │               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/StakingVault.sol:66:30\n   │\n66 │     function setRewardFunder(address funder) external onlyOwner {\n   │                              ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:167:26\n    │\n167 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/TreasuryFeeHook.sol:200:22\n    │\n200 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:254:13\n    │\n254 │         if (block.timestamp >= r.commitDeadline) revert CommitClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/TreasuryFeeHook.sol:200:22\n    │\n200 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/StakingVault.sol:81:16\n   │\n81 │         return block.timestamp < periodFinish ? block.timestamp : periodFinish;\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:206:9\n    │\n206 │         emit FeeDelivered(treasury, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:268:13\n    │\n268 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:268:51\n    │\n268 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/TreasuryFeeHook.sol:214:9\n    │\n214 │         poolManager.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:331:22\n    │\n331 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/FeeTreasury.sol:236:83\n    │\n236 │     function withdrawOwner(address payable to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                                   ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:240:9\n    │\n240 │         emit OwnerWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:296:13\n    │\n296 │         if (block.timestamp < r.revealDeadline) revert RevealNotOver();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Arena.sol:300:25\n    │\n300 │         uint8 winning = uint8(res.answer % r.choiceCount) + 1;\n    │                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:321:13\n    │\n321 │         if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:204:13\n    │\n204 │         if (block.timestamp >= windowStart + BUDGET_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:215:9\n    │\n215 │         emit Requested(roundId, intakeId, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:220:44\n    │\n220 │         if (pendingSince[intakeId] == 0 || block.timestamp < pendingSince[intakeId] + REQUEST_TIMEOUT) {\n    │                                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:224:9\n    │\n224 │         emit ClaimsRedeemed(amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:249:9\n    │\n249 │         emit ReserveWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-no-eth]: external call can be reentered before `spentInWindow` is updated\n    ╭▸ src/FeeTreasury.sol:305:31\n    │\n305 │         bytes memory result = poolManager.unlock(abi.encode(key, ethIn));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:269:9\n    │\n269 │         IRewardSink(stakingVault).notifyReward(toStaking);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:271:9\n    │\n271 │         IPrizeSink(arena).fundPrizes(toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:272:9\n    │\n272 │         emit PrioBought(spent, out, toStaking, toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/StakingVault.sol:144:13\n    │\n144 │         if (block.timestamp < periodFinish) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:276:9\n    │\n276 │         emit ResultStored(roundId, answer, a.requestId, a.panelJobId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:288:9\n    │\n288 │         emit ImdBought(spent, out);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/FeeTreasury.sol:300:13\n    │\n300 │         if (block.timestamp >= windowStart + SPEND_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:267:30\n    │\n267 │             ? _buyExactInput(uint256(-params.amountSpecified), params.sqrtPriceLimitX96)\n    │                              ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:268:32\n    │\n268 │             : _sellExactOutput(uint256(params.amountSpecified), params.sqrtPriceLimitX96);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:317:53\n    │\n317 │                 zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:321:24\n    │\n321 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:321:32\n    │\n321 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:322:23\n    │\n322 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:322:31\n    │\n322 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/FeeTreasury.sol:325:9\n    │\n325 │         poolManager.settle{value: owed}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/FeeTreasury.sol:331:22\n    │\n331 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:289:13\n    │\n289 │         if (filled != leg) fee = feeOnLeg(filled);\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:292:62\n    │\n292 │         return toBeforeSwapDelta((filled + fee).toInt128(), -prioOut.toInt128());\n    │                                                              ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:420:9\n    │\n420 │         emit FeeCharged(poolId, zeroForOne, exactInput, leg, fee, asClaim);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:286:22\n    │\n286 │             filled = uint256(uint128(-d.amount0()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:286:30\n    │\n286 │             filled = uint256(uint128(-d.amount0()));\n    │                              ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:287:23\n    │\n287 │             prioOut = uint256(uint128(d.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:287:31\n    │\n287 │             prioOut = uint256(uint128(d.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:312:13\n    │\n312 │         if (filled != leg) {\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:324:51\n    │\n324 │         return toBeforeSwapDelta(-pay.toInt128(), prioIn.toInt128());\n    │                                                   ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:308:22\n    │\n308 │             filled = uint256(uint128(d.amount0()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:308:30\n    │\n308 │             filled = uint256(uint128(d.amount0()));\n    │                              ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:309:22\n    │\n309 │             prioIn = uint256(uint128(-d.amount1()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:309:30\n    │\n309 │             prioIn = uint256(uint128(-d.amount1()));\n    │                              ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:38\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:46\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                              ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:68\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:76\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                            ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:345:44\n    │\n345 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:345:51\n    │\n345 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                                   ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/Arena.t.sol:49:40\n    │\n 49 │         uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n    │                                        ━━━━━━━━━━━━━━━\n    │\n    ⸬  lib/forge-std/src/StdCheats.sol:677:9\n    │\n677 │         vm.warp(vm.getBlockTimestamp() + time);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":197,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/PrismRiotToken.t.sol:PrismRiotTokenTest\n[PASS] test_metadataAndSupply() (gas: 44137)\n[PASS] test_noMintOrOwnerFunctions() (gas: 51960)\n[PASS] test_transferMovesExactly() (gas: 78817)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.19ms (475.39µs CPU time)\n\nRan 9 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] test_durationBounds() (gas: 67910)\n[PASS] test_leftoverRollsIntoNextStream() (gas: 432247)\n[PASS] test_noRewardsWithoutFunding() (gas: 204391)\n[PASS] test_notifyWhileIdleKeepsRemainderAndRestartsSchedule() (gas: 391149)\n[PASS] test_onlyFunderOrOwnerNotifies() (gas: 41455)\n[PASS] test_rewardsProRataByStakeAndTime_andStopAtPeriodEnd() (gas: 643629)\n[PASS] test_stakeWithdrawKeepsPrincipalExact() (gas: 249915)\n[PASS] test_streamPausesAgainWhenEveryoneLeaves() (gas: 675018)\n[PASS] test_streamPausesWhileNothingIsStaked_nothingStranded() (gas: 473558)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 2.64ms (2.06ms CPU time)\n\nRan 13 tests for test/OracleAdapter.t.sol:OracleAdapterTest\n[PASS] test_budgetAndExecutorEnforced() (gas: 961255)\n[PASS] test_callbackRefusesWrongSenderAndUnknownId() (gas: 685444)\n[PASS] test_callbackSelectorIsCanonical() (gas: 529)\n[PASS] test_clearStaleAfterTimeout() (gas: 660165)\n[PASS] test_digestMatchesTheProtocol() (gas: 10836)\n[PASS] test_manualRelayStoresResultAndEvidence() (gas: 224019)\n[PASS] test_nothingIsStoredOrRequestedBeforeTheBoundary() (gas: 999103)\n[PASS] test_paidRequestDisabledUntilConfigured() (gas: 99249)\n[PASS] test_paidRequestThenCallbackUnder200kGas() (gas: 854626)\n[PASS] test_pinnedSignerOutlivesRotation() (gas: 420903)\n[PASS] test_relayRejectsReplayAndSecondResult() (gas: 391853)\n[PASS] test_relayRejectsWrongQuestionChainQuorumPanelSignerExpiryAndType() (gas: 604372)\n[PASS] test_withdrawTokenRefusesTheConfiguredAsset() (gas: 444917)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 3.64ms (9.30ms CPU time)\n\nRan 14 tests for test/Arena.t.sol:ArenaTest\n[PASS] test_bossChallengeNeedsThreshold() (gas: 1138514)\n[PASS] test_cancelAfter72hRefundsEverything() (gas: 1240175)\n[PASS] test_constantsPublished() (gas: 16224)\n[PASS] test_createRoundNeedsPinnedQuestionAtTheCommitBoundary() (gas: 361560)\n[PASS] test_entryRules() (gas: 777445)\n[PASS] test_noWinnerReturnsPrizeToPool() (gas: 959998)\n[PASS] test_oldRoundStaysClaimableAfterNewRounds() (gas: 1535638)\n[PASS] test_ownerCannotSwapOracleForAnOpenRound() (gas: 1856152)\n[PASS] test_resolvedRoundCannotBeCancelled_onlySettled() (gas: 1199743)\n[PASS] test_resultIssuedBeforeCommitBoundaryCannotSettle() (gas: 556639)\n[PASS] test_roundCreationRules() (gas: 132179)\n[PASS] test_signerRotationDoesNotReachPinnedRounds() (gas: 803200)\n[PASS] test_unresolvedRoundCancelsAndALateResultCannotSettleIt() (gas: 858249)\n[PASS] test_vaultRaidFullLifecycle() (gas: 1752356)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 5.49ms (9.84ms CPU time)\n\nRan 3 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookClaimsTest\n[PASS] test_buyOnTokenOnlyPoolMintsClaimThenRedeems() (gas: 673609)\n[PASS] test_redeemRefusesMoreThanHeld() (gas: 75515)\n[PASS] test_secondBuyPaysDirectlyOnceManagerHoldsEth() (gas: 781711)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 6.20ms (5.56ms CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployAllPlacesHookOnFlaggedAddress() (gas: 27273735)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.16ms (6.56ms CPU time)\n\nRan 11 tests for test/FeeTreasury.t.sol:FeeTreasuryTest\n[PASS] test_allocationSplitAndReserveCap() (gas: 562896)\n[PASS] test_buyPrioGuards() (gas: 994911)\n[PASS] test_buyPrioSplitsBetweenStakingAndArena() (gas: 1013065)\n[PASS] test_imdPurchaseWaitsForConfiguration_prioIndependent() (gas: 964127)\n[PASS] test_onlyHookCanFund_noOwnerAdvances() (gas: 43633)\n[PASS] test_ownerAndReserveWithdrawalsAreCapped() (gas: 692965)\n[PASS] test_partialFillCreditsUnspentEthBackToTheBudget() (gas: 1833934)\n[PASS] test_reserveStopsAtTarget() (gas: 878659)\n[PASS] test_rollingWindowAndPriceFloorBoundTheExecutor() (gas: 2247675)\n[PASS] test_setImdUnsetsThePoolKey() (gas: 751231)\n[PASS] test_sinksAreSetOnceAndNonZero() (gas: 165567)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 9.00ms (9.10ms CPU time)\n\nRan 16 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookTest\n[PASS] testFuzz_feeChargedMatchesTreasuryIncome(uint96,bool,bool) (runs: 256, μ: 381469, ~: 379014)\n[PASS] testFuzz_quotesAreConsistentWeiLevel(uint128) (runs: 256, μ: 22607, ~: 22607)\n[PASS] test_buyExactInput_feeFromEthInput() (gas: 393752)\n[PASS] test_buyExactOutput_feeOnTopOfEthCharged() (gas: 376548)\n[PASS] test_callbacksRefuseNonManager() (gas: 123049)\n[PASS] test_constructorRefusesMismatchedAddress() (gas: 74525)\n[PASS] test_feeOnLegRoundsUpAtWeiLevel() (gas: 22980)\n[PASS] test_initializeOnFreshHookRefusesWrongPair() (gas: 344774)\n[PASS] test_initializeRefusesNonFactoryAndOtherPools() (gas: 48021)\n[PASS] test_oneWeiBuyIsRefusedNotSwallowed() (gas: 439366)\n[PASS] test_otherDirectionsLeaveTokenTransfersUntaxed() (gas: 74600)\n[PASS] test_permissionsAndAddressAgree() (gas: 31553)\n[PASS] test_receiveRefusesEveryoneButThePoolManager() (gas: 38301)\n[PASS] test_sellExactInput_feeOutOfEthOutput() (gas: 343970)\n[PASS] test_sellExactOutput_swapperGetsExactlyTheEthAsked() (gas: 369304)\n[PASS] test_treasuryBindingIsCorrectableUntilFirstDelivery_thenImmutable() (gas: 593116)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 28.89ms (31.53ms CPU time)\n\nRan 7 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookPartialFillTest\n[PASS] testFuzz_partialFillFeeIsAlwaysOnTheFilledLeg(uint96,bool) (runs: 256, μ: 670090, ~: 745558)\n[PASS] test_buyExactInput_liquidityExhausted_feeIsHalfPercentOfFilledLeg() (gas: 766277)\n[PASS] test_buyExactInput_priceLimit_feeIsHalfPercentOfFilledLeg() (gas: 410658)\n[PASS] test_fullFillsStillCostExactlyWhatWasSpecified() (gas: 634061)\n[PASS] test_limitNextToSpotMovesNothingAndChargesNothing() (gas: 233336)\n[PASS] test_sellExactOutput_liquidityExhausted_sellerNeverPaysEth() (gas: 763001)\n[PASS] test_sellExactOutput_priceLimit_feeIsHalfPercentOfFilledLeg() (gas: 381614)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 53.48ms (52.51ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:InvariantsTest\n[PASS]\nInvariantsTest invariants:\n[PASS] invariant_arenaBalanceIsFullyAccounted\n[PASS] invariant_vaultCoversPrincipalAndOwedRewards\n InvariantsTest invariants (runs: 32, calls: 1024, reverts: 2)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 278   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | enter    | 140   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | fund     | 148   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | stake    | 147   | 2       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | warp     | 165   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 146   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 71.58ms (69.67ms CPU time)\n\nRan 10 test suites in 74.69ms (189.26ms CPU time): 78 tests passed, 0 failed, 0 skipped (78 total tests)\n","passed":true},{"durationMs":94,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Arena.acceptOwnership()\",\"Arena.cancel(uint256)\",\"Arena.claim(uint256)\",\"Arena.createRound(uint8,uint8,uint64,uint64,uint64,uint256,uint16,bytes32)\",\"Arena.enter(uint256,bytes32)\",\"Arena.fundPrizes(uint256)\",\"Arena.refund(uint256)\",\"Arena.reveal(uint256,uint8,bytes32)\",\"Arena.setOracle(address)\",\"Arena.settle(uint256)\",\"Arena.transferOwnership(address)\",\"FeeTreasury.acceptOwnership()\",\"FeeTreasury.allocate()\",\"FeeTreasury.bindHook(address)\",\"FeeTreasury.buyImd(uint256,uint256)\",\"FeeTreasury.buyPrio(uint256,uint256)\",\"FeeTreasury.receive()\",\"FeeTreasury.setExecutor(address)\",\"FeeTreasury.setImd(address)\",\"FeeTreasury.setImdPool(uint24,int24,address)\",\"FeeTreasury.setMaxSpendPerSwap(uint256)\",\"FeeTreasury.setPriceFloors(uint256,uint256)\",\"FeeTreasury.setPrio(address)\",\"FeeTreasury.setReserveTarget(uint256)\",\"FeeTreasury.setSinks(address,address,address)\",\"FeeTreasury.setSpendPerWindow(uint256)\",\"FeeTreasury.transferOwnership(address)\",\"FeeTreasury.unlockCallback(bytes)\",\"FeeTreasury.withdrawOwner(address,uint256)\",\"FeeTreasury.withdrawReserve(address,uint256)\",\"OracleAdapter.acceptOwnership()\",\"OracleAdapter.clearStale(bytes32)\",\"OracleAdapter.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.pinQuestion(uint256,bytes32,uint256,uint16,uint16,uint64,bytes)\",\"OracleAdapter.request(uint256)\",\"OracleAdapter.setAction(bytes32)\",\"OracleAdapter.setBudget(uint256)\",\"OracleAdapter.setCallbackConfigured(bool)\",\"OracleAdapter.setExecutor(address)\",\"OracleAdapter.setIntake(address)\",\"OracleAdapter.setPayment(address,uint256)\",\"OracleAdapter.setSigner(address)\",\"OracleAdapter.submitAttestation(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.transferOwnership(address)\",\"OracleAdapter.withdrawToken(address,address,uint256)\",\"PrismRiotToken.approve(address,uint256)\",\"PrismRiotToken.transfer(address,uint256)\",\"PrismRiotToken.transferFrom(address,address,uint256)\",\"StakingVault.acceptOwnership()\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.notifyReward(uint256)\",\"StakingVault.setRewardFunder(address)\",\"StakingVault.setRewardsDuration(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.transferOwnership(address)\",\"StakingVault.withdraw(uint256)\",\"TreasuryFeeHook.acceptOwnership()\",\"TreasuryFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"TreasuryFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"TreasuryFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"TreasuryFeeHook.bindTreasury(address)\",\"TreasuryFeeHook.flush()\",\"TreasuryFeeHook.receive()\",\"TreasuryFeeHook.redeemClaims(uint256)\",\"TreasuryFeeHook.transferOwnership(address)\",\"TreasuryFeeHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":9,\"LICENSE\":9,\"README.md\":308,\"docs/OPERATOR.md\":55,\"docs/REVIEW.md\":79,\"docs/abi/Arena.json\":1055,\"docs/abi/FeeTreasury.json\":1224,\"docs/abi/OracleAdapter.json\":1450,\"docs/abi/PrismRiotToken.json\":328,\"docs/abi/StakingVault.json\":627,\"docs/abi/TreasuryFeeHook.json\":1515,\"foundry.toml\":27,\"operator/operator.example.json\":19,\"operator/operator.py\":294,\"operator/test_operator.py\":110,\"remappings.txt\":6,\"script/Deploy.s.sol\":66,\"src/Arena.sol\":359,\"src/FeeTreasury.sol\":334,\"src/OracleAdapter.sol\":298,\"src/OracleAttestation.sol\":125,\"src/PrismRiotToken.sol\":16,\"src/StakingVault.sol\":177,\"src/TreasuryFeeHook.sol\":422,\"src/TwoStepOwned.sol\":19,\"test/Arena.t.sol\":464,\"test/Deploy.t.sol\":23,\"test/FeeTreasury.t.sol\":235,\"test/Invariants.t.sol\":155,\"test/OracleAdapter.t.sol\":320,\"test/PrismRiotToken.t.sol\":37,\"test/StakingVault.t.sol\":156,\"test/TreasuryFeeHook.t.sol\":415,\"test/utils/Fixture.sol\":117,\"test/utils/MockIntake.sol\":36},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":5025,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/FeeTreasury.sol:330: FeeTreasury._send(address,uint256) (src/FeeTreasury.sol#330-333) sends eth to arbitrary user\n[high/medium] arbitrary-send-eth at src/TreasuryFeeHook.sol:195: TreasuryFeeHook.flush() (src/TreasuryFeeHook.sol#195-207) sends eth to arbitrary user\n[high/medium] reentrancy-eth at src/TreasuryFeeHook.sol:195: Reentrancy in TreasuryFeeHook.flush() (src/TreasuryFeeHook.sol#195-207):\n[medium/medium] divide-before-multiply at src/StakingVault.sol:138: StakingVault.notifyReward(uint256) (src/StakingVault.sol#138-155) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/StakingVault.sol:120: StakingVault.claim() (src/StakingVault.sol#120-128) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/OracleAdapter.sol:219: OracleAdapter.clearStale(bytes32) (src/OracleAdapter.sol#219-226) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/OracleAdapter.sol:231: OracleAdapter.onOracleResult(bytes32,OracleAttestation.Attestation,bytes) (src/OracleAdapter.sol#231-240) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/FeeTreasury.sol:256: Reentrancy in FeeTreasury.buyPrio(uint256,uint256) (src/FeeTreasury.sol#256-273):\n[medium/medium] reentrancy-no-eth at src/FeeTreasury.sol:295: Reentrancy in FeeTreasury._swapEthFor(PoolKey,uint256,uint256,uint256) (src/FeeTreasury.sol#295-309):\n[medium/medium] reentrancy-no-eth at src/FeeTreasury.sol:276: Reentrancy in FeeTreasury.buyImd(uint256,uint256) (src/FeeTreasury.sol#276-289):\n[medium/medium] uninitialized-local at src/TreasuryFeeHook.sol:278: TreasuryFeeHook._buyExactInput(uint256,uint160).filled (src/TreasuryFeeHook.sol#278) is a local variable never initialized\n[medium/medium] uninitialized-local at src/TreasuryFeeHook.sol:301: TreasuryFeeHook._sellExactOutput(uint256,uint160).prioIn (src/TreasuryFeeHook.sol#301) is a local variable never initialized\n[medium/medium] uninitialized-local at src/TreasuryFeeHook.sol:279: TreasuryFeeHook._buyExactInput(uint256,uint160).prioOut (src/TreasuryFeeHook.sol#279) is a local variable never initialized\n[medium/medium] uninitialized-local at src/TreasuryFeeHook.sol:300: TreasuryFeeHook._sellExactOutput(uint256,uint160).filled (src/TreasuryFeeHook.sol#300) is a local variable never initialized\n[medium/medium] unused-return at src/FeeTreasury.sol:311: FeeTreasury.unlockCallback(bytes) (src/FeeTreasury.sol#311-328) ignores return value by poolManager.settle{value: owed}() (src/FeeTreasury.sol#325)\n[medium/medium] unused-return at src/TreasuryFeeHook.sol:274: TreasuryFeeHook._buyExactInput(uint256,uint160) (src/TreasuryFeeHook.sol#274-293) ignores return value by (price,None,None,None) = poolManager.getSlot0(poolId) (src/TreasuryFeeHook.sol#277)\n[medium/medium] unused-return at src/TreasuryFeeHook.sol:297: TreasuryFeeHook._sellExactOutput(uint256,uint160) (src/TreasuryFeeHook.sol#297-325) ignores return value by (price,None,None,None) = poolManager.getSlot0(poolId) (src/TreasuryFeeHook.sol#299)\n[medium/medium] unused-return at src/TreasuryFeeHook.sol:211: TreasuryFeeHook.redeemClaims(uint256) (src/TreasuryFeeHook.sol#211-216) ignores return value by poolManager.unlock(abi.encode(amount)) (src/TreasuryFeeHook.sol#214)\n[low/medium] missing-zero-check at src/OracleAdapter.sol:145: OracleAdapter.setExecutor(address).to (src/OracleAdapter.sol#145) lacks a zero-check on :\n[low/medium] missing-zero-check at src/FeeTreasury.sol:167: FeeTreasury.setExecutor(address).to (src/FeeTreasury.sol#167) lacks a zero-check on :\n[low/medium] missing-zero-check at src/StakingVault.sol:66: StakingVault.setRewardFunder(address).funder (src/StakingVault.sol#66) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/TreasuryFeeHook.sol:195: Reentrancy in TreasuryFeeHook.flush() (src/TreasuryFeeHook.sol#195-207):\n[low/medium] reentrancy-benign at src/OracleAdapter.sol:196: Reentrancy in OracleAdapter.request(uint256) (src/OracleAdapter.sol#196-216):\n[low/medium] reentrancy-benign at src/TreasuryFeeHook.sol:218: Reentrancy in TreasuryFeeHook.unlockCallback(bytes) (src/TreasuryFeeHook.sol#218-226):\n[low/medium] reentrancy-benign at src/TreasuryFeeHook.sol:409: Reentrancy in TreasuryFeeHook._collect(uint256,bool,bool,uint256) (src/TreasuryFeeHook.sol#409-421):\n[low/medium] reentrancy-benign at src/TreasuryFeeHook.sol:409: Reentrancy in TreasuryFeeHook._collect(uint256,bool,bool,uint256) (src/TreasuryFeeHook.sol#409-421):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:195: Reentrancy in TreasuryFeeHook.flush() (src/TreasuryFeeHook.sol#195-207):\n[low/medium] reentrancy-events at src/OracleAdapter.sol:196: Reentrancy in OracleAdapter.request(uint256) (src/OracleAdapter.sol#196-216):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:274: Reentrancy in TreasuryFeeHook._buyExactInput(uint256,uint160) (src/TreasuryFeeHook.sol#274-293):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:409: Reentrancy in TreasuryFeeHook._collect(uint256,bool,bool,uint256) (src/TreasuryFeeHook.sol#409-421):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:297: Reentrancy in TreasuryFeeHook._sellExactOutput(uint256,uint160) (src/TreasuryFeeHook.sol#297-325):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:211: Reentrancy in TreasuryFeeHook.redeemClaims(uint256) (src/TreasuryFeeHook.sol#211-216):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:409: Reentrancy in TreasuryFeeHook._collect(uint256,bool,bool,uint256) (src/TreasuryFeeHook.sol#409-421):\n[low/medium] reentrancy-events at src/TreasuryFeeHook.sol:218: Reentrancy in TreasuryFeeHook.unlockCallback(bytes) (src/TreasuryFeeHook.sol#218-226):\n[low/medium] timestamp at src/OracleAdapter.sol:231: OracleAdapter.onOracleResult(bytes32,OracleAttestation.Attestation,bytes) (src/OracleAdapter.sol#231-240) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:120: StakingVault.claim() (src/StakingVault.sol#120-128) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:159: StakingVault._update(address) (src/StakingVault.sol#159-176) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAdapter.sol:219: OracleAdapter.clearStale(bytes32) (src/OracleAdapter.sol#219-226) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:265: Arena.reveal(uint256,uint8,bytes32) (src/Arena.sol#265-277) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:138: StakingVault.notifyReward(uint256) (src/StakingVault.sol#138-155) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAttestation.sol:91: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/OracleAttestation.sol#91-99) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAdapter.sol:281: OracleAdapter._verifyAttestationBy(address,OracleAttestation.Attestation,bytes) (src/OracleAdapter.sol#281-290) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAdapter.sol:249: OracleAdapter._accept(uint256,OracleAttestation.Attestation,bytes) (src/OracleAdapter.sol#249-277) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:251: Arena.enter(uint256,bytes32) (src/Arena.sol#251-263) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:318: Arena.cancel(uint256) (src/Arena.sol#318-327) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:84: StakingVault.rewardPerToken() (src/StakingVault.sol#84-88) uses timestamp for comparisons\n[low/medium] timestamp at src/FeeTreasury.sol:295: FeeTreasury._swapEthFor(PoolKey,uint256,uint256,uint256) (src/FeeTreasury.sol#295-309) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:293: Arena.settle(uint256) (src/Arena.sol#293-314) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:80: StakingVault.lastTimeRewardApplicable() (src/StakingVault.sol#80-82) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAdapter.sol:196: OracleAdapter.request(uint256) (src/OracleAdapter.sol#196-216) uses timestamp for comparisons\n[low/medium] timestamp at src/Arena.sol:175: Arena.createRound(Arena.Mode,uint8,uint64,uint64,uint64,uint256,uint16,bytes32) (src/Arena.sol#175-212) uses timestamp for comparisons","passed":true},{"durationMs":1235,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/Arena.sol:194: Reentrancy: State change after external call (8 places)\n[high] storage-array-memory-edit at src/FeeTreasury.sol:285: Storage Array Edited with Memory\n[low] centralization-risk at src/Arena.sol:166: Centralization Risk (27 places)\n[low] internal-function-used-once at src/OracleAttestation.sol:117: Internal Function Used Only Once\n[low] large-numeric-literal at src/FeeTreasury.sol:53: Large Numeric Literal (3 places)\n[low] local-variable-shadowing at src/Arena.sol:92: Local Variable Shadows State Variable\n[low] non-reentrant-not-first at src/FeeTreasury.sol:236: `nonReentrant` is Not the First Modifier\n[low] state-no-address-check at src/FeeTreasury.sol:168: Address State Variable Set Without Checks (3 places)\n[low] unchecked-return at src/TreasuryFeeHook.sol:214: Unchecked Return\n[low] unused-error at src/Arena.sol:136: Unused Error","passed":true},{"durationMs":3457,"exitCode":0,"name":"proof b4f3e5c7e4a5","output":"Compiling 109 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.68s\nCompiler run successful!\n\nRan 3 tests for test/imd-proof-af1b4985/Proof_b4f3e5c7e4a5.t.sol:HookPartialFillTest\n[PASS] test_buyExactInput_partialFill_feeExceedsHalfPercentOfLeg() (gas: 703631)\n[PASS] test_sellExactOutput_partialFill_feeExceedsHalfPercentOfLeg() (gas: 678834)\n[PASS] test_sellExactOutput_partialFill_sellerPaysEthNet() (gas: 699056)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 2.52ms (2.70ms CPU time)\n\nRan 1 test suite in 3.89ms (2.52ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)\n","passed":true},{"durationMs":1293,"exitCode":0,"name":"proof caf40ce6421f","output":"2026-10-09T09:44:34.936348Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-2PSnNa/repo/test/imd-proof-af1b4985/Proof_b4f3e5c7e4a5.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 679.03ms\nCompiler run successful!\n\nRan 1 test for test/imd-proof-0f084f7d/Proof_caf40ce6421f.t.sol:StakingStrandedTest\n[PASS] test_rewardsStreamedWithNoStakersAreLostForever() (gas: 415378)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 803.87µs (136.46µs CPU time)\n\nRan 1 test suite in 3.67ms (803.87µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1765,"exitCode":0,"name":"proof f3ad4c90393b","output":"2026-10-09T09:44:36.277383Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-2PSnNa/repo/test/imd-proof-0f084f7d/Proof_caf40ce6421f.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.13s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-7b7f34a7/Proof_f3ad4c90393b.t.sol:ProofEarlyResultTest\n[PASS] test_resultIsNotStoredWhileCommitsAreOpen() (gas: 760016)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.26ms (466.20µs CPU time)\n\nRan 1 test suite in 3.28ms (1.26ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":2684,"exitCode":0,"name":"proof 11fcc3caf4a7","output":"2026-10-09T09:44:38.108740Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-2PSnNa/repo/test/imd-proof-7b7f34a7/Proof_f3ad4c90393b.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.01s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-72cf80ff/Proof_11fcc3caf4a7.t.sol:ProofTreasuryStrandedTest\n[PASS] test_partialFillKeepsEveryWeiOnABudgetLine() (gas: 1780363)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.36ms (1.65ms CPU time)\n\nRan 1 test suite in 4.60ms (3.36ms 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":"c2a7eeee2e622e72c32d7271cb3e2e7cff4e3fbda3143e1ebe9229ca381b45a5","verifiedTreeHash":"4040d1af9e38c94b570b7b6aad1e79b37a43618c","verifierVersion":"0.1.0+c4d32abc"},{"checks":[{"durationMs":4188,"exitCode":0,"name":"build","output":"Compiling 118 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.05s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> script/Deploy.s.sol:63:5:\n   |\n63 |     function run() external {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/StakingVault.sol:66:30\n   │\n66 │     function setRewardFunder(address funder) external onlyOwner {\n   │                              ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/StakingVault.sol:81:16\n   │\n81 │         return block.timestamp < periodFinish ? block.timestamp : periodFinish;\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/OracleAdapter.sol:145:26\n    │\n145 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:189:15\n    │\n189 │         if (!(block.timestamp < commitDeadline && commitDeadline < revealDeadline && revealDeadline < resultDeadline)) {\n    │               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:254:13\n    │\n254 │         if (block.timestamp >= r.commitDeadline) revert CommitClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:268:13\n    │\n268 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:268:51\n    │\n268 │         if (block.timestamp < r.commitDeadline || block.timestamp >= r.revealDeadline) revert RevealWindowClosed();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:296:13\n    │\n296 │         if (block.timestamp < r.revealDeadline) revert RevealNotOver();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/Arena.sol:300:25\n    │\n300 │         uint8 winning = uint8(res.answer % r.choiceCount) + 1;\n    │                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/Arena.sol:321:13\n    │\n321 │         if (block.timestamp < uint256(r.resultDeadline) + CANCEL_GRACE) revert NotCancellable();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/FeeTreasury.sol:167:26\n    │\n167 │     function setExecutor(address to) external onlyOwner {\n    │                          ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:204:13\n    │\n204 │         if (block.timestamp >= windowStart + BUDGET_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:215:9\n    │\n215 │         emit Requested(roundId, intakeId, price);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:331:22\n    │\n331 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/OracleAdapter.sol:220:44\n    │\n220 │         if (pendingSince[intakeId] == 0 || block.timestamp < pendingSince[intakeId] + REQUEST_TIMEOUT) {\n    │                                            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/FeeTreasury.sol:236:83\n    │\n236 │     function withdrawOwner(address payable to, uint256 amount) external onlyOwner nonReentrant {\n    │                                                                                   ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:240:9\n    │\n240 │         emit OwnerWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/StakingVault.sol:144:13\n    │\n144 │         if (block.timestamp < periodFinish) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:249:9\n    │\n249 │         emit ReserveWithdrawn(to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/TreasuryFeeHook.sol:200:22\n    │\n200 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-eth]: uncapped ETH transfer can be reentered before `pendingEth` is updated\n    ╭▸ src/TreasuryFeeHook.sol:200:22\n    │\n200 │         (bool ok,) = treasury.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:206:9\n    │\n206 │         emit FeeDelivered(treasury, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/TreasuryFeeHook.sol:214:9\n    │\n214 │         poolManager.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-no-eth]: external call can be reentered before `spentInWindow` is updated\n    ╭▸ src/FeeTreasury.sol:305:31\n    │\n305 │         bytes memory result = poolManager.unlock(abi.encode(key, ethIn));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:269:9\n    │\n269 │         IRewardSink(stakingVault).notifyReward(toStaking);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `_status` is updated\n    ╭▸ src/FeeTreasury.sol:271:9\n    │\n271 │         IPrizeSink(arena).fundPrizes(toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:272:9\n    │\n272 │         emit PrioBought(spent, out, toStaking, toArena);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/OracleAdapter.sol:276:9\n    │\n276 │         emit ResultStored(roundId, answer, a.requestId, a.panelJobId);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:224:9\n    │\n224 │         emit ClaimsRedeemed(amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/FeeTreasury.sol:288:9\n    │\n288 │         emit ImdBought(spent, out);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/FeeTreasury.sol:300:13\n    │\n300 │         if (block.timestamp >= windowStart + SPEND_WINDOW) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:317:53\n    │\n317 │                 zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:321:24\n    │\n321 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:321:32\n    │\n321 │         uint256 owed = uint256(uint128(-delta.amount0()));\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:322:23\n    │\n322 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/FeeTreasury.sol:322:31\n    │\n322 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/FeeTreasury.sol:325:9\n    │\n325 │         poolManager.settle{value: owed}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/FeeTreasury.sol:331:22\n    │\n331 │         (bool ok,) = to.call{value: amount}(\"\");\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:267:30\n    │\n267 │             ? _buyExactInput(uint256(-params.amountSpecified), params.sqrtPriceLimitX96)\n    │                              ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:268:32\n    │\n268 │             : _sellExactOutput(uint256(params.amountSpecified), params.sqrtPriceLimitX96);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:289:13\n    │\n289 │         if (filled != leg) fee = feeOnLeg(filled);\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:292:62\n    │\n292 │         return toBeforeSwapDelta((filled + fee).toInt128(), -prioOut.toInt128());\n    │                                                              ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/TreasuryFeeHook.sol:420:9\n    │\n420 │         emit FeeCharged(poolId, zeroForOne, exactInput, leg, fee, asClaim);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:286:22\n    │\n286 │             filled = uint256(uint128(-d.amount0()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:286:30\n    │\n286 │             filled = uint256(uint128(-d.amount0()));\n    │                              ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:287:23\n    │\n287 │             prioOut = uint256(uint128(d.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:287:31\n    │\n287 │             prioOut = uint256(uint128(d.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:324:51\n    │\n324 │         return toBeforeSwapDelta(-pay.toInt128(), prioIn.toInt128());\n    │                                                   ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/TreasuryFeeHook.sol:312:13\n    │\n312 │         if (filled != leg) {\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:308:22\n    │\n308 │             filled = uint256(uint128(d.amount0()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:308:30\n    │\n308 │             filled = uint256(uint128(d.amount0()));\n    │                              ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:309:22\n    │\n309 │             prioIn = uint256(uint128(-d.amount1()));\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:309:30\n    │\n309 │             prioIn = uint256(uint128(-d.amount1()));\n    │                              ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:38\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:46\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                              ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:68\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:341:76\n    │\n341 │         uint256 leg = ethDelta < 0 ? uint256(uint128(-ethDelta)) : uint256(uint128(ethDelta));\n    │                                                                            ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:345:44\n    │\n345 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/TreasuryFeeHook.sol:345:51\n    │\n345 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                                   ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/Arena.t.sol:49:40\n    │\n 49 │         uint64 commitDeadline = uint64(block.timestamp + 1 hours);\n    │                                        ━━━━━━━━━━━━━━━\n    │\n    ⸬  lib/forge-std/src/StdCheats.sol:677:9\n    │\n677 │         vm.warp(vm.getBlockTimestamp() + time);\n    │         ────────────────────────────────────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":153,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/PrismRiotToken.t.sol:PrismRiotTokenTest\n[PASS] test_metadataAndSupply() (gas: 44137)\n[PASS] test_noMintOrOwnerFunctions() (gas: 51960)\n[PASS] test_transferMovesExactly() (gas: 78817)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 462.22µs (299.75µs CPU time)\n\nRan 11 tests for test/FeeTreasury.t.sol:FeeTreasuryTest\n[PASS] test_allocationSplitAndReserveCap() (gas: 562896)\n[PASS] test_buyPrioGuards() (gas: 994911)\n[PASS] test_buyPrioSplitsBetweenStakingAndArena() (gas: 1013065)\n[PASS] test_imdPurchaseWaitsForConfiguration_prioIndependent() (gas: 964127)\n[PASS] test_onlyHookCanFund_noOwnerAdvances() (gas: 43633)\n[PASS] test_ownerAndReserveWithdrawalsAreCapped() (gas: 692965)\n[PASS] test_partialFillCreditsUnspentEthBackToTheBudget() (gas: 1833934)\n[PASS] test_reserveStopsAtTarget() (gas: 878659)\n[PASS] test_rollingWindowAndPriceFloorBoundTheExecutor() (gas: 2247675)\n[PASS] test_setImdUnsetsThePoolKey() (gas: 751231)\n[PASS] test_sinksAreSetOnceAndNonZero() (gas: 165567)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 4.40ms (7.61ms CPU time)\n\nRan 13 tests for test/OracleAdapter.t.sol:OracleAdapterTest\n[PASS] test_budgetAndExecutorEnforced() (gas: 961255)\n[PASS] test_callbackRefusesWrongSenderAndUnknownId() (gas: 685444)\n[PASS] test_callbackSelectorIsCanonical() (gas: 529)\n[PASS] test_clearStaleAfterTimeout() (gas: 660165)\n[PASS] test_digestMatchesTheProtocol() (gas: 10836)\n[PASS] test_manualRelayStoresResultAndEvidence() (gas: 224019)\n[PASS] test_nothingIsStoredOrRequestedBeforeTheBoundary() (gas: 999103)\n[PASS] test_paidRequestDisabledUntilConfigured() (gas: 99249)\n[PASS] test_paidRequestThenCallbackUnder200kGas() (gas: 854626)\n[PASS] test_pinnedSignerOutlivesRotation() (gas: 420903)\n[PASS] test_relayRejectsReplayAndSecondResult() (gas: 391853)\n[PASS] test_relayRejectsWrongQuestionChainQuorumPanelSignerExpiryAndType() (gas: 604372)\n[PASS] test_withdrawTokenRefusesTheConfiguredAsset() (gas: 444917)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 4.40ms (6.57ms CPU time)\n\nRan 1 test for test/Deploy.t.sol:DeployTest\n[PASS] test_deployAllPlacesHookOnFlaggedAddress() (gas: 27273735)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.89ms (4.43ms CPU time)\n\nRan 3 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookClaimsTest\n[PASS] test_buyOnTokenOnlyPoolMintsClaimThenRedeems() (gas: 673609)\n[PASS] test_redeemRefusesMoreThanHeld() (gas: 75515)\n[PASS] test_secondBuyPaysDirectlyOnceManagerHoldsEth() (gas: 781711)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 8.42ms (1.32ms CPU time)\n\nRan 16 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookTest\n[PASS] testFuzz_feeChargedMatchesTreasuryIncome(uint96,bool,bool) (runs: 256, μ: 382391, ~: 384940)\n[PASS] testFuzz_quotesAreConsistentWeiLevel(uint128) (runs: 256, μ: 22607, ~: 22607)\n[PASS] test_buyExactInput_feeFromEthInput() (gas: 393752)\n[PASS] test_buyExactOutput_feeOnTopOfEthCharged() (gas: 376548)\n[PASS] test_callbacksRefuseNonManager() (gas: 123049)\n[PASS] test_constructorRefusesMismatchedAddress() (gas: 74525)\n[PASS] test_feeOnLegRoundsUpAtWeiLevel() (gas: 22980)\n[PASS] test_initializeOnFreshHookRefusesWrongPair() (gas: 344774)\n[PASS] test_initializeRefusesNonFactoryAndOtherPools() (gas: 48021)\n[PASS] test_oneWeiBuyIsRefusedNotSwallowed() (gas: 439366)\n[PASS] test_otherDirectionsLeaveTokenTransfersUntaxed() (gas: 74600)\n[PASS] test_permissionsAndAddressAgree() (gas: 31553)\n[PASS] test_receiveRefusesEveryoneButThePoolManager() (gas: 38301)\n[PASS] test_sellExactInput_feeOutOfEthOutput() (gas: 343970)\n[PASS] test_sellExactOutput_swapperGetsExactlyTheEthAsked() (gas: 369304)\n[PASS] test_treasuryBindingIsCorrectableUntilFirstDelivery_thenImmutable() (gas: 593116)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 25.30ms (30.35ms CPU time)\n\nRan 14 tests for test/Arena.t.sol:ArenaTest\n[PASS] test_bossChallengeNeedsThreshold() (gas: 1138514)\n[PASS] test_cancelAfter72hRefundsEverything() (gas: 1240175)\n[PASS] test_constantsPublished() (gas: 16224)\n[PASS] test_createRoundNeedsPinnedQuestionAtTheCommitBoundary() (gas: 361560)\n[PASS] test_entryRules() (gas: 777445)\n[PASS] test_noWinnerReturnsPrizeToPool() (gas: 959998)\n[PASS] test_oldRoundStaysClaimableAfterNewRounds() (gas: 1535638)\n[PASS] test_ownerCannotSwapOracleForAnOpenRound() (gas: 1856152)\n[PASS] test_resolvedRoundCannotBeCancelled_onlySettled() (gas: 1199743)\n[PASS] test_resultIssuedBeforeCommitBoundaryCannotSettle() (gas: 556639)\n[PASS] test_roundCreationRules() (gas: 132179)\n[PASS] test_signerRotationDoesNotReachPinnedRounds() (gas: 803200)\n[PASS] test_unresolvedRoundCancelsAndALateResultCannotSettleIt() (gas: 858249)\n[PASS] test_vaultRaidFullLifecycle() (gas: 1752356)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 44.09ms (8.92ms CPU time)\n\nRan 9 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] test_durationBounds() (gas: 67910)\n[PASS] test_leftoverRollsIntoNextStream() (gas: 432247)\n[PASS] test_noRewardsWithoutFunding() (gas: 204391)\n[PASS] test_notifyWhileIdleKeepsRemainderAndRestartsSchedule() (gas: 391149)\n[PASS] test_onlyFunderOrOwnerNotifies() (gas: 41455)\n[PASS] test_rewardsProRataByStakeAndTime_andStopAtPeriodEnd() (gas: 643629)\n[PASS] test_stakeWithdrawKeepsPrincipalExact() (gas: 249915)\n[PASS] test_streamPausesAgainWhenEveryoneLeaves() (gas: 675018)\n[PASS] test_streamPausesWhileNothingIsStaked_nothingStranded() (gas: 473558)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 44.06ms (2.16ms CPU time)\n\nRan 7 tests for test/TreasuryFeeHook.t.sol:TreasuryFeeHookPartialFillTest\n[PASS] testFuzz_partialFillFeeIsAlwaysOnTheFilledLeg(uint96,bool) (runs: 256, μ: 676770, ~: 745558)\n[PASS] test_buyExactInput_liquidityExhausted_feeIsHalfPercentOfFilledLeg() (gas: 766277)\n[PASS] test_buyExactInput_priceLimit_feeIsHalfPercentOfFilledLeg() (gas: 410658)\n[PASS] test_fullFillsStillCostExactlyWhatWasSpecified() (gas: 634061)\n[PASS] test_limitNextToSpotMovesNothingAndChargesNothing() (gas: 233336)\n[PASS] test_sellExactOutput_liquidityExhausted_sellerNeverPaysEth() (gas: 763001)\n[PASS] test_sellExactOutput_priceLimit_feeIsHalfPercentOfFilledLeg() (gas: 381614)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 46.89ms (49.18ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:InvariantsTest\n[PASS]\nInvariantsTest invariants:\n[PASS] invariant_arenaBalanceIsFullyAccounted\n[PASS] invariant_vaultCoversPrincipalAndOwedRewards\n InvariantsTest invariants (runs: 32, calls: 1024, reverts: 2)\n\n╭----------+----------+-------+---------+----------╮\n| Contract | Selector | Calls | Reverts | Discards |\n+==================================================+\n| Handler  | claim    | 299   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | enter    | 157   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | fund     | 145   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | stake    | 131   | 2       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | warp     | 146   | 0       | 0        |\n|----------+----------+-------+---------+----------|\n| Handler  | withdraw | 146   | 0       | 0        |\n╰----------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 57.80ms (56.40ms CPU time)\n\nRan 10 test suites in 58.94ms (240.70ms CPU time): 78 tests passed, 0 failed, 0 skipped (78 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Arena.acceptOwnership()\",\"Arena.cancel(uint256)\",\"Arena.claim(uint256)\",\"Arena.createRound(uint8,uint8,uint64,uint64,uint64,uint256,uint16,bytes32)\",\"Arena.enter(uint256,bytes32)\",\"Arena.fundPrizes(uint256)\",\"Arena.refund(uint256)\",\"Arena.reveal(uint256,uint8,bytes32)\",\"Arena.setOracle(address)\",\"Arena.settle(uint256)\",\"Arena.transferOwnership(address)\",\"FeeTreasury.acceptOwnership()\",\"FeeTreasury.allocate()\",\"FeeTreasury.bindHook(address)\",\"FeeTreasury.buyImd(uint256,uint256)\",\"FeeTreasury.buyPrio(uint256,uint256)\",\"FeeTreasury.receive()\",\"FeeTreasury.setExecutor(address)\",\"FeeTreasury.setImd(address)\",\"FeeTreasury.setImdPool(uint24,int24,address)\",\"FeeTreasury.setMaxSpendPerSwap(uint256)\",\"FeeTreasury.setPriceFloors(uint256,uint256)\",\"FeeTreasury.setPrio(address)\",\"FeeTreasury.setReserveTarget(uint256)\",\"FeeTreasury.setSinks(address,address,address)\",\"FeeTreasury.setSpendPerWindow(uint256)\",\"FeeTreasury.transferOwnership(address)\",\"FeeTreasury.unlockCallback(bytes)\",\"FeeTreasury.withdrawOwner(address,uint256)\",\"FeeTreasury.withdrawReserve(address,uint256)\",\"OracleAdapter.acceptOwnership()\",\"OracleAdapter.clearStale(bytes32)\",\"OracleAdapter.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.pinQuestion(uint256,bytes32,uint256,uint16,uint16,uint64,bytes)\",\"OracleAdapter.request(uint256)\",\"OracleAdapter.setAction(bytes32)\",\"OracleAdapter.setBudget(uint256)\",\"OracleAdapter.setCallbackConfigured(bool)\",\"OracleAdapter.setExecutor(address)\",\"OracleAdapter.setIntake(address)\",\"OracleAdapter.setPayment(address,uint256)\",\"OracleAdapter.setSigner(address)\",\"OracleAdapter.submitAttestation(uint256,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"OracleAdapter.transferOwnership(address)\",\"OracleAdapter.withdrawToken(address,address,uint256)\",\"PrismRiotToken.approve(address,uint256)\",\"PrismRiotToken.transfer(address,uint256)\",\"PrismRiotToken.transferFrom(address,address,uint256)\",\"StakingVault.acceptOwnership()\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.notifyReward(uint256)\",\"StakingVault.setRewardFunder(address)\",\"StakingVault.setRewardsDuration(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.transferOwnership(address)\",\"StakingVault.withdraw(uint256)\",\"TreasuryFeeHook.acceptOwnership()\",\"TreasuryFeeHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"TreasuryFeeHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"TreasuryFeeHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"TreasuryFeeHook.bindTreasury(address)\",\"TreasuryFeeHook.flush()\",\"TreasuryFeeHook.receive()\",\"TreasuryFeeHook.redeemClaims(uint256)\",\"TreasuryFeeHook.transferOwnership(address)\",\"TreasuryFeeHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":9,\"LICENSE\":9,\"README.md\":308,\"docs/OPERATOR.md\":55,\"docs/REVIEW.md\":79,\"docs/abi/Arena.json\":1055,\"docs/abi/FeeTreasury.json\":1224,\"docs/abi/OracleAdapter.json\":1450,\"docs/abi/PrismRiotToken.json\":328,\"docs/abi/StakingVault.json\":627,\"docs/abi/TreasuryFeeHook.json\":1515,\"foundry.toml\":27,\"launch.json\":32,\"operator/operator.example.json\":19,\"operator/operator.py\":294,\"operator/test_operator.py\":110,\"remappings.txt\":6,\"script/Deploy.s.sol\":66,\"src/Arena.sol\":359,\"src/FeeTreasury.sol\":334,\"src/OracleAdapter.sol\":298,\"src/OracleAttestation.sol\":125,\"src/PrismRiotToken.sol\":16,\"src/StakingVault.sol\":177,\"src/TreasuryFeeHook.sol\":422,\"src/TwoStepOwned.sol\":19,\"test/Arena.t.sol\":464,\"test/Deploy.t.sol\":23,\"test/FeeTreasury.t.sol\":235,\"test/Invariants.t.sol\":155,\"test/OracleAdapter.t.sol\":320,\"test/PrismRiotToken.t.sol\":37,\"test/StakingVault.t.sol\":156,\"test/TreasuryFeeHook.t.sol\":415,\"test/utils/Fixture.sol\":117,\"test/utils/MockIntake.sol\":36},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e1517165d047a4a5138ca1c29174f09722f1080df25e017b64b7fb0c157cf0fb","verifiedTreeHash":"205acaa8740d2ada4096b95ee9db5874a0cefec5","verifierVersion":"0.1.0+c4d32abc"}]}