fix: Remove dead Optimizer V2/V3 — Push3 is the active optimizer (#312)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-02-26 14:20:11 +00:00
parent 1e5ac0de80
commit e925538309
9 changed files with 23 additions and 736 deletions

View file

@ -2,7 +2,6 @@
pragma solidity ^0.8.19; pragma solidity ^0.8.19;
import { Optimizer } from "../src/Optimizer.sol"; import { Optimizer } from "../src/Optimizer.sol";
import { OptimizerV2 } from "../src/OptimizerV2.sol";
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol"; import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";
import { BearMarketOptimizer } from "../test/mocks/BearMarketOptimizer.sol"; import { BearMarketOptimizer } from "../test/mocks/BearMarketOptimizer.sol";
@ -78,11 +77,6 @@ contract StreamlinedFuzzing is FuzzingBase {
address optimizer = _deployOptimizer(optimizerClass); address optimizer = _deployOptimizer(optimizerClass);
_setupEnvironment(optimizer, runIndex % 2 == 0, uncapped); _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 // Deploy background LP if configured
if (bgLpEthPerLayer > 0) { if (bgLpEthPerLayer > 0) {
_deployBackgroundLP(bgLpEthPerLayer); _deployBackgroundLP(bgLpEthPerLayer);
@ -292,10 +286,6 @@ contract StreamlinedFuzzing is FuzzingBase {
return address(new ExtremeOptimizer()); return address(new ExtremeOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("MaliciousOptimizer"))) { } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("MaliciousOptimizer"))) {
return address(new 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 { } else {
return address(new BullMarketOptimizer()); return address(new BullMarketOptimizer());
} }

View file

@ -4,7 +4,7 @@ pragma solidity ^0.8.19;
import "../src/Kraiken.sol"; import "../src/Kraiken.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol"; import { LiquidityManager } from "../src/LiquidityManager.sol";
import "../src/OptimizerV3.sol"; import "../src/OptimizerV3Push3.sol";
import "../src/Stake.sol"; import "../src/Stake.sol";
import "../src/helpers/UniswapHelpers.sol"; import "../src/helpers/UniswapHelpers.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
@ -71,14 +71,14 @@ contract DeployBase is Script {
console.log("Pool initialized"); console.log("Pool initialized");
} }
// Deploy OptimizerV3 (if not already deployed) // Deploy OptimizerV3Push3 (if not already deployed)
address optimizerAddress; address optimizerAddress;
if (optimizer == address(0)) { if (optimizer == address(0)) {
OptimizerV3 optimizerImpl = new OptimizerV3(); OptimizerV3Push3 optimizerImpl = new OptimizerV3Push3();
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake)); bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params); ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params);
optimizerAddress = address(proxy); optimizerAddress = address(proxy);
console.log("OptimizerV3 deployed at:", optimizerAddress); console.log("OptimizerV3Push3 deployed at:", optimizerAddress);
} else { } else {
optimizerAddress = optimizer; optimizerAddress = optimizer;
console.log("Using existing optimizer at:", optimizerAddress); console.log("Using existing optimizer at:", optimizerAddress);
@ -99,7 +99,7 @@ contract DeployBase is Script {
console.log("Stake:", address(stake)); console.log("Stake:", address(stake));
console.log("Pool:", address(pool)); console.log("Pool:", address(pool));
console.log("LiquidityManager:", address(liquidityManager)); console.log("LiquidityManager:", address(liquidityManager));
console.log("OptimizerV3:", optimizerAddress); console.log("OptimizerV3Push3:", optimizerAddress);
console.log("\nPost-deploy steps:"); console.log("\nPost-deploy steps:");
console.log(" 1. Fund LiquidityManager with ETH"); console.log(" 1. Fund LiquidityManager with ETH");
console.log(" 2. Set recenterAccess to txnBot: lm.setRecenterAccess(txnBot) from feeDestination"); console.log(" 2. Set recenterAccess to txnBot: lm.setRecenterAccess(txnBot) from feeDestination");

View file

@ -17,7 +17,7 @@ contract DeployBaseMainnet is DeployBase {
weth = 0x4200000000000000000000000000000000000006; // WETH on Base weth = 0x4200000000000000000000000000000000000006; // WETH on Base
v3Factory = 0x33128a8fC17869897dcE68Ed026d694621f6FDfD; // Uniswap V3 Factory on Base v3Factory = 0x33128a8fC17869897dcE68Ed026d694621f6FDfD; // Uniswap V3 Factory on Base
// Deploy fresh OptimizerV3 (UUPS proxy) // Deploy fresh OptimizerV3Push3 (UUPS proxy)
optimizer = address(0); optimizer = address(0);
} }
} }

View file

@ -4,7 +4,7 @@ pragma solidity ^0.8.19;
import "../src/Kraiken.sol"; import "../src/Kraiken.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol"; import { LiquidityManager } from "../src/LiquidityManager.sol";
import "../src/OptimizerV3.sol"; import "../src/OptimizerV3Push3.sol";
import "../src/Stake.sol"; import "../src/Stake.sol";
import "../src/helpers/UniswapHelpers.sol"; import "../src/helpers/UniswapHelpers.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.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"); console.log(" Pool initialized at 1 cent price");
} }
// Deploy OptimizerV3 // Deploy OptimizerV3Push3
OptimizerV3 optimizerImpl = new OptimizerV3(); OptimizerV3Push3 optimizerImpl = new OptimizerV3Push3();
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake)); bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params); ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params);
address optimizerAddress = address(proxy); address optimizerAddress = address(proxy);
console.log("\n[4/6] OptimizerV3 deployed:", optimizerAddress); console.log("\n[4/6] OptimizerV3Push3 deployed:", optimizerAddress);
// Deploy LiquidityManager // Deploy LiquidityManager
liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress); liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress);
@ -112,7 +112,7 @@ contract DeployLocal is Script {
console.log("Stake:", address(stake)); console.log("Stake:", address(stake));
console.log("Pool:", address(pool)); console.log("Pool:", address(pool));
console.log("LiquidityManager:", address(liquidityManager)); console.log("LiquidityManager:", address(liquidityManager));
console.log("OptimizerV3:", optimizerAddress); console.log("OptimizerV3Push3:", optimizerAddress);
console.log("\n=== Next Steps ==="); console.log("\n=== Next Steps ===");
console.log("1. Fund LiquidityManager with ETH:"); console.log("1. Fund LiquidityManager with ETH:");

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19; pragma solidity ^0.8.19;
import "../src/OptimizerV3.sol"; import "../src/OptimizerV3Push3.sol";
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
import "forge-std/Script.sol"; import "forge-std/Script.sol";
@ -28,22 +28,19 @@ contract UpgradeOptimizer is Script {
console.log("Proxy address:", proxyAddress); console.log("Proxy address:", proxyAddress);
console.log("Admin (sender):", sender); console.log("Admin (sender):", sender);
// Deploy new OptimizerV3 implementation // Deploy new OptimizerV3Push3 implementation
OptimizerV3 newImpl = new OptimizerV3(); OptimizerV3Push3 newImpl = new OptimizerV3Push3();
console.log("New OptimizerV3 implementation:", address(newImpl)); console.log("New OptimizerV3Push3 implementation:", address(newImpl));
// Upgrade proxy to new implementation (no reinitialize needed storage layout compatible) // Upgrade proxy to new implementation (no reinitialize needed storage layout compatible)
UUPSUpgradeable(proxyAddress).upgradeTo(address(newImpl)); 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 // Verify upgrade by calling isBullMarket through the proxy
OptimizerV3 upgraded = OptimizerV3(proxyAddress); OptimizerV3Push3 upgraded = OptimizerV3Push3(proxyAddress);
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = upgraded.getLiquidityParams(); bool bull = upgraded.isBullMarket(0, 0);
console.log("\n=== Post-Upgrade Verification ==="); console.log("\n=== Post-Upgrade Verification ===");
console.log("capitalInefficiency:", ci); console.log("isBullMarket(0,0):", bull);
console.log("anchorShare:", as_);
console.log("anchorWidth:", uint256(aw));
console.log("discoveryDepth:", dd);
vm.stopBroadcast(); vm.stopBroadcast();
} }

View file

@ -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;
}
}

View file

@ -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 stakingconfig 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
}
}
}

View file

@ -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));
}
}

View file

@ -1,27 +1,21 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19; pragma solidity ^0.8.19;
import { OptimizerV3 } from "../src/OptimizerV3.sol";
import { OptimizerV3Push3 } from "../src/OptimizerV3Push3.sol"; import { OptimizerV3Push3 } from "../src/OptimizerV3Push3.sol";
import "forge-std/Test.sol"; import "forge-std/Test.sol";
/** /**
* @title OptimizerV3Push3Test * @title OptimizerV3Push3Test
* @notice Verifies that the Push3-transpiled OptimizerV3Push3 produces * @notice Verifies the correctness of OptimizerV3Push3 isBullMarket logic.
* identical results to the hand-written OptimizerV3 for all test cases.
*
* Tests mirror OptimizerV3.t.sol to ensure equivalence.
*/ */
contract OptimizerV3Push3Test is Test { contract OptimizerV3Push3Test is Test {
OptimizerV3 ref; // reference (hand-written) OptimizerV3Push3 push3;
OptimizerV3Push3 push3; // transpiled from Push3
uint256[30] TAX_RATES = 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(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; uint256 constant MAX_TAX = 9700;
function setUp() public { function setUp() public {
ref = new OptimizerV3();
push3 = new OptimizerV3Push3(); push3 = new OptimizerV3Push3();
} }
@ -33,36 +27,7 @@ contract OptimizerV3Push3Test is Test {
return pct * 1e18 / 100; return pct * 1e18 / 100;
} }
// ---- Equivalence against reference ---- // ---- Direct correctness tests ----
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) ----
function testAlwaysBearAt0Percent() public view { function testAlwaysBearAt0Percent() public view {
for (uint256 t = 0; t < 30; t++) { for (uint256 t = 0; t < 30; t++) {
@ -128,14 +93,6 @@ contract OptimizerV3Push3Test is Test {
// ---- Fuzz ---- // ---- 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 { function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {
percentageStaked = bound(percentageStaked, 0, 1e18); percentageStaked = bound(percentageStaked, 0, 1e18);
averageTaxRate = bound(averageTaxRate, 0, 1e18); averageTaxRate = bound(averageTaxRate, 0, 1e18);