// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.4; // The second reviewer's findings (GPT, 14 Sep 2026, pack at f577071), // reproduced against the real contracts in the EVM rather than the report's // Python models. Each test is named for the finding and asserts what the // CODE does; where that matches the report the finding holds, where it // does not the divergence is in the assertion. Every test here must pass - // a test that "fails" because the finding is real is written to PASS on the // bad behaviour, so the suite documents the state at review time. When a // finding is fixed, the corresponding test is inverted, not deleted. import "./TicketPolicy.t.sol"; import "./TicketHub.t.sol"; contract SecondReviewPolicyTest is TicketPolicyTest { // ---------------------------------------------------------------- F-01 // On-chain half: a one-base-unit stake is accepted, becomes eligible // after one settlement, and winnersFor(10, share) is 10 at every share. // (The off-chain half - the draw itself - is reproduced in // analysis/repro-f01-draw.mjs against the keeper's algorithm.) function test_F01_dustStakeIsEligible_andTenOfTenAreDrawn() public { address dust = makeAddr("dust"); vm.prank(genesis); token.transfer(dust, 1); // one base unit: 0.000000001 TICKET vm.startPrank(dust); token.approve(address(staking), 1); staking.stake(1); vm.stopPrank(); assertEq(staking.scoreOf(dust), 0, "no score before a settlement"); _setPoolPrice(1e18); vm.warp(DAY0 + 24 hours); _advance(0); policy.rebase(); // one settled night assertGt(staking.scoreOf(dust), 0, "one base unit staked for one epoch is eligible"); assertGt(staking.scoreOf(alice), 0); // Ten eligible wallets -> ten winners at every allowed share. for (uint256 s = 100; s <= 1000; s += 100) { assertEq(strikeVault.winnersFor(10, s), 10, "N == eligible when eligible <= MIN_WINNERS"); } // 2,010 eligible: at the 1% share, 21 winners; at 10%, 201. assertEq(strikeVault.winnersFor(2010, 100), 21); assertEq(strikeVault.winnersFor(2010, 1000), 201); } // ---------------------------------------------------------------- F-04 // TWAP 1.20 over the day, spot moved to 1.00 just before the window: // the green mint is 20% of the CURRENT pool balance and lands spot at // 1.00 / 1.20 = 0.8333. function test_F04_greenNightOnStaleTwapPushesSpotBelowTarget() public { // setUp leaves the clock at DAY0, primed. A day at 1.20 from here. _setPoolPrice(1.2e18); _advance(24 hours); // 24h of observations at 1.20, now at DAY0 + 24h assertApproxEqRel(policy.twap(), 1.2e18, 0.01e18, "twap sits at 1.20"); _setPoolPrice(1e18); // a seller brings spot to target at the last moment assertApproxEqRel(_spotPrice(), 1e18, 0.001e18); uint256 poolBefore = token.balanceOf(address(pair)); policy.rebase(); uint256 poolAfter = token.balanceOf(address(pair)); // 20% of the current balance was minted to the pool... assertApproxEqRel(poolAfter, (poolBefore * 120) / 100, 0.001e18, "pool grew by dev"); // ...and spot is now below target, on a green night. assertLt(_spotPrice(), 0.99e18, "spot below the dead band after a green settlement"); assertApproxEqRel(_spotPrice(), 0.8333e18, 0.01e18); } // ---------------------------------------------------------------- F-05 // A 90-day Policy (this fixture's) settles 91 times, not 90, when the // first rebase lands at the window start; 92 when it lands late in the // window and a later caller front-runs the boundary night. function _countSettlements(uint256 firstOffset) internal returns (uint256 n) { _setPoolPrice(1e18); _advance(24 hours); // DAY0 + 24h, window start uint256 first = DAY0 + 24 hours; vm.warp(first + firstOffset); // no extra checkpoint: _advance left one at `first` policy.rebase(); n = 1; for (uint256 d = 1; d < 200; d++) { if (policy.phase() == TicketPolicy.SeasonPhase.Intermission) break; // next night, at the window start uint256 t = first + d * 1 days; // Step to the window start EXACTLY, as a keeper aiming at the // window does; stepping past it hides the report's 44th night. while (block.timestamp + 30 minutes <= t) { vm.warp(block.timestamp + 30 minutes); policy.checkpoint(); } if (block.timestamp < t) { vm.warp(t); } // < 30 min since the last checkpoint: no new one, the TWAP still covers policy.rebase(); n++; } } function test_F05_ninetyDaySeasonSettles91Times() public { uint256 n = _countSettlements(0); assertEq(n, 91, "seasonLength/1day + 1 settlements"); } function test_F05_lateFirstCallSettles92Times() public { uint256 n = _countSettlements(29 minutes); assertEq(n, 92, "a late first call adds one more"); } // ---------------------------------------------------------------- F-07b // The pinned exit fee does NOT decay while queued: request at 5%, wait // out the queue and then some, withdraw during a pause -> still 5%. function test_F07_queuedFeeIsPinnedNotDecaying() public { _setPoolPrice(1e18); policy.rebase(); // DAY0 is primed and at the window start // alice staked in setUp: day-0 tenure, fee 5% assertEq(staking.unstakeFeeBpsOf(alice), 500, "5% on day 0"); vm.prank(alice); staking.requestUnstake(10_000 * ONE_TICKET); // Two more nights and 15 days of wall clock: the queue is released // and the wall-clock tenure would now read 0% for a NEW request. for (uint256 d = 1; d <= 2; d++) { uint256 t = DAY0 + d * 1 days; while (block.timestamp < t) { vm.warp(block.timestamp + 30 minutes); policy.checkpoint(); } policy.rebase(); } vm.warp(DAY0 + 15 days); policy.checkpoint(); assertEq(staking.feeTenureDays(alice), 15); uint256 before = token.balanceOf(alice); vm.prank(alice); uint256 got = staking.withdraw(); // 5% of 10,000 kept as fee: the pinned figure, not the decayed one. assertApproxEqAbs(got, 9_500 * ONE_TICKET, 2, "fee was pinned at 5% on request"); assertEq(token.balanceOf(alice) - before, got); } } contract SecondReviewHubTest is TicketHubTest { // ---------------------------------------------------------------- F-07a // The ledger once said x2.95 per settlement, which assumed the hostile // module ALSO contracts 5%. Contraction is optional. Mint-only reaches x3 per // window and x27 across three windows spent at 19:59:59 / 20:00:00 / // 40:00:00 - all inside 20h + 1s. function test_F07_mintOnlyBoundIs3xPerWindowAnd27xOverThree() public { uint256 S = token.totalSupply(); // Open a window at t0 with a zero-cost touch (zero mint still rolls it). uint256 t0 = block.timestamp; hub.mint(address(pair), 0); // Spend window 1 just before it closes: +2S -> 3S vm.warp(t0 + 20 hours - 1); hub.mint(address(pair), 2 * S); assertEq(token.totalSupply(), 3 * S, "x3 in one window, no contraction needed"); vm.expectRevert(bytes("Hub: window mint cap")); hub.mint(address(pair), 1); // Window 2 opens at t0+20h with base 3S: +6S -> 9S vm.warp(t0 + 20 hours); hub.mint(address(pair), 6 * S); assertEq(token.totalSupply(), 9 * S); // Window 3 opens at t0+40h with base 9S: +18S -> 27S vm.warp(t0 + 40 hours); hub.mint(address(pair), 18 * S); assertEq(token.totalSupply(), 27 * S, "x27, not x25.67"); } }