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:
parent
1e5ac0de80
commit
e925538309
9 changed files with 23 additions and 736 deletions
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:");
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue