// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import {OptimizerV3} from "../src/OptimizerV3.sol"; import {OptimizerInput} from "../src/IOptimizer.sol"; import "forge-std/Test.sol"; /** * @title OptimizerV3Test * @notice Table-driven unit tests for OptimizerV3.calculateParams covering * bull/bear boundaries, edge inputs, and input validation guards. * * Bear output: CI=0, AS=0.3e18, AW=100, DD=0.3e18 * Bull output: CI=0, AS=1e18, AW=20, DD=1e18 * * Bull condition: stakedPct > 91 AND deltaS^3 * effIdx / 20 < 50 * where deltaS = 100 - stakedPct, effIdx = taxIdx (+ 1 if taxIdx >= 14, capped at 29) */ contract OptimizerV3Test is Test { OptimizerV3 v3; /// @dev Normalised tax-rate thresholds used by the transpiler to map /// continuous averageTaxRate → discrete effective index 0..29. /// These are the raw bracket values from the 30-bracket schedule. uint256[30] TAX_RATES = [ uint256(1), 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700 ]; uint256 constant MAX_TAX = 9700; // Expected bear/bull outputs uint256 constant BEAR_CI = 0; uint256 constant BEAR_AS = 3e17; uint24 constant BEAR_AW = 100; uint256 constant BEAR_DD = 3e17; uint256 constant BULL_CI = 0; uint256 constant BULL_AS = 1e18; uint24 constant BULL_AW = 20; uint256 constant BULL_DD = 1e18; function setUp() public { v3 = new OptimizerV3(); } // ---- Helpers ---- function _norm(uint256 taxIdx) internal view returns (uint256) { return TAX_RATES[taxIdx] * 1e18 / MAX_TAX; } function _pct(uint256 pct) internal pure returns (uint256) { return pct * 1e18 / 100; } function _inputs(uint256 percentageStaked, uint256 averageTaxRate) internal pure returns (OptimizerInput[8] memory inp) { inp[0] = OptimizerInput({mantissa: int256(percentageStaked), shift: 0}); inp[1] = OptimizerInput({mantissa: int256(averageTaxRate), shift: 0}); } function _assertBear(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure { assertEq(ci, BEAR_CI, "bear: ci"); assertEq(as_, BEAR_AS, "bear: anchorShare"); assertEq(aw, BEAR_AW, "bear: anchorWidth"); assertEq(dd, BEAR_DD, "bear: discoveryDepth"); } function _assertBull(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure { assertEq(ci, BULL_CI, "bull: ci"); assertEq(as_, BULL_AS, "bull: anchorShare"); assertEq(aw, BULL_AW, "bull: anchorWidth"); assertEq(dd, BULL_DD, "bull: discoveryDepth"); } // ---- Shift guard ---- function testNonZeroShiftReverts() public { for (uint256 k = 0; k < 8; k++) { OptimizerInput[8] memory inp; inp[k] = OptimizerInput({mantissa: 0, shift: 1}); vm.expectRevert("shift not yet supported"); v3.calculateParams(inp); } } // ---- Negative mantissa guard ---- function testNegativeMantissaSlot0Reverts() public { OptimizerInput[8] memory inp; inp[0] = OptimizerInput({mantissa: -1, shift: 0}); vm.expectRevert("negative mantissa"); v3.calculateParams(inp); } function testNegativeMantissaSlot1Reverts() public { OptimizerInput[8] memory inp; inp[0] = OptimizerInput({mantissa: int256(95e16), shift: 0}); inp[1] = OptimizerInput({mantissa: -1, shift: 0}); vm.expectRevert("negative mantissa"); v3.calculateParams(inp); } function testNegativeMantissaHighSlotReverts() public { OptimizerInput[8] memory inp; inp[5] = OptimizerInput({mantissa: -1, shift: 0}); vm.expectRevert("negative mantissa"); v3.calculateParams(inp); } // ---- Mantissa overflow guard ---- function testMantissaOverflowReverts() public { OptimizerInput[8] memory inp; inp[0] = OptimizerInput({mantissa: int256(1e18 + 1), shift: 0}); vm.expectRevert("mantissa overflow"); v3.calculateParams(inp); } // ---- Bear: staking at or below 91% boundary ---- function testAlwaysBearAt0Percent() public view { for (uint256 t = 0; t < 30; t++) { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(0, _norm(t))); _assertBear(ci, as_, aw, dd); } } function testAlwaysBearAt50Percent() public view { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(50), 0)); _assertBear(ci, as_, aw, dd); } function testAlwaysBearAt91Percent() public view { for (uint256 t = 0; t < 30; t++) { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(91), _norm(t))); _assertBear(ci, as_, aw, dd); } } function testBearAt1Percent() public view { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(1), 0)); _assertBear(ci, as_, aw, dd); } // ---- Bull: boundary at 92% staking ---- function testBull92PercentTaxIdx0() public view { // deltaS=8, effIdx=0 → penalty=0 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(92), _norm(0))); _assertBull(ci, as_, aw, dd); } function testBull92PercentTaxIdx1() public view { // deltaS=8, effIdx=1 → penalty=512*1/20=25 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(92), _norm(1))); _assertBull(ci, as_, aw, dd); } function testBear92PercentTaxIdx2() public view { // deltaS=8, effIdx=2 → penalty=512*2/20=51 >= 50 → BEAR (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(92), _norm(2))); _assertBear(ci, as_, aw, dd); } // ---- Bull/Bear at 95% staking ---- function testBull95PercentTaxIdx7() public view { // deltaS=5, effIdx=7 → penalty=125*7/20=43 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(95), _norm(7))); _assertBull(ci, as_, aw, dd); } function testBear95PercentTaxIdx8() public view { // deltaS=5, effIdx=8 → penalty=125*8/20=50 NOT < 50 → BEAR (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(95), _norm(8))); _assertBear(ci, as_, aw, dd); } // ---- Bull/Bear at 96% with effIdx shift ---- function testEffIdxShiftAt96Percent() public view { // taxIdx=13: effIdx=13, penalty=64*13/20=41 < 50 → BULL { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(96), _norm(13))); _assertBull(ci, as_, aw, dd); } // taxIdx=14: effIdx=15 (shift! +1 for idx>=14), penalty=64*15/20=48 < 50 → BULL { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(96), _norm(14))); _assertBull(ci, as_, aw, dd); } // taxIdx=15: effIdx=16, penalty=64*16/20=51 >= 50 → BEAR { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(96), _norm(15))); _assertBear(ci, as_, aw, dd); } } // ---- Bull at 97% with max tax ---- function testBull97PercentMaxTax() public view { // deltaS=3, effIdx=29 → penalty=27*29/20=39 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(97), _norm(29))); _assertBull(ci, as_, aw, dd); } // ---- 100% staking always bull ---- function testAlwaysBullAt100Percent() public view { for (uint256 t = 0; t < 30; t++) { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(1e18, _norm(t))); _assertBull(ci, as_, aw, dd); } } // ---- Edge: zero inputs ---- function testAllZeroInputs() public view { OptimizerInput[8] memory inp; (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(inp); _assertBear(ci, as_, aw, dd); } // ---- Edge: zero tax at high staking ---- function testZeroTaxAt99Percent() public view { // deltaS=1, effIdx=0 → penalty=0 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(99), 0)); _assertBull(ci, as_, aw, dd); } // ---- Unused slots are ignored ---- function testUnusedSlotsIgnored() public view { OptimizerInput[8] memory inp; inp[0] = OptimizerInput({mantissa: int256(_pct(92)), shift: 0}); inp[1] = OptimizerInput({mantissa: int256(_norm(0)), shift: 0}); inp[2] = OptimizerInput({mantissa: 12345678, shift: 0}); inp[3] = OptimizerInput({mantissa: 9876, shift: 0}); inp[4] = OptimizerInput({mantissa: 1e17, shift: 0}); inp[5] = OptimizerInput({mantissa: 3600, shift: 0}); inp[6] = OptimizerInput({mantissa: 5e17, shift: 0}); inp[7] = OptimizerInput({mantissa: 42, shift: 0}); (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(inp); _assertBull(ci, as_, aw, dd); } // ---- Fuzz: no unexpected reverts ---- function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view { percentageStaked = bound(percentageStaked, 0, 1e18); averageTaxRate = bound(averageTaxRate, 0, 1e18); v3.calculateParams(_inputs(percentageStaked, averageTaxRate)); } // ---- Fuzz: output always bear or bull ---- function testFuzzOutputsAreAlwaysBearOrBull(uint256 percentageStaked, uint256 averageTaxRate) public view { percentageStaked = bound(percentageStaked, 0, 1e18); averageTaxRate = bound(averageTaxRate, 0, 1e18); (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(percentageStaked, averageTaxRate)); assertEq(ci, 0, "ci always 0"); bool isBear = (as_ == BEAR_AS && aw == BEAR_AW && dd == BEAR_DD); bool isBull = (as_ == BULL_AS && aw == BULL_AW && dd == BULL_DD); assertTrue(isBear || isBull, "output must be bear or bull"); } }