// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import {OptimizerV3Push3} from "../src/OptimizerV3Push3.sol"; import {OptimizerInput} from "../src/IOptimizer.sol"; import "forge-std/Test.sol"; /** * @title OptimizerV3Push3Test * @notice Verifies that the transpiled Push3 optimizer produces correct * bear/bull parameters via the 8-slot dyadic rational interface. * * 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 penalty < 50 * where penalty = deltaS^3 * effIdx / 20 */ contract OptimizerV3Push3Test is Test { OptimizerV3Push3 push3; 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_ANCHOR_SHARE = 3e17; // 0.3e18 uint24 constant BEAR_ANCHOR_WIDTH = 100; uint256 constant BEAR_DISCOVERY = 3e17; // 0.3e18 uint256 constant BULL_CI = 0; uint256 constant BULL_ANCHOR_SHARE = 1e18; uint24 constant BULL_ANCHOR_WIDTH = 20; uint256 constant BULL_DISCOVERY = 1e18; function setUp() public { push3 = new OptimizerV3Push3(); } // ---- 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; } /// @dev Build an 8-slot input array with only slots 0 and 1 populated. 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}); // slots 2-7 default to (0, 0) } function _assertBear(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure { assertEq(ci, BEAR_CI, "bear: ci"); assertEq(as_, BEAR_ANCHOR_SHARE, "bear: anchorShare"); assertEq(aw, BEAR_ANCHOR_WIDTH, "bear: anchorWidth"); assertEq(dd, BEAR_DISCOVERY, "bear: discoveryDepth"); } function _assertBull(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure { assertEq(ci, BULL_CI, "bull: ci"); assertEq(as_, BULL_ANCHOR_SHARE, "bull: anchorShare"); assertEq(aw, BULL_ANCHOR_WIDTH, "bull: anchorWidth"); assertEq(dd, BULL_DISCOVERY, "bull: discoveryDepth"); } // ---- Bear cases ---- function testAlwaysBearAt0Percent() public view { for (uint256 t = 0; t < 30; t++) { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(0, _norm(t))); _assertBear(ci, as_, aw, dd); } } function testAlwaysBearAt91Percent() public view { for (uint256 t = 0; t < 30; t++) { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(91), _norm(t))); _assertBear(ci, as_, aw, dd); } } // ---- Bull cases ---- function testBoundary92PercentLowestTax() public view { // deltaS=8, effIdx=0 → penalty=0 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(0))); _assertBull(ci, as_, aw, dd); } function testBoundary92PercentTaxIdx1() public view { // deltaS=8, effIdx=1 → penalty=512*1/20=25 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(1))); _assertBull(ci, as_, aw, dd); } function testBoundary92PercentTaxIdx2Bear() public view { // deltaS=8, effIdx=2 → penalty=512*2/20=51 >= 50 → BEAR (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(2))); _assertBear(ci, as_, aw, dd); } function testAt95PercentTaxIdx7() public view { // deltaS=5, effIdx=7 → penalty=125*7/20=43 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(95), _norm(7))); _assertBull(ci, as_, aw, dd); } function testAt95PercentTaxIdx8Bear() public view { // deltaS=5, effIdx=8 → penalty=125*8/20=50 NOT < 50 → BEAR (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(95), _norm(8))); _assertBear(ci, as_, aw, dd); } function testAt97PercentHighTax() public view { // deltaS=3, effIdx=29 → penalty=27*29/20=39 < 50 → BULL (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(97), _norm(29))); _assertBull(ci, as_, aw, dd); } function testAt100PercentAlwaysBull() public view { for (uint256 t = 0; t < 30; t++) { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(1e18, _norm(t))); _assertBull(ci, as_, aw, dd); } } function testEffIdxShiftAtBoundary() public view { // taxIdx=13: effIdx=13, penalty=64*13/20=41 < 50 → BULL { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(13))); _assertBull(ci, as_, aw, dd); } // taxIdx=14: effIdx=15 (shift!), penalty=64*15/20=48 < 50 → BULL { (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.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) = push3.calculateParams(_inputs(_pct(96), _norm(15))); _assertBear(ci, as_, aw, dd); } } // ---- Unused slots are ignored ---- function testUnusedSlotsIgnored() public view { // Populate slots 2-7 with arbitrary values; output should be unchanged. 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) = push3.calculateParams(inp); _assertBull(ci, as_, aw, dd); } // ---- Negative mantissa guards ---- function testNegativeMantissaSlot0Reverts() public { OptimizerInput[8] memory inp; inp[0] = OptimizerInput({mantissa: -1, shift: 0}); vm.expectRevert("negative mantissa"); push3.calculateParams(inp); } function testNegativeMantissaSlot1Reverts() public { OptimizerInput[8] memory inp; inp[0] = OptimizerInput({mantissa: int256(_pct(95)), shift: 0}); inp[1] = OptimizerInput({mantissa: -1, shift: 0}); vm.expectRevert("negative mantissa"); push3.calculateParams(inp); } function testNegativeMantissaSlot5Reverts() public { OptimizerInput[8] memory inp; inp[5] = OptimizerInput({mantissa: -1, shift: 0}); vm.expectRevert("negative mantissa"); push3.calculateParams(inp); } // ---- Fuzz ---- function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view { percentageStaked = bound(percentageStaked, 0, 1e18); averageTaxRate = bound(averageTaxRate, 0, 1e18); push3.calculateParams(_inputs(percentageStaked, averageTaxRate)); } 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) = push3.calculateParams(_inputs(percentageStaked, averageTaxRate)); // CI is always 0 in the binary bear/bull model assertEq(ci, 0, "ci always 0"); // Output is exactly BEAR or BULL bool isBearOutput = (as_ == BEAR_ANCHOR_SHARE && aw == BEAR_ANCHOR_WIDTH && dd == BEAR_DISCOVERY); bool isBullOutput = (as_ == BULL_ANCHOR_SHARE && aw == BULL_ANCHOR_WIDTH && dd == BULL_DISCOVERY); assertTrue(isBearOutput || isBullOutput, "output must be bear or bull"); } }