Add require(shift == 0) guards to Optimizer.calculateParams and OptimizerV3.calculateParams so non-zero shifts revert instead of being silently discarded. OptimizerV3Push3 already had this guard. Update IOptimizer.sol NatSpec to document that shift is reserved for future use and must be 0 in all current implementations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
535 lines
26 KiB
Solidity
535 lines
26 KiB
Solidity
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
pragma solidity ^0.8.19;
|
||
|
||
import { IOptimizer, OptimizerInput, BEAR_CAPITAL_INEFFICIENCY, BEAR_ANCHOR_SHARE, BEAR_ANCHOR_WIDTH, BEAR_DISCOVERY_DEPTH } from "./IOptimizer.sol";
|
||
import { Kraiken } from "./Kraiken.sol";
|
||
import { Stake } from "./Stake.sol";
|
||
|
||
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
||
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
|
||
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
||
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Dyadic rational interface — Push3's native number format.
|
||
// Represents: mantissa × 2^(-shift).
|
||
// _toDyadic wraps an on-chain value with shift=0 (value == mantissa).
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Minimal interface for VWAPTracker (slots 2-4 computation)
|
||
interface IVWAPTracker {
|
||
function getVWAP() external view returns (uint256);
|
||
}
|
||
|
||
// Minimal interface for Uniswap V3 pool (slots 2-4, 6 computation)
|
||
interface IUniswapV3PoolSlot0 {
|
||
function slot0()
|
||
external
|
||
view
|
||
returns (
|
||
uint160 sqrtPriceX96,
|
||
int24 tick,
|
||
uint16 observationIndex,
|
||
uint16 observationCardinality,
|
||
uint16 observationCardinalityNext,
|
||
uint8 feeProtocol,
|
||
bool unlocked
|
||
);
|
||
}
|
||
|
||
// Minimal interface for pool TWAP observations (slots 3-4 computation)
|
||
interface IUniswapV3PoolObserve {
|
||
function observe(uint32[] calldata secondsAgos)
|
||
external
|
||
view
|
||
returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s);
|
||
}
|
||
|
||
// Minimal interface for LiquidityManager position data (slot 6 computation)
|
||
interface ILiquidityManagerPositions {
|
||
function positions(uint8 stage) external view returns (uint128 liquidity, int24 tickLower, int24 tickUpper);
|
||
}
|
||
|
||
/**
|
||
* @title Optimizer
|
||
* @notice Calculates liquidity parameters for the LiquidityManager using an
|
||
* 8-slot dyadic rational input interface (Push3's native format).
|
||
*
|
||
* @dev Upgradeable (UUPS). The core logic lives in `calculateParams`, which is
|
||
* a pure function taking an OptimizerInput[8] array. Future upgrades may
|
||
* replace `calculateParams` with a transpiled Push3 program via the
|
||
* evolution pipeline (#544, #545, #546).
|
||
*
|
||
* Input slots (all values in [0, 1e18] — uniform range makes evolution feasible):
|
||
* 0 percentageStaked 0..1e18 % of supply staked (Stake.getPercentageStaked())
|
||
* 1 averageTaxRate 0..1e18 Normalized tax rate (Stake.getAverageTaxRate())
|
||
* 2 pricePosition 0..1e18 Current price vs VWAP ± PRICE_BOUND_TICKS.
|
||
* 0 = at lower bound, 0.5e18 = at VWAP, 1e18 = at upper bound.
|
||
* (0 if vwapTracker/pool not configured or no VWAP data yet)
|
||
* 3 volatility 0..1e18 Normalized recent price volatility: |shortTwap - longTwap|
|
||
* ticks / MAX_VOLATILITY_TICKS, capped at 1e18.
|
||
* (0 if pool not configured or insufficient TWAP history)
|
||
* 4 momentum 0..1e18 Price trend: 0 = strongly falling, 0.5e18 = flat,
|
||
* 1e18 = strongly rising. Derived from short vs long TWAP.
|
||
* (0 if pool not configured or insufficient TWAP history)
|
||
* 5 timeSinceRecenter 0..1e18 Normalized time since last recenter.
|
||
* 0 = just recentered, 1e18 = MAX_STALE_SECONDS elapsed.
|
||
* (0 if recordRecenter has never been called)
|
||
* 6 utilizationRate 0..1e18 1e18 if current tick is within anchor position range,
|
||
* 0 otherwise. (0 if liquidityManager/pool not configured)
|
||
* 7 reserved 0 Future use.
|
||
*
|
||
* Four optimizer outputs (0..1e18 fractions unless noted):
|
||
* capitalInefficiency capital buffer level
|
||
* anchorShare fraction of non-floor ETH in anchor
|
||
* anchorWidth anchor position width (tick units, uint24)
|
||
* discoveryDepth discovery liquidity density
|
||
*/
|
||
contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
|
||
Kraiken private kraiken;
|
||
Stake private stake;
|
||
|
||
// ---- Extended data sources for input slots 2-6 ----
|
||
// These are optional; unset addresses leave the corresponding slots as 0.
|
||
address public vwapTracker; // slots 2-4 source (VWAPTracker)
|
||
address public pool; // slots 2-4, 6 source (Uniswap V3 pool)
|
||
uint256 public lastRecenterTimestamp; // slot 5 source (updated via recordRecenter)
|
||
address public recenterRecorder; // authorized to call recordRecenter
|
||
address public liquidityManager; // slot 6 source (LiquidityManager positions)
|
||
bool public token0isWeth; // true when WETH is token0 in the pool (flips tick direction)
|
||
|
||
// ---- Normalization constants ----
|
||
|
||
/// @notice Half-width in ticks for pricePosition normalization.
|
||
/// pricePosition = 0 at (vwapTick - PRICE_BOUND_TICKS), 1e18 at (vwapTick + PRICE_BOUND_TICKS).
|
||
/// 11 000 ticks ≈ the discovery position half-width (3× price from anchor).
|
||
int256 internal constant PRICE_BOUND_TICKS = 11_000;
|
||
|
||
/// @notice Maximum tick divergence (shortTwap vs longTwap) that maps to full volatility (1e18).
|
||
/// 1 000 ticks ≈ 10% price swing.
|
||
uint256 internal constant MAX_VOLATILITY_TICKS = 1000;
|
||
|
||
/// @notice Maximum tick trend signal (shortTwap - longTwap) for momentum saturation.
|
||
/// 1 000 ticks ≈ 10% price trend.
|
||
int256 internal constant MAX_MOMENTUM_TICKS = 1000;
|
||
|
||
/// @notice Time (seconds) beyond which timeSinceRecenter saturates at 1e18. 86 400 = 1 day.
|
||
uint256 internal constant MAX_STALE_SECONDS = 86_400;
|
||
|
||
/// @notice Short TWAP window for volatility / momentum (5 minutes = same as price-stability check).
|
||
uint32 internal constant SHORT_TWAP_WINDOW = 300;
|
||
|
||
/// @notice Long TWAP window for volatility / momentum baseline (30 minutes).
|
||
uint32 internal constant LONG_TWAP_WINDOW = 1800;
|
||
|
||
/// @dev Reverts if the caller is not the admin.
|
||
error UnauthorizedAccount(address account);
|
||
|
||
/// @dev Gas budget forwarded to calculateParams via staticcall.
|
||
/// Evolved programs that exceed this are treated as crashes — same outcome
|
||
/// as a revert — and getLiquidityParams() returns bear defaults instead.
|
||
/// 500 000 gives ~33x headroom over the current seed (~15 k gas) while
|
||
/// preventing unbounded growth from blocking recenter().
|
||
///
|
||
/// Note (EIP-150 / 63-64 rule): the outer getLiquidityParams() call must
|
||
/// arrive with at least ⌈500_000 × 64/63⌉ ≈ 507_937 gas for the inner
|
||
/// staticcall to actually receive 500 000. Callers with exactly 500–508 k
|
||
/// gas will see a spurious bear-defaults fallback. This is not a practical
|
||
/// concern from recenter(), which always has abundant gas.
|
||
uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 500_000;
|
||
|
||
/**
|
||
* @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 { }
|
||
|
||
// ---- Data-source configuration (admin only) ----
|
||
|
||
/**
|
||
* @notice Configure optional on-chain data sources for input slots 2-6.
|
||
* @param _vwapTracker VWAPTracker contract address (slots 2-4); zero = disabled.
|
||
* @param _pool Uniswap V3 pool address (slots 2-4, 6); zero = disabled.
|
||
* @param _liquidityManager LiquidityManager address (slot 6); zero = disabled.
|
||
* @param _token0isWeth True when WETH is token0 in the pool. Needed to correctly
|
||
* orient tick-based indicators (pricePosition, volatility, momentum).
|
||
*/
|
||
function setDataSources(address _vwapTracker, address _pool, address _liquidityManager, bool _token0isWeth) external onlyAdmin {
|
||
vwapTracker = _vwapTracker;
|
||
pool = _pool;
|
||
liquidityManager = _liquidityManager;
|
||
token0isWeth = _token0isWeth;
|
||
}
|
||
|
||
/**
|
||
* @notice Set the address authorized to call recordRecenter.
|
||
* @param _recorder The LiquidityManager or other authorized address.
|
||
*/
|
||
function setRecenterRecorder(address _recorder) external onlyAdmin {
|
||
recenterRecorder = _recorder;
|
||
}
|
||
|
||
/**
|
||
* @notice Record a recenter event for slot 5 (timeSinceLastRecenter).
|
||
* @dev Called by the LiquidityManager (or recenterRecorder) after each recenter.
|
||
*/
|
||
function recordRecenter() external {
|
||
if (msg.sender != recenterRecorder && msg.sender != _getAdmin()) {
|
||
revert UnauthorizedAccount(msg.sender);
|
||
}
|
||
lastRecenterTimestamp = block.timestamp;
|
||
}
|
||
|
||
// ---- Dyadic rational helpers ----
|
||
|
||
/**
|
||
* @notice Wrap an integer as a dyadic rational with shift=0.
|
||
* value = mantissa × 2^(-0) = mantissa.
|
||
*/
|
||
function _toDyadic(int256 value) internal pure returns (OptimizerInput memory) {
|
||
return OptimizerInput({ mantissa: value, shift: 0 });
|
||
}
|
||
|
||
/**
|
||
* @notice Safe bear-mode defaults returned when calculateParams exceeds its
|
||
* gas budget or reverts.
|
||
* @dev Constants defined in IOptimizer.sol, shared with LiquidityManager.recenter().
|
||
*/
|
||
function _bearDefaults() internal pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
|
||
return (BEAR_CAPITAL_INEFFICIENCY, BEAR_ANCHOR_SHARE, BEAR_ANCHOR_WIDTH, BEAR_DISCOVERY_DEPTH);
|
||
}
|
||
|
||
// ---- Normalization helpers ----
|
||
|
||
/**
|
||
* @notice Convert a Q96 price (price * 2^96) to the corresponding Uniswap V3 tick.
|
||
*
|
||
* @dev VWAP is stored as `price * 2^96` where `price = sqrtPriceX96^2 / 2^96`.
|
||
* Inverting: `sqrtPriceX96 = sqrt(vwapX96) << 48`.
|
||
* Integer sqrt introduces at most ±1 ULP error in sqrtPriceX96, which
|
||
* translates to at most ±1 tick error — acceptable for normalization.
|
||
*
|
||
* Overflow guard: for prices near TickMath extremes, `sqrt(vwapX96) << 48`
|
||
* can approach or exceed uint160 max. We clamp to TickMath's valid range.
|
||
*
|
||
* @param vwapX96 VWAP in Q96 price format (token1/token0 × 2^96).
|
||
* @return vwapTick The Uniswap V3 tick closest to the VWAP price.
|
||
*/
|
||
function _vwapToTick(uint256 vwapX96) internal pure returns (int24 vwapTick) {
|
||
uint256 sqrtVwap = Math.sqrt(vwapX96); // = sqrt(price) * 2^48
|
||
uint256 shifted = sqrtVwap << 48; // ≈ sqrtPriceX96 = sqrt(price) * 2^96
|
||
uint160 sqrtPriceX96;
|
||
if (shifted >= uint256(TickMath.MAX_SQRT_RATIO)) {
|
||
sqrtPriceX96 = TickMath.MAX_SQRT_RATIO - 1;
|
||
} else if (shifted < uint256(TickMath.MIN_SQRT_RATIO)) {
|
||
sqrtPriceX96 = TickMath.MIN_SQRT_RATIO;
|
||
} else {
|
||
sqrtPriceX96 = uint160(shifted);
|
||
}
|
||
vwapTick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
|
||
}
|
||
|
||
// ---- Core computation ----
|
||
|
||
/**
|
||
* @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)
|
||
*/
|
||
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
|
||
int256 stakingAdjustment = 20 - int256(percentageStaked * 40 / 1e18);
|
||
|
||
// Tax rate adjustment: -10% to +30% based on average tax rate
|
||
int256 taxAdjustment = int256(averageTaxRate * 40 / 1e18) - 10;
|
||
|
||
// Combine all adjustments
|
||
int256 totalWidth = baseWidth + stakingAdjustment + taxAdjustment;
|
||
|
||
// Clamp to safe bounds (10 to 80)
|
||
if (totalWidth < 10) {
|
||
return 10;
|
||
}
|
||
if (totalWidth > 80) {
|
||
return 80;
|
||
}
|
||
|
||
return uint24(uint256(totalWidth));
|
||
}
|
||
|
||
/**
|
||
* @notice Pure computation of all four liquidity parameters from 8 dyadic inputs.
|
||
*
|
||
* @dev This is the transpilation target: future versions of this function will be
|
||
* generated from evolved Push3 programs via the transpiler. The current
|
||
* implementation uses slots 0 (percentageStaked) and 1 (averageTaxRate);
|
||
* slots 2-7 are available to evolved programs that use the normalized indicators.
|
||
*
|
||
* @param inputs 8 dyadic rational slots. For shift == 0 (via _toDyadic), value == mantissa.
|
||
* inputs[0].mantissa = percentageStaked (0..1e18)
|
||
* inputs[1].mantissa = averageTaxRate (0..1e18)
|
||
* inputs[2].mantissa = pricePosition (0..1e18)
|
||
* inputs[3].mantissa = volatility (0..1e18)
|
||
* inputs[4].mantissa = momentum (0..1e18)
|
||
* inputs[5].mantissa = timeSinceRecenter (0..1e18)
|
||
* inputs[6].mantissa = utilizationRate (0..1e18)
|
||
* inputs[7] = reserved (0)
|
||
*
|
||
* @return capitalInefficiency Capital buffer level (0..1e18). CI=0 is safest.
|
||
* @return anchorShare Fraction of non-floor ETH in anchor (0..1e18).
|
||
* @return anchorWidth Anchor position width in tick units (uint24).
|
||
* @return discoveryDepth Discovery liquidity density (0..1e18).
|
||
*/
|
||
function calculateParams(OptimizerInput[8] memory inputs)
|
||
public
|
||
pure
|
||
virtual
|
||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
||
{
|
||
// Guard against non-zero shift and negative mantissa.
|
||
// shift is reserved for future use; uint256() cast silently wraps negatives.
|
||
for (uint256 k; k < 8; k++) {
|
||
require(inputs[k].shift == 0, "shift not yet supported");
|
||
require(inputs[k].mantissa >= 0, "negative mantissa");
|
||
}
|
||
// Extract slots 0 and 1 (shift=0 assumed — mantissa IS the value)
|
||
uint256 percentageStaked = uint256(inputs[0].mantissa);
|
||
uint256 averageTaxRate = uint256(inputs[1].mantissa);
|
||
|
||
uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked);
|
||
if (sentiment > 1e18) {
|
||
sentiment = 1e18;
|
||
}
|
||
|
||
capitalInefficiency = 1e18 - sentiment;
|
||
anchorShare = sentiment;
|
||
anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate);
|
||
discoveryDepth = sentiment;
|
||
}
|
||
|
||
/**
|
||
* @notice Returns liquidity parameters for the LiquidityManager.
|
||
*
|
||
* @dev Populates the 8-slot dyadic input array with normalized indicators
|
||
* (all in [0, 1e18]) and delegates to calculateParams. Normalization
|
||
* happens here so that evolved Push3 programs can reason about relative
|
||
* positions without dealing with raw Q96 prices or absolute ticks.
|
||
*
|
||
* Slots populated:
|
||
* 0 percentageStaked always
|
||
* 1 averageTaxRate always
|
||
* 2 pricePosition when vwapTracker + pool configured and VWAP > 0
|
||
* 3 volatility when pool configured and TWAP history available
|
||
* 4 momentum when pool configured and TWAP history available
|
||
* 5 timeSinceRecenter when recordRecenter has been called at least once
|
||
* 6 utilizationRate when liquidityManager + pool configured
|
||
* 7 reserved always 0
|
||
*
|
||
*/
|
||
/**
|
||
* @notice Build the 8-slot normalized input array from on-chain data sources.
|
||
* @dev Extracted so test harnesses can observe the computed inputs without
|
||
* duplicating normalization logic. All slots are in [0, 1e18].
|
||
*/
|
||
function _buildInputs() internal view returns (OptimizerInput[8] memory inputs) {
|
||
// Slot 0: percentageStaked
|
||
inputs[0] = _toDyadic(int256(stake.getPercentageStaked()));
|
||
|
||
// Slot 1: averageTaxRate
|
||
inputs[1] = _toDyadic(int256(stake.getAverageTaxRate()));
|
||
|
||
// Slots 2-4 (pricePosition, volatility, momentum) and slot 6 (utilizationRate)
|
||
// all require the pool address. Read slot0 once and reuse.
|
||
if (pool != address(0)) {
|
||
(, int24 poolTick,,,,,) = IUniswapV3PoolSlot0(pool).slot0();
|
||
|
||
// ---- Slot 2: pricePosition (also needs VWAP) ----
|
||
if (vwapTracker != address(0)) {
|
||
uint256 vwapX96 = IVWAPTracker(vwapTracker).getVWAP();
|
||
if (vwapX96 > 0) {
|
||
// Convert pool tick to KRK-price space: higher tick = more expensive KRK.
|
||
// Uniswap convention: tick ↑ → token1 more expensive relative to token0.
|
||
// If token0=WETH (token1=KRK): tick ↑ → KRK/WETH ↑ → KRK more expensive.
|
||
// No sign flip needed — pool tick already tracks KRK price direction.
|
||
// If token0=KRK (token1=WETH): tick ↑ → WETH/KRK ↑ → KRK cheaper → negate.
|
||
// Same convention as LiquidityManager._priceAtTick(token0isWeth ? -tick : tick).
|
||
int24 currentAdjTick = token0isWeth ? poolTick : -poolTick;
|
||
|
||
// vwapTick in same adjusted (KRK-price) space
|
||
int24 vwapAdjTick = _vwapToTick(vwapX96);
|
||
|
||
// Slot 2: pricePosition — where is current price vs VWAP ± PRICE_BOUND_TICKS?
|
||
// 0 = at lower bound (vwap − bound), 0.5e18 = at VWAP, 1e18 = at upper bound.
|
||
int256 delta = int256(currentAdjTick) - int256(vwapAdjTick);
|
||
int256 shifted = delta + PRICE_BOUND_TICKS; // map to [0, 2*bound]
|
||
if (shifted < 0) shifted = 0;
|
||
if (shifted > 2 * PRICE_BOUND_TICKS) shifted = 2 * PRICE_BOUND_TICKS;
|
||
inputs[2] = _toDyadic(int256(uint256(shifted) * 1e18 / uint256(2 * PRICE_BOUND_TICKS)));
|
||
}
|
||
}
|
||
|
||
// ---- Slots 3-4: volatility and momentum from pool TWAP ----
|
||
// Independent of VWAP — only the pool oracle is required.
|
||
// Fails gracefully if the pool lacks sufficient observation history.
|
||
{
|
||
uint32[] memory secondsAgo = new uint32[](3);
|
||
secondsAgo[0] = LONG_TWAP_WINDOW; // 1800 s — long baseline
|
||
secondsAgo[1] = SHORT_TWAP_WINDOW; // 300 s — recent
|
||
secondsAgo[2] = 0; // now
|
||
try IUniswapV3PoolObserve(pool).observe(secondsAgo) returns (int56[] memory tickCumulatives, uint160[] memory) {
|
||
int24 longTwap = int24((tickCumulatives[2] - tickCumulatives[0]) / int56(int32(LONG_TWAP_WINDOW)));
|
||
int24 shortTwap = int24((tickCumulatives[2] - tickCumulatives[1]) / int56(int32(SHORT_TWAP_WINDOW)));
|
||
|
||
// Adjust both TWAP ticks to KRK-price space (same sign convention)
|
||
int24 longAdj = token0isWeth ? longTwap : -longTwap;
|
||
int24 shortAdj = token0isWeth ? shortTwap : -shortTwap;
|
||
int256 twapDelta = int256(shortAdj) - int256(longAdj);
|
||
|
||
// Slot 3: volatility = |shortTwap − longTwap| / MAX_VOLATILITY_TICKS
|
||
{
|
||
uint256 absDelta = twapDelta >= 0 ? uint256(twapDelta) : uint256(-twapDelta);
|
||
uint256 vol = absDelta >= MAX_VOLATILITY_TICKS ? 1e18 : absDelta * 1e18 / MAX_VOLATILITY_TICKS;
|
||
inputs[3] = _toDyadic(int256(vol));
|
||
}
|
||
|
||
// Slot 4: momentum — 0.5e18 flat, 1e18 strongly rising, 0 strongly falling
|
||
{
|
||
int256 momentum;
|
||
if (twapDelta >= MAX_MOMENTUM_TICKS) {
|
||
momentum = int256(1e18);
|
||
} else if (twapDelta <= -MAX_MOMENTUM_TICKS) {
|
||
momentum = 0;
|
||
} else {
|
||
momentum = int256(5e17) + twapDelta * int256(5e17) / MAX_MOMENTUM_TICKS;
|
||
}
|
||
inputs[4] = _toDyadic(momentum);
|
||
}
|
||
} catch {
|
||
// Insufficient TWAP history — leave slots 3-4 as 0
|
||
}
|
||
}
|
||
|
||
// Slot 6: utilizationRate — 1e18 if current tick is within anchor range, else 0.
|
||
// Stage.ANCHOR == 1 in the ThreePositionStrategy enum.
|
||
if (liquidityManager != address(0)) {
|
||
(, int24 anchorLower, int24 anchorUpper) = ILiquidityManagerPositions(liquidityManager).positions(1);
|
||
if (poolTick >= anchorLower && poolTick <= anchorUpper) {
|
||
inputs[6] = _toDyadic(int256(1e18));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Slot 5: timeSinceRecenter normalized to [0, 1e18].
|
||
// 0 = just recentered, 1e18 = MAX_STALE_SECONDS or more have elapsed.
|
||
if (lastRecenterTimestamp > 0) {
|
||
uint256 elapsed = block.timestamp - lastRecenterTimestamp;
|
||
uint256 normalized = elapsed >= MAX_STALE_SECONDS ? 1e18 : elapsed * 1e18 / MAX_STALE_SECONDS;
|
||
inputs[5] = _toDyadic(int256(normalized));
|
||
}
|
||
|
||
// Slot 7: reserved (0)
|
||
}
|
||
|
||
/**
|
||
* @return capitalInefficiency Capital buffer level (0..1e18)
|
||
* @return anchorShare Fraction of non-floor ETH in anchor (0..1e18)
|
||
* @return anchorWidth Anchor position width in tick units (uint24)
|
||
* @return discoveryDepth Discovery liquidity density (0..1e18)
|
||
*/
|
||
function getLiquidityParams()
|
||
external
|
||
view
|
||
override
|
||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
||
{
|
||
OptimizerInput[8] memory inputs = _buildInputs();
|
||
|
||
// Call calculateParams with a fixed gas budget. Evolved programs that grow
|
||
// too large hit the cap and fall back to bear defaults — preventing any
|
||
// buggy or bloated optimizer from blocking recenter() with an OOG revert.
|
||
(bool ok, bytes memory ret) = address(this).staticcall{ gas: CALCULATE_PARAMS_GAS_LIMIT }(abi.encodeCall(this.calculateParams, (inputs)));
|
||
if (!ok) return _bearDefaults();
|
||
// ABI encoding of (uint256, uint256, uint24, uint256) is exactly 128 bytes
|
||
// (each value padded to 32 bytes). A truncated return — e.g. from a
|
||
// malformed evolved program — would cause abi.decode to revert; guard here
|
||
// so all failure modes fall back via _bearDefaults().
|
||
if (ret.length < 128) return _bearDefaults();
|
||
(capitalInefficiency, anchorShare, anchorWidth, discoveryDepth) = abi.decode(ret, (uint256, uint256, uint24, uint256));
|
||
// Clamp fraction outputs to [0, 1e18] so a buggy evolved program cannot
|
||
// produce out-of-range values that confuse the LiquidityManager.
|
||
// anchorWidth is already bounded by uint24 at the ABI level.
|
||
if (capitalInefficiency > 1e18) capitalInefficiency = 1e18;
|
||
if (anchorShare > 1e18) anchorShare = 1e18;
|
||
if (discoveryDepth > 1e18) discoveryDepth = 1e18;
|
||
}
|
||
}
|