Merge pull request 'fix: Remove dead Optimizer V2/V3 — Push3 is the active optimizer (#312)' (#321) from fix/issue-312 into master
This commit is contained in:
commit
c73f0e8bc1
11 changed files with 66 additions and 762 deletions
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/Optimizer.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 Optimizer (if not already deployed)
|
||||
address optimizerAddress;
|
||||
if (optimizer == address(0)) {
|
||||
OptimizerV3 optimizerImpl = new OptimizerV3();
|
||||
Optimizer optimizerImpl = new Optimizer();
|
||||
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("Optimizer 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("Optimizer:", 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");
|
||||
|
|
|
|||
|
|
@ -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 Optimizer (UUPS proxy)
|
||||
optimizer = address(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/Optimizer.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 Optimizer
|
||||
Optimizer optimizerImpl = new Optimizer();
|
||||
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] Optimizer 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("Optimizer:", optimizerAddress);
|
||||
|
||||
console.log("\n=== Next Steps ===");
|
||||
console.log("1. Fund LiquidityManager with ETH:");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "../src/OptimizerV3.sol";
|
||||
import "../src/Optimizer.sol";
|
||||
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "forge-std/Script.sol";
|
||||
|
||||
|
|
@ -28,16 +28,16 @@ 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 Optimizer implementation
|
||||
Optimizer newImpl = new Optimizer();
|
||||
console.log("New Optimizer 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 Optimizer");
|
||||
|
||||
// Verify upgrade by calling getLiquidityParams through the proxy
|
||||
OptimizerV3 upgraded = OptimizerV3(proxyAddress);
|
||||
Optimizer upgraded = Optimizer(proxyAddress);
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = upgraded.getLiquidityParams();
|
||||
console.log("\n=== Post-Upgrade Verification ===");
|
||||
console.log("capitalInefficiency:", ci);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ test.describe('Acquire & Stake', () => {
|
|||
console.log('[TEST] Checking if staking amount input is visible...');
|
||||
await expect(stakeAmountInput).toBeVisible({ timeout: 10_000 });
|
||||
console.log('[TEST] Staking amount input is visible, filling value...');
|
||||
await stakeAmountInput.fill('100');
|
||||
await stakeAmountInput.fill('1000');
|
||||
console.log('[TEST] Filled staking amount!');
|
||||
|
||||
const taxSelect = page.getByRole('combobox', { name: 'Tax' });
|
||||
|
|
@ -260,11 +260,15 @@ test.describe('Acquire & Stake', () => {
|
|||
} catch (e) {
|
||||
console.log('[TEST] Transaction may have completed instantly');
|
||||
}
|
||||
await page.waitForTimeout(3_000);
|
||||
|
||||
// Verify staking position via GraphQL
|
||||
console.log('[TEST] Verifying staking position via GraphQL...');
|
||||
const positions = await fetchPositions(request, ACCOUNT_ADDRESS);
|
||||
// Poll for Ponder to index the staking transaction (Ponder has indexing latency)
|
||||
console.log('[TEST] Polling GraphQL for staking position (Ponder indexing latency)...');
|
||||
let positions: Awaited<ReturnType<typeof fetchPositions>> = [];
|
||||
for (let attempt = 0; attempt < 15; attempt++) {
|
||||
await page.waitForTimeout(2_000);
|
||||
positions = await fetchPositions(request, ACCOUNT_ADDRESS);
|
||||
if (positions.length > 0) break;
|
||||
console.log(`[TEST] Ponder not yet indexed (attempt ${attempt + 1}/15), retrying...`);
|
||||
}
|
||||
console.log(`[TEST] Found ${positions.length} position(s)`);
|
||||
|
||||
expect(positions.length).toBeGreaterThan(0);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ const STACK_RPC_URL = STACK_CONFIG.rpcUrl;
|
|||
// Solidity function selectors
|
||||
const GET_LIQUIDITY_PARAMS_SELECTOR = '0xbd53c0dc'; // getLiquidityParams()
|
||||
const POSITIONS_SELECTOR = '0xf86aafc0'; // positions(uint8)
|
||||
// OptimizerV3 known bear-market parameters
|
||||
const BEAR_ANCHOR_SHARE = 3n * 10n ** 17n; // 3e17 = 30%
|
||||
const BEAR_ANCHOR_WIDTH = 100n;
|
||||
const BEAR_DISCOVERY_DEPTH = 3n * 10n ** 17n; // 3e17
|
||||
|
||||
// Optimizer.sol invariants (capitalInefficiency + anchorShare = 1e18)
|
||||
const ONE_ETHER = 10n ** 18n;
|
||||
|
||||
// Position stages
|
||||
const STAGE_FLOOR = 0;
|
||||
|
|
@ -62,7 +61,7 @@ test.describe('Optimizer Integration', () => {
|
|||
await validateStackHealthy(STACK_CONFIG);
|
||||
});
|
||||
|
||||
test('OptimizerV3 proxy returns valid bear-market parameters', async () => {
|
||||
test('Optimizer proxy returns valid parameters', async () => {
|
||||
const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
|
||||
if (!optimizerAddress) {
|
||||
console.log(
|
||||
|
|
@ -80,16 +79,19 @@ test.describe('Optimizer Integration', () => {
|
|||
console.log(`[TEST] anchorWidth: ${params.anchorWidth}`);
|
||||
console.log(`[TEST] discoveryDepth: ${params.discoveryDepth}`);
|
||||
|
||||
// With no staking activity, OptimizerV3 should return bear-market defaults
|
||||
expect(params.capitalInefficiency).toBe(0n);
|
||||
expect(params.anchorShare).toBe(BEAR_ANCHOR_SHARE);
|
||||
expect(params.anchorWidth).toBe(BEAR_ANCHOR_WIDTH);
|
||||
expect(params.discoveryDepth).toBe(BEAR_DISCOVERY_DEPTH);
|
||||
// Optimizer.sol invariants:
|
||||
// capitalInefficiency + anchorShare == 1e18
|
||||
expect(params.capitalInefficiency + params.anchorShare).toBe(ONE_ETHER);
|
||||
// discoveryDepth == anchorShare
|
||||
expect(params.discoveryDepth).toBe(params.anchorShare);
|
||||
// anchorWidth in [10, 80]
|
||||
expect(params.anchorWidth).toBeGreaterThanOrEqual(10n);
|
||||
expect(params.anchorWidth).toBeLessThanOrEqual(80n);
|
||||
|
||||
console.log('[TEST] OptimizerV3 returns correct bear-market parameters');
|
||||
console.log('[TEST] Optimizer returns valid parameters (invariants satisfied)');
|
||||
});
|
||||
|
||||
test('bootstrap positions reflect optimizer anchorWidth=100 parameter', async () => {
|
||||
test('bootstrap positions reflect valid optimizer anchorWidth', async () => {
|
||||
const lmAddress = STACK_CONFIG.contracts.LiquidityManager;
|
||||
const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
|
||||
if (!optimizerAddress) {
|
||||
|
|
@ -97,12 +99,11 @@ test.describe('Optimizer Integration', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Read optimizer params
|
||||
const params = await readLiquidityParams(optimizerAddress);
|
||||
const anchorWidth = Number(params.anchorWidth);
|
||||
console.log(`[TEST] Optimizer anchorWidth: ${anchorWidth}`);
|
||||
|
||||
// Read anchor position from LM (created by bootstrap's recenter call)
|
||||
// Read anchor position from LM (created by bootstrap's recenter call).
|
||||
// Note: optimizer state may have changed since bootstrap (e.g. staking activity in
|
||||
// earlier tests), so we don't read the *current* optimizer params here. Instead
|
||||
// we reverse-calculate the anchorWidth that was in effect when recenter() ran and
|
||||
// verify it falls within Optimizer.sol's valid range [10, 80].
|
||||
const anchorResult = (await rpcCall('eth_call', [
|
||||
{
|
||||
to: lmAddress,
|
||||
|
|
@ -117,16 +118,25 @@ test.describe('Optimizer Integration', () => {
|
|||
|
||||
console.log(`[TEST] Anchor position: ticks=[${tickLower}, ${tickUpper}], spread=${anchorSpread}`);
|
||||
|
||||
// Verify the anchor spread matches the optimizer formula:
|
||||
// anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100)
|
||||
// For anchorWidth=100: anchorSpacing = 200 + (34 * 100 * 200 / 100) = 200 + 6800 = 7000
|
||||
// Full anchor = 2 * anchorSpacing = 14000 ticks
|
||||
const expectedSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING) / 100;
|
||||
const expectedSpread = expectedSpacing * 2;
|
||||
console.log(`[TEST] Expected anchor spread: ${expectedSpread} (anchorSpacing=${expectedSpacing})`);
|
||||
// Reverse the formula to recover anchorWidth:
|
||||
// anchorSpread = 2 * (TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100))
|
||||
// => anchorWidth = (anchorSpread / 2 - TICK_SPACING) * 100 / (34 * TICK_SPACING)
|
||||
const halfSpread = anchorSpread / 2;
|
||||
expect(halfSpread).toBeGreaterThan(TICK_SPACING);
|
||||
|
||||
const impliedAnchorWidth = Math.round(((halfSpread - TICK_SPACING) * 100) / (34 * TICK_SPACING));
|
||||
console.log(`[TEST] Implied anchorWidth from spread: ${impliedAnchorWidth}`);
|
||||
|
||||
// Optimizer.sol constrains anchorWidth to [10, 80]
|
||||
expect(impliedAnchorWidth).toBeGreaterThanOrEqual(10);
|
||||
expect(impliedAnchorWidth).toBeLessThanOrEqual(80);
|
||||
|
||||
// Confirm the implied anchorWidth reproduces the exact spread (no rounding error)
|
||||
const expectedSpacing = TICK_SPACING + (34 * impliedAnchorWidth * TICK_SPACING) / 100;
|
||||
const expectedSpread = expectedSpacing * 2;
|
||||
expect(anchorSpread).toBe(expectedSpread);
|
||||
console.log('[TEST] Anchor spread matches optimizer anchorWidth=100 formula');
|
||||
|
||||
console.log(`[TEST] Anchor spread ${anchorSpread} corresponds to valid anchorWidth=${impliedAnchorWidth}`);
|
||||
});
|
||||
|
||||
test('all three positions have valid relative sizing', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue