// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import { OptimizerV3 } from "../src/OptimizerV3.sol"; import { Stake } from "../src/Stake.sol"; import "forge-std/Test.sol"; contract OptimizerV3Test is Test { OptimizerV3 optimizer; // TAX_RATES from Stake.sol 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; function setUp() public { // Deploy without initialization (we only test pure functions) optimizer = new OptimizerV3(); } function _normalizedTaxRate(uint256 taxRateIndex) internal view returns (uint256) { return TAX_RATES[taxRateIndex] * 1e18 / MAX_TAX; } function _percentageStaked(uint256 pct) internal pure returns (uint256) { return pct * 1e18 / 100; } // ==================== Always Bear (staked <= 91%) ==================== function testAlwaysBearAt0Percent() public view { for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) { assertFalse(optimizer.isBullMarket(0, _normalizedTaxRate(taxIdx)), "0% staked should always be bear"); } } function testAlwaysBearAt50Percent() public view { for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) { assertFalse(optimizer.isBullMarket(_percentageStaked(50), _normalizedTaxRate(taxIdx)), "50% staked should always be bear"); } } function testAlwaysBearAt91Percent() public view { for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) { assertFalse(optimizer.isBullMarket(_percentageStaked(91), _normalizedTaxRate(taxIdx)), "91% staked should always be bear"); } } function testAlwaysBearAt39Percent() public view { assertFalse(optimizer.isBullMarket(_percentageStaked(39), _normalizedTaxRate(0)), "39% staked should be bear"); } function testAlwaysBearAt80Percent() public view { assertFalse(optimizer.isBullMarket(_percentageStaked(80), _normalizedTaxRate(0)), "80% staked should be bear even with lowest tax"); } // ==================== 92% Boundary ==================== function testBoundary92PercentLowestTax() public view { // deltaS=8, effIdx=0 → penalty = 512*0/20 = 0 < 50 → BULL assertTrue(optimizer.isBullMarket(_percentageStaked(92), _normalizedTaxRate(0)), "92% staked, lowest tax should be bull"); } function testBoundary92PercentTaxIdx1() public view { // deltaS=8, effIdx=1 → penalty = 512*1/20 = 25 < 50 → BULL assertTrue(optimizer.isBullMarket(_percentageStaked(92), _normalizedTaxRate(1)), "92% staked, taxIdx=1 should be bull"); } function testBoundary92PercentTaxIdx2() public view { // deltaS=8, effIdx=2 → penalty = 512*2/20 = 51 >= 50 → BEAR assertFalse(optimizer.isBullMarket(_percentageStaked(92), _normalizedTaxRate(2)), "92% staked, taxIdx=2 should be bear"); } function testBoundary92PercentHighTax() public view { // deltaS=8, effIdx=29 → penalty = 512*29/20 = 742 → BEAR assertFalse(optimizer.isBullMarket(_percentageStaked(92), _normalizedTaxRate(29)), "92% staked, max tax should be bear"); } // ==================== 95% Staked ==================== function testAt95PercentLowTax() public view { // deltaS=5, effIdx=0 → penalty = 125*0/20 = 0 < 50 → BULL assertTrue(optimizer.isBullMarket(_percentageStaked(95), _normalizedTaxRate(0)), "95% staked, lowest tax should be bull"); } function testAt95PercentTaxIdx7() public view { // deltaS=5, effIdx=7 → penalty = 125*7/20 = 43 < 50 → BULL assertTrue(optimizer.isBullMarket(_percentageStaked(95), _normalizedTaxRate(7)), "95% staked, taxIdx=7 should be bull"); } function testAt95PercentTaxIdx8() public view { // deltaS=5, effIdx=8 → penalty = 125*8/20 = 50, NOT < 50 → BEAR assertFalse(optimizer.isBullMarket(_percentageStaked(95), _normalizedTaxRate(8)), "95% staked, taxIdx=8 should be bear"); } function testAt95PercentTaxIdx9() public view { // deltaS=5, effIdx=9 → penalty = 125*9/20 = 56 → BEAR assertFalse(optimizer.isBullMarket(_percentageStaked(95), _normalizedTaxRate(9)), "95% staked, taxIdx=9 should be bear"); } // ==================== 97% Staked ==================== function testAt97PercentLowTax() public view { // deltaS=3, effIdx=0 → penalty = 27*0/20 = 0 < 50 → BULL assertTrue(optimizer.isBullMarket(_percentageStaked(97), _normalizedTaxRate(0)), "97% staked, lowest tax should be bull"); } function testAt97PercentHighTax() public view { // deltaS=3, effIdx=29 → penalty = 27*29/20 = 39 < 50 → BULL assertTrue(optimizer.isBullMarket(_percentageStaked(97), _normalizedTaxRate(29)), "97% staked, max tax should still be bull"); } // ==================== 100% Staked ==================== function testAt100PercentAlwaysBull() public view { // deltaS=0 → penalty = 0 → always BULL for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) { assertTrue(optimizer.isBullMarket(1e18, _normalizedTaxRate(taxIdx)), "100% staked should always be bull"); } } // ==================== 96% Sweep ==================== function testAt96PercentSweep() public view { // deltaS=4, cubic=64 // penalty = 64 * effIdx / 20 // Bull when penalty < 50, i.e., 64 * effIdx / 20 < 50 → effIdx < 15.625 // effIdx 0-15: bull (penalty 0..48). effIdx 16+: bear (penalty 51+) for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) { bool result = optimizer.isBullMarket(_percentageStaked(96), _normalizedTaxRate(taxIdx)); // Compute expected: effIdx from the tax rate uint256 effIdx = taxIdx; if (taxIdx >= 14) { effIdx = taxIdx + 1; if (effIdx > 29) effIdx = 29; } uint256 penalty = 64 * effIdx / 20; bool expectedBull = penalty < 50; assertEq(result, expectedBull, string.concat("96% sweep mismatch at taxIdx=", vm.toString(taxIdx))); } } // ==================== 94% Sweep ==================== function testAt94PercentSweep() public view { // deltaS=6, cubic=216 // penalty = 216 * effIdx / 20 // Bull when penalty < 50, i.e., 216 * effIdx / 20 < 50 → effIdx < 4.629 // effIdx 0-4: bull. effIdx 5+: bear. for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) { bool result = optimizer.isBullMarket(_percentageStaked(94), _normalizedTaxRate(taxIdx)); uint256 effIdx = taxIdx; if (taxIdx >= 14) { effIdx = taxIdx + 1; if (effIdx > 29) effIdx = 29; } uint256 penalty = 216 * effIdx / 20; bool expectedBull = penalty < 50; assertEq(result, expectedBull, string.concat("94% sweep mismatch at taxIdx=", vm.toString(taxIdx))); } } // ==================== Revert on Invalid Input ==================== function testRevertsAbove100Percent() public { vm.expectRevert("Invalid percentage staked"); optimizer.isBullMarket(1e18 + 1, 0); } // ==================== 93% Staked ==================== function testAt93PercentSweep() public view { // deltaS=7, cubic=343 // penalty = 343 * effIdx / 20 // Bull when penalty < 50, i.e., 343 * effIdx / 20 < 50 → effIdx < 2.915 // effIdx 0-2: bull. effIdx 3+: bear. for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) { bool result = optimizer.isBullMarket(_percentageStaked(93), _normalizedTaxRate(taxIdx)); uint256 effIdx = taxIdx; if (taxIdx >= 14) { effIdx = taxIdx + 1; if (effIdx > 29) effIdx = 29; } uint256 penalty = 343 * effIdx / 20; bool expectedBull = penalty < 50; assertEq(result, expectedBull, string.concat("93% sweep mismatch at taxIdx=", vm.toString(taxIdx))); } } // ==================== 99% Staked ==================== function testAt99PercentAlwaysBull() public view { // deltaS=1, cubic=1 → penalty = effIdx/20, always < 50 for effIdx <= 29 for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) { assertTrue(optimizer.isBullMarket(_percentageStaked(99), _normalizedTaxRate(taxIdx)), "99% staked should always be bull"); } } // ==================== EffIdx Shift at Boundary (taxIdx 13 vs 14) ==================== function testEffIdxShiftAtBoundary() public view { // At 96% staked, deltaS=4, cubic=64 // taxIdx=13: effIdx=13, penalty = 64*13/20 = 41 < 50 → BULL assertTrue(optimizer.isBullMarket(_percentageStaked(96), _normalizedTaxRate(13)), "taxIdx=13 should be bull at 96%"); // taxIdx=14: effIdx=15 (shift!), penalty = 64*15/20 = 48 < 50 → BULL assertTrue(optimizer.isBullMarket(_percentageStaked(96), _normalizedTaxRate(14)), "taxIdx=14 should be bull at 96% (effIdx shift)"); // taxIdx=15: effIdx=16, penalty = 64*16/20 = 51 >= 50 → BEAR assertFalse(optimizer.isBullMarket(_percentageStaked(96), _normalizedTaxRate(15)), "taxIdx=15 should be bear at 96%"); } // ==================== Fuzz Tests ==================== function testFuzzBearBelow92(uint256 percentageStaked, uint256 taxIdx) public view { percentageStaked = bound(percentageStaked, 0, 91e18 / 100); taxIdx = bound(taxIdx, 0, 29); assertFalse(optimizer.isBullMarket(percentageStaked, _normalizedTaxRate(taxIdx)), "Should always be bear below 92%"); } function testFuzz100PercentAlwaysBull(uint256 taxIdx) public view { taxIdx = bound(taxIdx, 0, 29); assertTrue(optimizer.isBullMarket(1e18, _normalizedTaxRate(taxIdx)), "100% staked should always be bull"); } function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view { percentageStaked = bound(percentageStaked, 0, 1e18); averageTaxRate = bound(averageTaxRate, 0, 1e18); // Should not revert optimizer.isBullMarket(percentageStaked, averageTaxRate); } }