diff --git a/onchain/analysis/StreamlinedFuzzing.s.sol b/onchain/analysis/StreamlinedFuzzing.s.sol index 5806e79..d4e3ae2 100644 --- a/onchain/analysis/StreamlinedFuzzing.s.sol +++ b/onchain/analysis/StreamlinedFuzzing.s.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.19; import { Optimizer } from "../src/Optimizer.sol"; -import { OptimizerV2 } from "../src/OptimizerV2.sol"; import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol"; import { BearMarketOptimizer } from "../test/mocks/BearMarketOptimizer.sol"; @@ -78,11 +77,6 @@ contract StreamlinedFuzzing is FuzzingBase { address optimizer = _deployOptimizer(optimizerClass); _setupEnvironment(optimizer, runIndex % 2 == 0, uncapped); - // Late-initialize OptimizerV2 (needs stake address from setup) - if (keccak256(bytes(optimizerClass)) == keccak256(bytes("OptimizerV2"))) { - OptimizerV2(optimizer).initialize(address(kraiken), address(stake)); - } - // Deploy background LP if configured if (bgLpEthPerLayer > 0) { _deployBackgroundLP(bgLpEthPerLayer); @@ -292,10 +286,6 @@ contract StreamlinedFuzzing is FuzzingBase { return address(new ExtremeOptimizer()); } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("MaliciousOptimizer"))) { return address(new MaliciousOptimizer()); - } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("OptimizerV2"))) { - // Deploy uninitialized — will be initialized after _setupEnvironment - // when stake address is available - return address(new OptimizerV2()); } else { return address(new BullMarketOptimizer()); } diff --git a/onchain/script/DeployBase.sol b/onchain/script/DeployBase.sol index 07bfdc9..5ccff59 100644 --- a/onchain/script/DeployBase.sol +++ b/onchain/script/DeployBase.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import "../src/Kraiken.sol"; import { LiquidityManager } from "../src/LiquidityManager.sol"; -import "../src/OptimizerV3.sol"; +import "../src/OptimizerV3Push3.sol"; import "../src/Stake.sol"; import "../src/helpers/UniswapHelpers.sol"; import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; @@ -71,14 +71,14 @@ contract DeployBase is Script { console.log("Pool initialized"); } - // Deploy OptimizerV3 (if not already deployed) + // Deploy OptimizerV3Push3 (if not already deployed) address optimizerAddress; if (optimizer == address(0)) { - OptimizerV3 optimizerImpl = new OptimizerV3(); + OptimizerV3Push3 optimizerImpl = new OptimizerV3Push3(); bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake)); ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params); optimizerAddress = address(proxy); - console.log("OptimizerV3 deployed at:", optimizerAddress); + console.log("OptimizerV3Push3 deployed at:", optimizerAddress); } else { optimizerAddress = optimizer; console.log("Using existing optimizer at:", optimizerAddress); @@ -99,7 +99,7 @@ contract DeployBase is Script { console.log("Stake:", address(stake)); console.log("Pool:", address(pool)); console.log("LiquidityManager:", address(liquidityManager)); - console.log("OptimizerV3:", optimizerAddress); + console.log("OptimizerV3Push3:", optimizerAddress); console.log("\nPost-deploy steps:"); console.log(" 1. Fund LiquidityManager with ETH"); console.log(" 2. Set recenterAccess to txnBot: lm.setRecenterAccess(txnBot) from feeDestination"); diff --git a/onchain/script/DeployBaseMainnet.sol b/onchain/script/DeployBaseMainnet.sol index 8f1bab8..1a05682 100644 --- a/onchain/script/DeployBaseMainnet.sol +++ b/onchain/script/DeployBaseMainnet.sol @@ -17,7 +17,7 @@ contract DeployBaseMainnet is DeployBase { weth = 0x4200000000000000000000000000000000000006; // WETH on Base v3Factory = 0x33128a8fC17869897dcE68Ed026d694621f6FDfD; // Uniswap V3 Factory on Base - // Deploy fresh OptimizerV3 (UUPS proxy) + // Deploy fresh OptimizerV3Push3 (UUPS proxy) optimizer = address(0); } } diff --git a/onchain/script/DeployLocal.sol b/onchain/script/DeployLocal.sol index c061975..f40baf1 100644 --- a/onchain/script/DeployLocal.sol +++ b/onchain/script/DeployLocal.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import "../src/Kraiken.sol"; import { LiquidityManager } from "../src/LiquidityManager.sol"; -import "../src/OptimizerV3.sol"; +import "../src/OptimizerV3Push3.sol"; import "../src/Stake.sol"; import "../src/helpers/UniswapHelpers.sol"; import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; @@ -86,12 +86,12 @@ contract DeployLocal is Script { console.log(" Pool initialized at 1 cent price"); } - // Deploy OptimizerV3 - OptimizerV3 optimizerImpl = new OptimizerV3(); + // Deploy OptimizerV3Push3 + OptimizerV3Push3 optimizerImpl = new OptimizerV3Push3(); bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake)); ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params); address optimizerAddress = address(proxy); - console.log("\n[4/6] OptimizerV3 deployed:", optimizerAddress); + console.log("\n[4/6] OptimizerV3Push3 deployed:", optimizerAddress); // Deploy LiquidityManager liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress); @@ -112,7 +112,7 @@ contract DeployLocal is Script { console.log("Stake:", address(stake)); console.log("Pool:", address(pool)); console.log("LiquidityManager:", address(liquidityManager)); - console.log("OptimizerV3:", optimizerAddress); + console.log("OptimizerV3Push3:", optimizerAddress); console.log("\n=== Next Steps ==="); console.log("1. Fund LiquidityManager with ETH:"); diff --git a/onchain/script/UpgradeOptimizer.sol b/onchain/script/UpgradeOptimizer.sol index e7df2ed..443c525 100644 --- a/onchain/script/UpgradeOptimizer.sol +++ b/onchain/script/UpgradeOptimizer.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "../src/OptimizerV3.sol"; +import "../src/OptimizerV3Push3.sol"; import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; import "forge-std/Script.sol"; @@ -28,22 +28,19 @@ contract UpgradeOptimizer is Script { console.log("Proxy address:", proxyAddress); console.log("Admin (sender):", sender); - // Deploy new OptimizerV3 implementation - OptimizerV3 newImpl = new OptimizerV3(); - console.log("New OptimizerV3 implementation:", address(newImpl)); + // Deploy new OptimizerV3Push3 implementation + OptimizerV3Push3 newImpl = new OptimizerV3Push3(); + console.log("New OptimizerV3Push3 implementation:", address(newImpl)); // Upgrade proxy to new implementation (no reinitialize needed — storage layout compatible) UUPSUpgradeable(proxyAddress).upgradeTo(address(newImpl)); - console.log("Proxy upgraded to OptimizerV3"); + console.log("Proxy upgraded to OptimizerV3Push3"); - // Verify upgrade by calling getLiquidityParams through the proxy - OptimizerV3 upgraded = OptimizerV3(proxyAddress); - (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = upgraded.getLiquidityParams(); + // Verify upgrade by calling isBullMarket through the proxy + OptimizerV3Push3 upgraded = OptimizerV3Push3(proxyAddress); + bool bull = upgraded.isBullMarket(0, 0); console.log("\n=== Post-Upgrade Verification ==="); - console.log("capitalInefficiency:", ci); - console.log("anchorShare:", as_); - console.log("anchorWidth:", uint256(aw)); - console.log("discoveryDepth:", dd); + console.log("isBullMarket(0,0):", bull); vm.stopBroadcast(); } diff --git a/onchain/src/OptimizerV2.sol b/onchain/src/OptimizerV2.sol deleted file mode 100644 index 99e7ce2..0000000 --- a/onchain/src/OptimizerV2.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import { Kraiken } from "./Kraiken.sol"; -import { Stake } from "./Stake.sol"; -import { Math } from "@openzeppelin/utils/math/Math.sol"; - -import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol"; -import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; - -/** - * @title OptimizerV2 - * @notice Sentiment-driven liquidity parameter optimizer based on empirical fuzzing results. - * @dev Replaces the original Optimizer with a mapping informed by adversarial analysis: - * - * Key findings from parameter sweep + Gaussian competition model: - * - * 1. capitalInefficiency has ZERO effect on fee revenue. It only affects floor placement. - * Always set to 0 for maximum safety (adjusted VWAP = 0.7× → furthest floor). - * - * 2. anchorShare and anchorWidth are the ONLY fee levers: - * - Bull: AS=100%, AW=20 → deep narrow anchor → maximizes KRK fees (which appreciate) - * - Bear: AS=10%, AW=100 → thin wide anchor → maximizes WETH fees + safe floor distance - * - * 3. The two regimes naturally align safety with fee optimization: - * - Bearish config is also the safest against drain attacks (AW=100 → 7000 tick clamp) - * - Bullish config maximizes revenue when floor safety is least needed - * - * Staking sentiment drives the interpolation: - * - High staking % + low tax rate → bullish (sentiment=0) → aggressive fee capture - * - Low staking % + high tax rate → bearish (sentiment=1e18) → defensive positioning - */ -contract OptimizerV2 is Initializable, UUPSUpgradeable { - Kraiken private kraiken; - Stake private stake; - - /// @dev Reverts if the caller is not the admin. - error UnauthorizedAccount(address account); - - function initialize(address _kraiken, address _stake) public initializer { - _changeAdmin(msg.sender); - kraiken = Kraiken(_kraiken); - stake = Stake(_stake); - } - - modifier onlyAdmin() { - _checkAdmin(); - _; - } - - function _checkAdmin() internal view virtual { - if (_getAdmin() != msg.sender) { - revert UnauthorizedAccount(msg.sender); - } - } - - function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { } - - /** - * @notice Calculates sentiment from staking metrics. - * @dev Reuses the V1 sentiment formula for continuity. - * sentiment = 0 → bullish, sentiment = 1e18 → bearish. - */ - function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) public pure returns (uint256 sentimentValue) { - require(percentageStaked <= 1e18, "Invalid percentage staked"); - - uint256 deltaS = 1e18 - percentageStaked; - - if (percentageStaked > 92e16) { - uint256 penalty = (deltaS * deltaS * deltaS * averageTaxRate) / (20 * 1e48); - sentimentValue = penalty / 2; - } else { - uint256 scaledStake = (percentageStaked * 1e18) / (92e16); - uint256 baseSentiment = scaledStake >= 1e18 ? 0 : 1e18 - scaledStake; - if (averageTaxRate <= 1e16) { - sentimentValue = baseSentiment; - } else if (averageTaxRate <= 5e16) { - uint256 ratePenalty = ((averageTaxRate - 1e16) * baseSentiment) / (4e16); - sentimentValue = baseSentiment > ratePenalty ? baseSentiment - ratePenalty : 0; - } else { - sentimentValue = 1e18; - } - } - } - - function getSentiment() external view returns (uint256 sentiment) { - uint256 percentageStaked = stake.getPercentageStaked(); - uint256 averageTaxRate = stake.getAverageTaxRate(); - sentiment = calculateSentiment(averageTaxRate, percentageStaked); - } - - /** - * @notice Returns liquidity parameters driven by staking sentiment. - * - * @return capitalInefficiency Always 0 — maximizes floor safety with no fee cost. - * @return anchorShare sqrt-scaled: bull(0)=100% → bear(1e18)=10%. - * @return anchorWidth sqrt-scaled: bull(0)=20 → bear(1e18)=100. - * @return discoveryDepth Interpolated with sentiment (unchanged from V1). - * - * @dev Uses square-root response curve for ASYMMETRIC transitions: - * - Slow ramp to bull: requires sustained high staking to reach aggressive params - * - Fast snap to bear: small drops in staking cause large safety jumps - * Makes staking manipulation expensive: attacker must maintain >90% staking. - */ - function getLiquidityParams() external view returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { - uint256 percentageStaked = stake.getPercentageStaked(); - uint256 averageTaxRate = stake.getAverageTaxRate(); - uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked); - - if (sentiment > 1e18) sentiment = 1e18; - - // CI = 0 always. No fee impact, maximum floor safety. - capitalInefficiency = 0; - - // sqrt(sentiment) for aggressive bear transition: - // sentiment=2.2% (staking=90%) → sqrtS=14.8% → already shifting defensive - // sentiment=13% (staking=80%) → sqrtS=36% → well into defensive - // sentiment=100% (staking=0%) → sqrtS=100% → full bear - uint256 sqrtS = Math.sqrt(sentiment * 1e18); - // sqrtS is now in range [0, 1e18]. Scale to match sentiment range. - - // AS: 100% (bull) → 10% (bear), sqrt-scaled - anchorShare = 1e18 - (sqrtS * 90 / 100); - - // AW: 20 (bull) → 100 (bear), sqrt-scaled - anchorWidth = uint24(20 + (sqrtS * 80 / 1e18)); - - // DD: keep sentiment-driven (V1 behavior) - discoveryDepth = sentiment; - } -} diff --git a/onchain/src/OptimizerV3.sol b/onchain/src/OptimizerV3.sol deleted file mode 100644 index 7f58b9f..0000000 --- a/onchain/src/OptimizerV3.sol +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import { Kraiken } from "./Kraiken.sol"; -import { Stake } from "./Stake.sol"; - -import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol"; -import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; - -/** - * @title OptimizerV3 - * @notice Direct 2D (staking%, avgTax) → binary bear/bull liquidity optimizer. - * @dev Replaces the three-zone score-based model with a direct mapping: - * - * staked ≤ 91% → BEAR always (no euphoria signal) - * staked > 91% → BULL if deltaS³ × effIdx / 20 < 50, else BEAR - * - * where deltaS = 100 - stakedPct, effIdx = min(29, taxIdx + (taxIdx >= 14 ? 1 : 0)) - * - * Bear: AS=30%, AW=100, CI=0, DD=0.3e18 - * Bull: AS=100%, AW=20, CI=0, DD=1e18 - * - * The binary step avoids the AW 30-90 kill zone where intermediate params are exploitable. - * CI = 0 always (proven to have zero effect on fee revenue). - * - * Bull requires >91% staked with low enough tax. Any decline → instant snap to bear. - */ -contract OptimizerV3 is Initializable, UUPSUpgradeable { - Kraiken private kraiken; - Stake private stake; - - /// @dev Reserved storage gap for future upgrades (50 slots total: 2 used + 48 reserved) - uint256[48] private __gap; - - /// @dev Reverts if the caller is not the admin. - error UnauthorizedAccount(address account); - - /// @notice Initializes the proxy with Kraiken and Stake contract references. - /// @param _kraiken The Kraiken token contract address - /// @param _stake The Stake contract address - function initialize(address _kraiken, address _stake) public initializer { - _changeAdmin(msg.sender); - kraiken = Kraiken(_kraiken); - stake = Stake(_stake); - } - - modifier onlyAdmin() { - _checkAdmin(); - _; - } - - function _checkAdmin() internal view virtual { - if (_getAdmin() != msg.sender) { - revert UnauthorizedAccount(msg.sender); - } - } - - function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { } - - /** - * @notice Maps a normalized average tax rate (0-1e18) to an effective tax rate index. - * @dev The Stake contract normalizes: averageTaxRate = rawRate * 1e18 / MAX_TAX_RATE. - * We compare against pre-computed normalized midpoints between adjacent TAX_RATES - * to find the closest index, avoiding double-truncation from denormalization. - * - * The effective index has a +1 shift at position ≥14 to account for the - * non-uniform spacing in the TAX_RATES array (130% → 180% is a 38% jump), - * capped at 29. - */ - function _taxRateToEffectiveIndex(uint256 averageTaxRate) internal pure returns (uint256) { - // Pre-computed normalized midpoints between adjacent TAX_RATES: - // midpoint_norm = ((TAX_RATES[i] + TAX_RATES[i+1]) / 2) * 1e18 / 9700 - // Using these directly avoids integer truncation from denormalization. - uint256 idx; - if (averageTaxRate <= 206_185_567_010_309) idx = 0; // midpoint(1,3) - - else if (averageTaxRate <= 412_371_134_020_618) idx = 1; // midpoint(3,5) - - else if (averageTaxRate <= 618_556_701_030_927) idx = 2; // midpoint(5,8) - - else if (averageTaxRate <= 1_030_927_835_051_546) idx = 3; // midpoint(8,12) - - else if (averageTaxRate <= 1_546_391_752_577_319) idx = 4; // midpoint(12,18) - - else if (averageTaxRate <= 2_164_948_453_608_247) idx = 5; // midpoint(18,24) - - else if (averageTaxRate <= 2_783_505_154_639_175) idx = 6; // midpoint(24,30) - - else if (averageTaxRate <= 3_608_247_422_680_412) idx = 7; // midpoint(30,40) - - else if (averageTaxRate <= 4_639_175_257_731_958) idx = 8; // midpoint(40,50) - - else if (averageTaxRate <= 5_670_103_092_783_505) idx = 9; // midpoint(50,60) - - else if (averageTaxRate <= 7_216_494_845_360_824) idx = 10; // midpoint(60,80) - - else if (averageTaxRate <= 9_278_350_515_463_917) idx = 11; // midpoint(80,100) - - else if (averageTaxRate <= 11_855_670_103_092_783) idx = 12; // midpoint(100,130) - - else if (averageTaxRate <= 15_979_381_443_298_969) idx = 13; // midpoint(130,180) - - else if (averageTaxRate <= 22_164_948_453_608_247) idx = 14; // midpoint(180,250) - - else if (averageTaxRate <= 29_381_443_298_969_072) idx = 15; // midpoint(250,320) - - else if (averageTaxRate <= 38_144_329_896_907_216) idx = 16; // midpoint(320,420) - - else if (averageTaxRate <= 49_484_536_082_474_226) idx = 17; // midpoint(420,540) - - else if (averageTaxRate <= 63_917_525_773_195_876) idx = 18; // midpoint(540,700) - - else if (averageTaxRate <= 83_505_154_639_175_257) idx = 19; // midpoint(700,920) - - else if (averageTaxRate <= 109_278_350_515_463_917) idx = 20; // midpoint(920,1200) - - else if (averageTaxRate <= 144_329_896_907_216_494) idx = 21; // midpoint(1200,1600) - - else if (averageTaxRate <= 185_567_010_309_278_350) idx = 22; // midpoint(1600,2000) - - else if (averageTaxRate <= 237_113_402_061_855_670) idx = 23; // midpoint(2000,2600) - - else if (averageTaxRate <= 309_278_350_515_463_917) idx = 24; // midpoint(2600,3400) - - else if (averageTaxRate <= 402_061_855_670_103_092) idx = 25; // midpoint(3400,4400) - - else if (averageTaxRate <= 520_618_556_701_030_927) idx = 26; // midpoint(4400,5700) - - else if (averageTaxRate <= 680_412_371_134_020_618) idx = 27; // midpoint(5700,7500) - - else if (averageTaxRate <= 886_597_938_144_329_896) idx = 28; // midpoint(7500,9700) - - else idx = 29; - - // Apply effective index shift: +1 at idx >= 14, capped at 29 - if (idx >= 14) { - idx = idx + 1; - if (idx > 29) idx = 29; - } - - return idx; - } - - /** - * @notice Determines if the market is in bull configuration. - * @param percentageStaked Percentage of authorized stake in use (0 to 1e18). - * @param averageTaxRate Normalized average tax rate from Stake contract (0 to 1e18). - * @return bull True if bull config, false if bear. - * - * @dev Direct 2D mapping — no intermediate score: - * staked ≤ 91% → always bear (no euphoria signal) - * staked > 91% → bull if deltaS³ × effIdx / 20 < 50 - * where deltaS = 100 - stakedPct (integer percentage) - */ - function isBullMarket(uint256 percentageStaked, uint256 averageTaxRate) public pure returns (bool bull) { - require(percentageStaked <= 1e18, "Invalid percentage staked"); - - uint256 stakedPct = percentageStaked * 100 / 1e18; // 0-100 - if (stakedPct <= 91) return false; - - uint256 deltaS = 100 - stakedPct; // 0-8 - uint256 effIdx = _taxRateToEffectiveIndex(averageTaxRate); - uint256 penalty = deltaS * deltaS * deltaS * effIdx / 20; - return penalty < 50; - } - - /** - * @notice Returns liquidity parameters driven by the direct 2D staking→config mapping. - * - * @return capitalInefficiency Always 0 — proven to have zero effect on fee revenue. - * @return anchorShare Bear=30% (0.3e18), Bull=100% (1e18). - * @return anchorWidth Bear=100, Bull=20. - * @return discoveryDepth Bear=0.3e18, Bull=1e18. - */ - function getLiquidityParams() external view returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { - uint256 percentageStaked = stake.getPercentageStaked(); - uint256 averageTaxRate = stake.getAverageTaxRate(); - - capitalInefficiency = 0; - - if (isBullMarket(percentageStaked, averageTaxRate)) { - anchorShare = 1e18; // 100% - anchorWidth = 20; - discoveryDepth = 1e18; - } else { - anchorShare = 3e17; // 30% - anchorWidth = 100; - discoveryDepth = 3e17; // 0.3e18 - } - } -} diff --git a/onchain/test/OptimizerV3.t.sol b/onchain/test/OptimizerV3.t.sol deleted file mode 100644 index 3a74b6c..0000000 --- a/onchain/test/OptimizerV3.t.sol +++ /dev/null @@ -1,335 +0,0 @@ -// 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)); - } -} diff --git a/onchain/test/OptimizerV3Push3.t.sol b/onchain/test/OptimizerV3Push3.t.sol index 8ea5ea3..c912b00 100644 --- a/onchain/test/OptimizerV3Push3.t.sol +++ b/onchain/test/OptimizerV3Push3.t.sol @@ -1,27 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import { OptimizerV3 } from "../src/OptimizerV3.sol"; import { OptimizerV3Push3 } from "../src/OptimizerV3Push3.sol"; import "forge-std/Test.sol"; /** * @title OptimizerV3Push3Test - * @notice Verifies that the Push3-transpiled OptimizerV3Push3 produces - * identical results to the hand-written OptimizerV3 for all test cases. - * - * Tests mirror OptimizerV3.t.sol to ensure equivalence. + * @notice Verifies the correctness of OptimizerV3Push3 isBullMarket logic. */ contract OptimizerV3Push3Test is Test { - OptimizerV3 ref; // reference (hand-written) - OptimizerV3Push3 push3; // transpiled from Push3 + 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; function setUp() public { - ref = new OptimizerV3(); push3 = new OptimizerV3Push3(); } @@ -33,36 +27,7 @@ contract OptimizerV3Push3Test is Test { return pct * 1e18 / 100; } - // ---- Equivalence against reference ---- - - function testEquivalenceAllTaxRates() public view { - uint256[7] memory staked = [uint256(0), 50, 91, 92, 95, 96, 100]; - for (uint256 s = 0; s < staked.length; s++) { - for (uint256 t = 0; t < 30; t++) { - bool r = ref.isBullMarket(_pct(staked[s]), _norm(t)); - bool p = push3.isBullMarket(_pct(staked[s]), _norm(t)); - assertEq(p, r, string.concat( - "Mismatch at staked=", vm.toString(staked[s]), - "% taxIdx=", vm.toString(t) - )); - } - } - } - - function testEquivalence93to99Percent() public view { - for (uint256 s = 93; s <= 99; s++) { - for (uint256 t = 0; t < 30; t++) { - bool r = ref.isBullMarket(_pct(s), _norm(t)); - bool p = push3.isBullMarket(_pct(s), _norm(t)); - assertEq(p, r, string.concat( - "Mismatch at staked=", vm.toString(s), - "% taxIdx=", vm.toString(t) - )); - } - } - } - - // ---- Direct correctness tests (mirror OptimizerV3.t.sol) ---- + // ---- Direct correctness tests ---- function testAlwaysBearAt0Percent() public view { for (uint256 t = 0; t < 30; t++) { @@ -128,14 +93,6 @@ contract OptimizerV3Push3Test is Test { // ---- Fuzz ---- - function testFuzzEquivalence(uint256 percentageStaked, uint256 averageTaxRate) public view { - percentageStaked = bound(percentageStaked, 0, 1e18); - averageTaxRate = bound(averageTaxRate, 0, 1e18); - bool r = ref.isBullMarket(percentageStaked, averageTaxRate); - bool p = push3.isBullMarket(percentageStaked, averageTaxRate); - assertEq(p, r, "Push3 result must match reference for all inputs"); - } - function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view { percentageStaked = bound(percentageStaked, 0, 1e18); averageTaxRate = bound(averageTaxRate, 0, 1e18);