From 87912b06da5aeaf6b109106fbb327353915bbf8e Mon Sep 17 00:00:00 2001 From: johba Date: Sun, 22 Mar 2026 06:31:06 +0000 Subject: [PATCH] fix: No Foundry test for OptimizerV3 calculateParams correctness (#607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add table-driven Foundry tests for OptimizerV3.calculateParams covering: - Bear regime at 0%, 1%, 50%, 91% staking (all tax rates) - Bull/bear boundary at 92% with tax index transitions - Bull/bear at 95% with penalty=50 exact boundary - EffIdx shift behavior at 96% (taxIdx 13→14 discontinuity) - Bull at 97% with max tax, 100% always bull - Edge cases: all-zero inputs, zero tax at high staking - Mantissa overflow guard - Unused slots ignored - Fuzz: no reverts, output always exactly bear or bull Co-Authored-By: Claude Opus 4.6 (1M context) --- onchain/test/OptimizerV3.t.sol | 250 ++++++++++++++++++++++++++++++--- 1 file changed, 234 insertions(+), 16 deletions(-) diff --git a/onchain/test/OptimizerV3.t.sol b/onchain/test/OptimizerV3.t.sol index fc3b662..62e0a7a 100644 --- a/onchain/test/OptimizerV3.t.sol +++ b/onchain/test/OptimizerV3.t.sol @@ -7,18 +7,80 @@ import "forge-std/Test.sol"; /** * @title OptimizerV3Test - * @notice Unit tests for OptimizerV3.calculateParams input validation guards - * (shift and negative-mantissa) and basic bear/bull output correctness. + * @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 @@ -28,6 +90,20 @@ contract OptimizerV3Test is Test { 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 { @@ -63,23 +139,165 @@ contract OptimizerV3Test is Test { v3.calculateParams(inp); } - // ---- Basic output correctness ---- + // ---- Mantissa overflow guard ---- - function testBearAtLowStaking() public view { - (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = - v3.calculateParams(_inputs(50e16, 0)); - assertEq(ci, 0); - assertEq(as_, 3e17); - assertEq(aw, 100); - assertEq(dd, 3e17); + function testMantissaOverflowReverts() public { + OptimizerInput[8] memory inp; + inp[0] = OptimizerInput({mantissa: int256(1e18 + 1), shift: 0}); + vm.expectRevert("mantissa overflow"); + v3.calculateParams(inp); } - function testBullAtHighStaking() public view { + // ---- 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(96e16, 0)); - assertEq(ci, 0); - assertEq(as_, 1e18); - assertEq(aw, 20); - assertEq(dd, 1e18); + 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"); } }