// 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"; import "./mocks/MockKraiken.sol"; import "./mocks/MockStake.sol"; import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.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); } } // ========================================================= // COVERAGE TESTS: initialize, getLiquidityParams, UUPS upgrade // ========================================================= /** * @title OptimizerV3ProxyTest * @notice Tests OptimizerV3 features that require proxy deployment: * initialize, getLiquidityParams, _authorizeUpgrade, onlyAdmin, _checkAdmin */ contract OptimizerV3ProxyTest is Test { MockKraiken mockKraiken; MockStake mockStake; OptimizerV3 proxyOptimizer; function setUp() public { mockKraiken = new MockKraiken(); mockStake = new MockStake(); OptimizerV3 impl = new OptimizerV3(); ERC1967Proxy proxy = new ERC1967Proxy( address(impl), abi.encodeWithSelector(OptimizerV3.initialize.selector, address(mockKraiken), address(mockStake)) ); proxyOptimizer = OptimizerV3(address(proxy)); } /** * @notice Verify initialize set up the proxy correctly (covers initialize body) */ function testInitialize() public view { // The proxy was initialized — calling getLiquidityParams should not revert // (it calls stake.getPercentageStaked() and stake.getAverageTaxRate()) (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = proxyOptimizer.getLiquidityParams(); assertEq(ci, 0, "capitalInefficiency always 0"); assertTrue(aw == 100 || aw == 20, "anchorWidth is either bear(100) or bull(20)"); // Bear or bull values are valid assertTrue( (as_ == 3e17 && dd == 3e17 && aw == 100) || (as_ == 1e18 && dd == 1e18 && aw == 20), "Params should be valid bear or bull set" ); } /** * @notice Bear market: staked <= 91% → AS=30%, AW=100, DD=0.3e18, CI=0 */ function testGetLiquidityParamsBear() public { mockStake.setPercentageStaked(50e16); // 50% staked → always bear mockStake.setAverageTaxRate(0); (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = proxyOptimizer.getLiquidityParams(); assertEq(capitalInefficiency, 0, "Bear: CI=0"); assertEq(anchorShare, 3e17, "Bear: AS=30%"); assertEq(anchorWidth, 100, "Bear: AW=100"); assertEq(discoveryDepth, 3e17, "Bear: DD=0.3e18"); } /** * @notice Bull market: staked > 91%, low tax → AS=100%, AW=20, DD=1e18, CI=0 */ function testGetLiquidityParamsBull() public { mockStake.setPercentageStaked(1e18); // 100% staked → always bull mockStake.setAverageTaxRate(0); (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = proxyOptimizer.getLiquidityParams(); assertEq(capitalInefficiency, 0, "Bull: CI=0"); assertEq(anchorShare, 1e18, "Bull: AS=100%"); assertEq(anchorWidth, 20, "Bull: AW=20"); assertEq(discoveryDepth, 1e18, "Bull: DD=1e18"); } /** * @notice Admin can upgrade to a new implementation (covers _authorizeUpgrade, onlyAdmin, _checkAdmin) */ function testV3UUPSUpgrade() public { OptimizerV3 impl2 = new OptimizerV3(); // This contract (address(this)) is the admin since it deployed the proxy proxyOptimizer.upgradeTo(address(impl2)); // Verify proxy still works after upgrade (uint256 ci,,,) = proxyOptimizer.getLiquidityParams(); assertEq(ci, 0, "CI always 0 after upgrade"); } /** * @notice Non-admin calling upgradeTo reverts with UnauthorizedAccount */ function testV3UnauthorizedUpgradeReverts() public { // Deploy impl2 BEFORE the prank so the prank applies only to upgradeTo OptimizerV3 impl2 = new OptimizerV3(); address nonAdmin = makeAddr("nonAdmin"); vm.expectRevert(abi.encodeWithSelector(OptimizerV3.UnauthorizedAccount.selector, nonAdmin)); vm.prank(nonAdmin); proxyOptimizer.upgradeTo(address(impl2)); } }