// 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 Optimizer * @notice This contract (formerly Sentimenter) calculates a "sentiment" value and liquidity parameters * based on the tax rate and the percentage of Kraiken staked. * @dev It is upgradeable using UUPS. Only the admin (set during initialization) can upgrade. * * Key features: * - Analyzes staking sentiment (% staked, average tax rate) * - Returns four key parameters for liquidity management: * 1. capitalInefficiency (0 to 1e18): Capital buffer level * 2. anchorShare (0 to 1e18): % of non-floor ETH in anchor * 3. anchorWidth (0 to 100): Anchor position width % * 4. discoveryDepth (0 to 1e18): Discovery liquidity density (2x-10x) * - Upgradeable for future algorithm improvements * * AnchorWidth Price Ranges: * The anchor position's price range depends on anchorWidth value: * - anchorWidth = 10: ±9% range (0.92x to 1.09x current price) * - anchorWidth = 40: ±33% range (0.75x to 1.34x current price) * - anchorWidth = 50: ±42% range (0.70x to 1.43x current price) * - anchorWidth = 80: ±74% range (0.57x to 1.75x current price) * - anchorWidth = 100: -50% to +100% range (0.50x to 2.00x current price) * * The formula: anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100) * creates a non-linear price range due to Uniswap V3's tick-based system */ contract Optimizer is Initializable, UUPSUpgradeable { Kraiken private kraiken; Stake private stake; /// @dev Reverts if the caller is not the admin. error UnauthorizedAccount(address account); /** * @notice Initialize the Optimizer. * @param _kraiken The address of the Kraiken token. * @param _stake The address of the Stake contract. */ function initialize(address _kraiken, address _stake) public initializer { // Set the admin for upgradeability (using ERC1967Upgrade _changeAdmin) _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 the sentiment based on the average tax rate and the percentage staked. * @param averageTaxRate The average tax rate (as returned by the Stake contract). * @param percentageStaked The percentage (in 1e18 precision) of the authorized stake that is currently staked. * @return sentimentValue A value in the range 0 to 1e18 where 1e18 represents the worst sentiment. */ function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) public pure returns (uint256 sentimentValue) { // Ensure percentageStaked doesn't exceed 100% require(percentageStaked <= 1e18, "Invalid percentage staked"); // deltaS is the "slack" available below full staking uint256 deltaS = 1e18 - percentageStaked; if (percentageStaked > 92e16) { // If more than 92% of the authorized stake is in use, the sentiment drops rapidly. // Penalty is computed as: (deltaS^3 * averageTaxRate) / (20 * 1e48) uint256 penalty = (deltaS * deltaS * deltaS * averageTaxRate) / (20 * 1e48); sentimentValue = penalty / 2; } else { // For lower staked percentages, sentiment decreases roughly linearly. // Ensure we don't underflow if percentageStaked approaches 92% uint256 scaledStake = (percentageStaked * 1e18) / (92e16); uint256 baseSentiment = scaledStake >= 1e18 ? 0 : 1e18 - scaledStake; // Apply a penalty based on the average tax rate. if (averageTaxRate <= 1e16) { sentimentValue = baseSentiment; } else if (averageTaxRate <= 5e16) { uint256 ratePenalty = ((averageTaxRate - 1e16) * baseSentiment) / (4e16); sentimentValue = baseSentiment > ratePenalty ? baseSentiment - ratePenalty : 0; } else { // For very high tax rates, sentiment is maximally poor. sentimentValue = 1e18; } } return sentimentValue; } /** * @notice Returns the current sentiment. * @return sentiment A number (with 1e18 precision) representing the staker sentiment. */ function getSentiment() external view returns (uint256 sentiment) { uint256 percentageStaked = stake.getPercentageStaked(); uint256 averageTaxRate = stake.getAverageTaxRate(); sentiment = calculateSentiment(averageTaxRate, percentageStaked); } /** * @notice Calculates the optimal anchor width based on staking metrics. * @param percentageStaked The percentage of tokens staked (0 to 1e18) * @param averageTaxRate The average tax rate across all stakers (0 to 1e18) * @return anchorWidth The calculated anchor width (10 to 80) * * @dev This function implements a staking-based approach to determine anchor width: * * Base Strategy: * - Start with base width of 40% (balanced default) * * Staking Adjustment (-20% to +20%): * - High staking (>70%) indicates bullish confidence → narrow anchor for fee optimization * - Low staking (<30%) indicates bearish/uncertainty → wide anchor for safety * - Inverse relationship: higher staking = lower width adjustment * * Tax Rate Adjustment (-10% to +30%): * - High tax rates signal expected volatility → wider anchor to reduce rebalancing * - Low tax rates signal expected stability → narrower anchor for fee collection * - Direct relationship: higher tax = higher width adjustment * * The Harberger tax mechanism acts as a decentralized prediction market where: * - Tax rates reflect holders' expectations of being "snatched" (volatility) * - Staking percentage reflects overall market confidence * * Final width is clamped between 10 (minimum safe) and 80 (maximum effective) */ function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) internal pure returns (uint24) { // Base width: 40% is our neutral starting point int256 baseWidth = 40; // Staking adjustment: -20% to +20% based on staking percentage // Formula: 20 - (percentageStaked * 40 / 1e18) // High staking (1e18) → -20 adjustment → narrower width // Low staking (0) → +20 adjustment → wider width int256 stakingAdjustment = 20 - int256(percentageStaked * 40 / 1e18); // Tax rate adjustment: -10% to +30% based on average tax rate // Formula: (averageTaxRate * 40 / 1e18) - 10 // High tax (1e18) → +30 adjustment → wider width for volatility // Low tax (0) → -10 adjustment → narrower width for stability int256 taxAdjustment = int256(averageTaxRate * 40 / 1e18) - 10; // Combine all adjustments int256 totalWidth = baseWidth + stakingAdjustment + taxAdjustment; // Clamp to safe bounds (10 to 80) // Below 10%: rebalancing costs exceed benefits // Above 80%: capital efficiency degrades significantly if (totalWidth < 10) { return 10; } if (totalWidth > 80) { return 80; } return uint24(uint256(totalWidth)); } /** * @notice Returns liquidity parameters for the liquidity manager. * @return capitalInefficiency Calculated as (1e18 - sentiment). Capital buffer level (0-1e18) * @return anchorShare Set equal to the sentiment. % of non-floor ETH in anchor (0-1e18) * @return anchorWidth Dynamically adjusted based on staking metrics. Anchor position width % (1-100) * @return discoveryDepth Set equal to the sentiment. * * @dev AnchorWidth Strategy: * The anchorWidth parameter controls the price range of the anchor liquidity position. * - anchorWidth = 50: Price range from 0.70x to 1.43x current price * - anchorWidth = 100: Price range from 0.50x to 2.00x current price * * We use staking metrics as a decentralized prediction market: * - High staking % → Bullish sentiment → Narrower width (30-50%) for fee optimization * - Low staking % → Bearish/uncertain → Wider width (60-80%) for defensive positioning * - High avg tax rate → Expects volatility → Wider anchor to reduce rebalancing * - Low avg tax rate → Expects stability → Narrower anchor for fee collection */ 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); // Ensure sentiment doesn't exceed 1e18 to prevent underflow // Cap sentiment at 1e18 if it somehow exceeds it if (sentiment > 1e18) { sentiment = 1e18; } capitalInefficiency = 1e18 - sentiment; anchorShare = sentiment; // Calculate dynamic anchorWidth based on staking metrics anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate); discoveryDepth = sentiment; } }