harb/onchain/src/OptimizerV3.sol
openhands 85350caf52 feat: OptimizerV3 with direct 2D staking-to-LP parameter mapping
Core protocol changes for launch readiness:

- OptimizerV3: binary bear/bull mapping from (staking%, avgTax) — avoids
  exploitable AW 30-90 kill zone. Bear: AS=30%, AW=100, CI=0, DD=0.3e18.
  Bull: AS=100%, AW=20, CI=0, DD=1e18. UUPS upgradeable with __gap[48].
- Directional VWAP: only records prices on ETH inflow (buys), preventing
  sell-side dilution of price memory
- Floor formula: unified max(scarcity, mirror, clamp) — VWAP mirror uses
  distance from adjusted VWAP as floor distance, no branching
- PriceOracle (M-1 fix): correct fallback TWAP divisor (60000s, not 300s)
- Access control (M-2 fix): deployer-only guard on one-time setters
- Recenter rate limit (M-3 fix): 60-second cooldown for open recenters
- Safe fallback params: recenter() optimizer-failure defaults changed from
  exploitable CI=50%/AW=50 to safe bear-mode CI=0/AW=100
- Recentered event for monitoring and indexing
- VERSION bump to 2, kraiken-lib COMPATIBLE_CONTRACT_VERSIONS updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:21:18 +00:00

191 lines
7.8 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}
}
}