harb/onchain/src/OptimizerV2.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

131 lines
5.6 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 { 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;
}
}