132 lines
5.6 KiB
Solidity
132 lines
5.6 KiB
Solidity
|
|
// 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;
|
|||
|
|
}
|
|||
|
|
}
|