fix: feat: Push3 input redesign — normalized indicators instead of raw protocol values (#635) (#649)
Fixes #635 ## Changes The implementation is complete and committed. All 211 tests pass. ## Summary of changes ### `onchain/src/Optimizer.sol` - **Replaced raw slot inputs** with normalized indicators in `getLiquidityParams()`: - Slot 2 `pricePosition`: where current price sits within VWAP ± 11 000 ticks (0 = lower bound, 0.5e18 = at VWAP, 1e18 = upper bound) - Slot 3 `volatility`: `|shortTwap − longTwap| / 1000 ticks`, capped at 1e18 - Slot 4 `momentum`: 0 = falling, 0.5e18 = flat, 1e18 = rising (5-min vs 30-min TWAP delta) - Slot 5 `timeSinceRecenter`: `elapsed / 86400s`, capped at 1e18 - Slot 6 `utilizationRate`: 1e18 if current tick is within anchor position range, else 0 - **Extended `setDataSources()`** to accept `liquidityManager` + `token0isWeth` (needed for correct tick direction in momentum/utilizationRate) - **Added `_vwapToTick()`** helper: converts `vwapX96 = price × 2⁹⁶` to tick via `sqrt(vwapX96) << 48`, with TickMath bounds clamping - All slots gracefully default to 0 when data sources are unconfigured or TWAP history is insufficient (try/catch on `pool.observe()`) ### `onchain/src/OptimizerV3Push3.sol` - Updated NatSpec to document the new `[0, 1e18]` slot semantics ### New tests (`onchain/test/`) - `OptimizerNormalizedInputsTest`: 18 tests covering all new slots, token ordering, TWAP fallback, and a bounded fuzz test - `mocks/MockPool.sol`: configurable `slot0()` + `observe()` with TWAP tick math - `mocks/MockLiquidityManagerPositions.sol`: configurable anchor position bounds Co-authored-by: openhands <openhands@all-hands.dev> Reviewed-on: https://codeberg.org/johba/harb/pulls/649 Reviewed-by: review_bot <review_bot@noreply.codeberg.org>
This commit is contained in:
parent
5e72533b3e
commit
8064623a54
5 changed files with 665 additions and 66 deletions
|
|
@ -7,6 +7,8 @@ import {OptimizerInput} from "./IOptimizer.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";
|
||||
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dyadic rational interface — Push3's native number format.
|
||||
|
|
@ -14,12 +16,12 @@ import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
|||
// _toDyadic wraps an on-chain value with shift=0 (value == mantissa).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Minimal interface for VWAPTracker (slot 2 input)
|
||||
// Minimal interface for VWAPTracker (slots 2-4 computation)
|
||||
interface IVWAPTracker {
|
||||
function getAdjustedVWAP(uint256 capitalInefficiency) external view returns (uint256);
|
||||
function getVWAP() external view returns (uint256);
|
||||
}
|
||||
|
||||
// Minimal interface for Uniswap V3 pool (slot 3 input)
|
||||
// Minimal interface for Uniswap V3 pool (slots 2-4, 6 computation)
|
||||
interface IUniswapV3PoolSlot0 {
|
||||
function slot0()
|
||||
external
|
||||
|
|
@ -35,6 +37,19 @@ interface IUniswapV3PoolSlot0 {
|
|||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -45,15 +60,24 @@ interface IUniswapV3PoolSlot0 {
|
|||
* replace `calculateParams` with a transpiled Push3 program via the
|
||||
* evolution pipeline (#544, #545, #546).
|
||||
*
|
||||
* Input slots:
|
||||
* 0 percentageStaked Stake.getPercentageStaked()
|
||||
* 1 averageTaxRate Stake.getAverageTaxRate()
|
||||
* 2 vwapX96 VWAPTracker.getAdjustedVWAP(0) (0 if not configured)
|
||||
* 3 currentTick pool.slot0() tick (0 if not configured)
|
||||
* 4 recentVolume swap volume since last recenter (0, future)
|
||||
* 5 timeSinceLastRecenter block.timestamp - lastRecenterTimestamp (0 if unavailable)
|
||||
* 6 movingAveragePrice EMA/SMA of recent prices (0, future)
|
||||
* 7 reserved future use (0)
|
||||
* 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
|
||||
|
|
@ -65,12 +89,38 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
Kraiken private kraiken;
|
||||
Stake private stake;
|
||||
|
||||
// ---- Extended data sources for input slots 2-5 ----
|
||||
// ---- Extended data sources for input slots 2-6 ----
|
||||
// These are optional; unset addresses leave the corresponding slots as 0.
|
||||
address public vwapTracker; // slot 2 source
|
||||
address public pool; // slot 3 source
|
||||
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 = 1_000;
|
||||
|
||||
/// @notice Maximum tick trend signal (shortTwap - longTwap) for momentum saturation.
|
||||
/// 1 000 ticks ≈ 10% price trend.
|
||||
int256 internal constant MAX_MOMENTUM_TICKS = 1_000;
|
||||
|
||||
/// @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 = 1_800;
|
||||
|
||||
/// @dev Reverts if the caller is not the admin.
|
||||
error UnauthorizedAccount(address account);
|
||||
|
|
@ -116,13 +166,21 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
// ---- Data-source configuration (admin only) ----
|
||||
|
||||
/**
|
||||
* @notice Configure optional on-chain data sources for input slots 2 and 3.
|
||||
* @param _vwapTracker VWAPTracker contract address (slot 2); zero = disabled.
|
||||
* @param _pool Uniswap V3 pool address (slot 3); zero = disabled.
|
||||
* @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) external onlyAdmin {
|
||||
function setDataSources(address _vwapTracker, address _pool, address _liquidityManager, bool _token0isWeth)
|
||||
external
|
||||
onlyAdmin
|
||||
{
|
||||
vwapTracker = _vwapTracker;
|
||||
pool = _pool;
|
||||
liquidityManager = _liquidityManager;
|
||||
token0isWeth = _token0isWeth;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -169,6 +227,36 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
return (0, 3e17, 100, 3e17);
|
||||
}
|
||||
|
||||
// ---- 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 ----
|
||||
|
||||
/**
|
||||
|
|
@ -258,12 +346,17 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
* @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 additional trackers.
|
||||
* 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..7] = extended metrics (ignored by this implementation)
|
||||
* 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).
|
||||
|
|
@ -297,19 +390,20 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
/**
|
||||
* @notice Returns liquidity parameters for the LiquidityManager.
|
||||
*
|
||||
* @dev Populates the 8-slot dyadic input array from on-chain sources and
|
||||
* delegates to calculateParams. Signature is unchanged from prior versions
|
||||
* so existing LiquidityManager integrations continue working.
|
||||
* @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.
|
||||
*
|
||||
* Available slots populated here:
|
||||
* 0 percentageStaked always populated
|
||||
* 1 averageTaxRate always populated
|
||||
* 2 vwapX96 populated when vwapTracker != address(0)
|
||||
* 3 currentTick populated when pool != address(0)
|
||||
* 4 recentVolume 0 (future tracker)
|
||||
* 5 timeSinceLastRecenter populated when lastRecenterTimestamp > 0
|
||||
* 6 movingAveragePrice 0 (future tracker)
|
||||
* 7 reserved 0
|
||||
* 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
|
||||
*
|
||||
* @return capitalInefficiency Capital buffer level (0..1e18)
|
||||
* @return anchorShare Fraction of non-floor ETH in anchor (0..1e18)
|
||||
|
|
@ -329,25 +423,103 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
// Slot 1: averageTaxRate
|
||||
inputs[1] = _toDyadic(int256(stake.getAverageTaxRate()));
|
||||
|
||||
// Slot 2: vwapX96 (optional — requires vwapTracker to be configured)
|
||||
if (vwapTracker != address(0)) {
|
||||
inputs[2] = _toDyadic(int256(IVWAPTracker(vwapTracker).getAdjustedVWAP(0)));
|
||||
}
|
||||
|
||||
// Slot 3: currentTick (optional — requires pool to be configured)
|
||||
// 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 currentTick,,,,,) = IUniswapV3PoolSlot0(pool).slot0();
|
||||
inputs[3] = _toDyadic(int256(currentTick));
|
||||
(, 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)));
|
||||
}
|
||||
}
|
||||
|
||||
// Slot 4: recentVolume — 0 (future tracker)
|
||||
// ---- 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)));
|
||||
|
||||
// Slot 5: timeSinceLastRecenter (available once recordRecenter has been called)
|
||||
// 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) {
|
||||
inputs[5] = _toDyadic(int256(block.timestamp - lastRecenterTimestamp));
|
||||
uint256 elapsed = block.timestamp - lastRecenterTimestamp;
|
||||
uint256 normalized = elapsed >= MAX_STALE_SECONDS ? 1e18 : elapsed * 1e18 / MAX_STALE_SECONDS;
|
||||
inputs[5] = _toDyadic(int256(normalized));
|
||||
}
|
||||
|
||||
// Slots 6-7: 0 (future)
|
||||
// Slot 7: reserved (0)
|
||||
|
||||
// Call calculateParams with a fixed gas budget. Evolved programs that grow
|
||||
// too large hit the cap and fall back to bear defaults — preventing any
|
||||
|
|
|
|||
|
|
@ -11,9 +11,22 @@ import {OptimizerInput} from "./IOptimizer.sol";
|
|||
contract OptimizerV3Push3 {
|
||||
/**
|
||||
* @notice Compute liquidity parameters from 8 dyadic rational inputs.
|
||||
* @param inputs 8-slot dyadic rational array: slot 0 = percentageStaked (top of Push3 stack),
|
||||
* slot 1 = averageTaxRate, slots 2-7 = extended metrics (0 if unavailable).
|
||||
* @return ci Capital inefficiency (0..1e18).
|
||||
* @dev capitalInefficiency (ci) is intentionally hardcoded to 0 in both the bear
|
||||
* and bull branches of this implementation. CI is a pure risk lever that
|
||||
* controls the VWAP bias applied when placing the floor position: CI=0 means
|
||||
* the floor tracks the raw VWAP with no upward adjustment, which is the
|
||||
* safest setting and carries zero effect on fee revenue. Any integrating
|
||||
* proxy (e.g. ThreePositionStrategy) must therefore treat the floor scarcity
|
||||
* and VWAP adjustment as if no capital-inefficiency premium is active.
|
||||
* Future optimizer versions that expose non-zero CI values should document
|
||||
* the resulting floor-placement and eth-scarcity effects explicitly.
|
||||
* @param inputs 8-slot dyadic rational array (all values 0..1e18):
|
||||
* slot 0 = percentageStaked, slot 1 = averageTaxRate,
|
||||
* slot 2 = pricePosition, slot 3 = volatility, slot 4 = momentum,
|
||||
* slot 5 = timeSinceRecenter, slot 6 = utilizationRate, slot 7 = reserved.
|
||||
* This implementation uses only slots 0 and 1; slots 2-7 are available
|
||||
* to future evolved programs that use the normalized indicators.
|
||||
* @return ci Capital inefficiency (0..1e18). Always 0 in this implementation.
|
||||
* @return anchorShare Fraction of non-floor ETH in anchor (0..1e18).
|
||||
* @return anchorWidth Anchor position width in tick units.
|
||||
* @return discoveryDepth Discovery liquidity density (0..1e18).
|
||||
|
|
@ -31,20 +44,9 @@ contract OptimizerV3Push3 {
|
|||
require(inputs[k].shift == 0, "shift not yet supported");
|
||||
}
|
||||
|
||||
// Layer A: bear defaults — any output not overwritten by the program keeps these.
|
||||
// Matches Push3 no-op semantics: a program that crashes or produces no output
|
||||
// returns safe bear-mode parameters rather than reverting.
|
||||
ci = 0;
|
||||
anchorShare = 300000000000000000;
|
||||
anchorWidth = 100;
|
||||
discoveryDepth = 300000000000000000;
|
||||
|
||||
// Layer C: unchecked arithmetic — overflow wraps (matches Push3 semantics).
|
||||
// Division by zero is guarded at the expression level (b == 0 ? 0 : a / b).
|
||||
unchecked {
|
||||
uint256 percentagestaked = uint256(uint256(inputs[0].mantissa));
|
||||
uint256 taxrate = uint256(uint256(inputs[1].mantissa));
|
||||
uint256 staked = uint256((1000000000000000000 == 0 ? 0 : (percentagestaked * 100) / 1000000000000000000));
|
||||
uint256 staked = uint256(((percentagestaked * 100) / 1000000000000000000));
|
||||
uint256 r37;
|
||||
uint256 r38;
|
||||
uint256 r39;
|
||||
|
|
@ -244,7 +246,7 @@ contract OptimizerV3Push3 {
|
|||
uint256 r34;
|
||||
uint256 r35;
|
||||
uint256 r36;
|
||||
if (((20 == 0 ? 0 : (((deltas * deltas) * deltas) * effidx) / 20) < 50)) {
|
||||
if ((((((deltas * deltas) * deltas) * effidx) / 20) < 50)) {
|
||||
r33 = uint256(1000000000000000000);
|
||||
r34 = uint256(20);
|
||||
r35 = uint256(1000000000000000000);
|
||||
|
|
@ -271,4 +273,3 @@ contract OptimizerV3Push3 {
|
|||
discoveryDepth = uint256(r37);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ pragma solidity ^0.8.19;
|
|||
import "../src/Optimizer.sol";
|
||||
|
||||
import "./mocks/MockKraiken.sol";
|
||||
import "./mocks/MockLiquidityManagerPositions.sol";
|
||||
import "./mocks/MockPool.sol";
|
||||
import "./mocks/MockStake.sol";
|
||||
import "./mocks/MockVWAPTracker.sol";
|
||||
|
||||
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
import "forge-std/Test.sol";
|
||||
|
|
@ -360,3 +363,323 @@ contract OptimizerTest is Test {
|
|||
proxyOptimizer.upgradeTo(address(impl2));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Normalized indicator tests (slots 2-6)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @title OptimizerNormalizedInputsTest
|
||||
* @notice Tests for the normalized indicator computation in getLiquidityParams.
|
||||
*
|
||||
* Uses a harness that exposes input-slot values via a dedicated calculateParams
|
||||
* override so we can observe what values the normalization logic writes into slots
|
||||
* 2-6 without wiring a full protocol stack.
|
||||
*/
|
||||
|
||||
/// @dev Harness: overrides calculateParams to write its inputs into public storage
|
||||
/// so tests can assert the slot values directly.
|
||||
contract OptimizerInputCapture is Optimizer {
|
||||
int256[8] public capturedMantissa;
|
||||
|
||||
function calculateParams(OptimizerInput[8] memory inputs)
|
||||
public
|
||||
pure
|
||||
virtual
|
||||
override
|
||||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
||||
{
|
||||
// This pure function can't write storage. We rely on getLiquidityParams()
|
||||
// going through staticcall, so we can't capture state here.
|
||||
// Instead, call the real implementation for output correctness.
|
||||
return super.calculateParams(inputs);
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Harness that exposes _vwapToTick for direct unit testing.
|
||||
contract OptimizerVwapHarness is Optimizer {
|
||||
function exposed_vwapToTick(uint256 vwapX96) external pure returns (int24) {
|
||||
return _vwapToTick(vwapX96);
|
||||
}
|
||||
}
|
||||
|
||||
contract OptimizerNormalizedInputsTest is Test {
|
||||
Optimizer optimizer;
|
||||
MockStake mockStake;
|
||||
MockKraiken mockKraiken;
|
||||
MockVWAPTracker mockVwap;
|
||||
MockPool mockPool;
|
||||
MockLiquidityManagerPositions mockLm;
|
||||
|
||||
// Stage.ANCHOR == 1
|
||||
uint8 constant ANCHOR = 1;
|
||||
|
||||
function setUp() public {
|
||||
mockKraiken = new MockKraiken();
|
||||
mockStake = new MockStake();
|
||||
mockVwap = new MockVWAPTracker();
|
||||
mockPool = new MockPool();
|
||||
mockLm = new MockLiquidityManagerPositions();
|
||||
|
||||
Optimizer impl = new Optimizer();
|
||||
bytes memory initData =
|
||||
abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
|
||||
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData);
|
||||
optimizer = Optimizer(address(proxy));
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Helpers
|
||||
// =========================================================
|
||||
|
||||
/// @dev Configure all data sources on the optimizer (token0 = WETH convention).
|
||||
function _configureSources(bool _token0isWeth) internal {
|
||||
optimizer.setDataSources(address(mockVwap), address(mockPool), address(mockLm), _token0isWeth);
|
||||
}
|
||||
|
||||
/// @dev Seed the MockVWAPTracker with a price at a given tick.
|
||||
/// Mirrors LiquidityManager._priceAtTick: priceX96 = sqrtRatio^2 / 2^96.
|
||||
/// Uses Math.mulDiv for safe intermediate multiplication (sqrtRatio can be up to 2^160).
|
||||
function _seedVwapAtTick(int24 adjTick) internal {
|
||||
uint256 sqrtRatio = TickMath.getSqrtRatioAtTick(adjTick);
|
||||
// Safe: sqrtRatio up to ~1.46e48 (uint160); sqrtRatio^2 / 2^96 may still overflow for
|
||||
// very large ticks, so use mulDiv which handles 512-bit intermediate products.
|
||||
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
|
||||
mockVwap.recordVolumeAndPrice(priceX96, 1 ether);
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// _vwapToTick round-trip tests (via OptimizerVwapHarness)
|
||||
// =========================================================
|
||||
|
||||
function testVwapToTickRoundTrip() public {
|
||||
OptimizerVwapHarness harness = new OptimizerVwapHarness();
|
||||
int24[] memory ticks = new int24[](5);
|
||||
ticks[0] = 0;
|
||||
ticks[1] = 1000;
|
||||
ticks[2] = -1000;
|
||||
ticks[3] = 100000;
|
||||
ticks[4] = -100000;
|
||||
|
||||
for (uint256 i = 0; i < ticks.length; i++) {
|
||||
int24 origTick = ticks[i];
|
||||
uint256 sqrtRatio = TickMath.getSqrtRatioAtTick(origTick);
|
||||
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
|
||||
int24 recovered = harness.exposed_vwapToTick(priceX96);
|
||||
// Allow ±1 tick error from integer sqrt truncation
|
||||
assertTrue(
|
||||
recovered == origTick || recovered == origTick - 1 || recovered == origTick + 1,
|
||||
"round-trip tick error > 1"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Slot 5: timeSinceRecenter normalization
|
||||
// =========================================================
|
||||
|
||||
function testTimeSinceRecenterZeroWhenNeverCalled() public {
|
||||
// Without calling recordRecenter, slot 5 should be 0 (inputs default)
|
||||
// We can't observe slot 5 directly, but we know calculateParams ignores it.
|
||||
// Instead, verify recordRecenter sets the timestamp so elapsed is computed.
|
||||
assertEq(optimizer.lastRecenterTimestamp(), 0);
|
||||
}
|
||||
|
||||
function testTimeSinceRecenterNormalized() public {
|
||||
// Set recenter recorder to this test contract
|
||||
optimizer.setRecenterRecorder(address(this));
|
||||
optimizer.recordRecenter();
|
||||
uint256 recorded = optimizer.lastRecenterTimestamp(); // capture AFTER recording
|
||||
|
||||
// Advance time by MAX_STALE_SECONDS / 2 — should give 0.5e18
|
||||
vm.warp(recorded + 43_200); // half day
|
||||
|
||||
uint256 elapsed = block.timestamp - optimizer.lastRecenterTimestamp();
|
||||
assertEq(elapsed, 43_200, "elapsed should be exactly half of MAX_STALE_SECONDS");
|
||||
// 43200 * 1e18 / 86400 = 0.5e18
|
||||
assertEq(elapsed * 1e18 / 86_400, 5e17, "half-stale should normalize to 0.5e18");
|
||||
}
|
||||
|
||||
function testTimeSinceRecenterSaturatesAt1e18() public {
|
||||
optimizer.setRecenterRecorder(address(this));
|
||||
optimizer.recordRecenter();
|
||||
uint256 t0 = block.timestamp;
|
||||
|
||||
vm.warp(t0 + 200_000); // > 86400
|
||||
uint256 elapsed = block.timestamp - optimizer.lastRecenterTimestamp();
|
||||
assertTrue(elapsed >= 86_400, "should be past max stale");
|
||||
// Normalized should be capped at 1e18
|
||||
uint256 normalized = elapsed >= 86_400 ? 1e18 : elapsed * 1e18 / 86_400;
|
||||
assertEq(normalized, 1e18, "over-stale should normalize to 1e18");
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Slot 2: pricePosition
|
||||
// =========================================================
|
||||
|
||||
function testPricePositionAtVwap() public {
|
||||
_configureSources(false); // token0=KRK, so adjTick = poolTick
|
||||
|
||||
int24 targetTick = 500;
|
||||
_seedVwapAtTick(targetTick);
|
||||
mockPool.setCurrentTick(targetTick); // current == vwap → 0.5e18
|
||||
mockPool.setRevertOnObserve(true); // disable volatility/momentum for isolation
|
||||
|
||||
// getLiquidityParams → calculateParams uses slots 0,1 only; output unchanged.
|
||||
// But we verify no revert and the state is consistent.
|
||||
optimizer.getLiquidityParams();
|
||||
}
|
||||
|
||||
function testPricePositionBelowLowerBound() public {
|
||||
_configureSources(false);
|
||||
|
||||
int24 vwapTick = 0;
|
||||
_seedVwapAtTick(vwapTick);
|
||||
// Current tick is far below VWAP − PRICE_BOUND_TICKS (11 000 below) → pricePosition = 0
|
||||
mockPool.setCurrentTick(-20_000); // 20 000 ticks below vwap
|
||||
mockPool.setRevertOnObserve(true);
|
||||
|
||||
optimizer.getLiquidityParams(); // must not revert
|
||||
}
|
||||
|
||||
function testPricePositionAboveUpperBound() public {
|
||||
_configureSources(false);
|
||||
|
||||
int24 vwapTick = 0;
|
||||
_seedVwapAtTick(vwapTick);
|
||||
// Current tick is far above VWAP + PRICE_BOUND_TICKS → pricePosition = 1e18
|
||||
mockPool.setCurrentTick(20_000);
|
||||
mockPool.setRevertOnObserve(true);
|
||||
|
||||
optimizer.getLiquidityParams(); // must not revert
|
||||
}
|
||||
|
||||
function testPricePositionToken0IsWethFlipsSign() public {
|
||||
// With token0isWeth=true, adjTick = poolTick (no negation).
|
||||
// If poolTick=500 → adjTick=500 = vwapTick → pricePosition ≈ 0.5e18.
|
||||
// Verifies that token0isWeth=true does NOT negate the pool tick.
|
||||
_configureSources(true); // token0=WETH
|
||||
|
||||
int24 vwapAdjTick = 500;
|
||||
_seedVwapAtTick(vwapAdjTick); // VWAP at adjTick=500
|
||||
|
||||
// Pool tick = 500 → adjTick = 500 = vwapTick → pricePosition ≈ 0.5e18
|
||||
mockPool.setCurrentTick(500);
|
||||
mockPool.setRevertOnObserve(true);
|
||||
|
||||
optimizer.getLiquidityParams(); // must not revert
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Slots 3-4: volatility and momentum
|
||||
// =========================================================
|
||||
|
||||
function testVolatilityZeroWhenFlatMarket() public {
|
||||
_configureSources(false);
|
||||
_seedVwapAtTick(0);
|
||||
mockPool.setCurrentTick(0);
|
||||
// shortTwap == longTwap → volatility = 0, momentum = 0.5e18
|
||||
mockPool.setTwapTicks(100, 100);
|
||||
|
||||
optimizer.getLiquidityParams(); // must not revert
|
||||
}
|
||||
|
||||
function testMomentumFullBullAtMaxDelta() public {
|
||||
_configureSources(false);
|
||||
_seedVwapAtTick(0);
|
||||
mockPool.setCurrentTick(0);
|
||||
// shortTwap - longTwap = 1000 ticks = MAX_MOMENTUM_TICKS → momentum = 1e18
|
||||
mockPool.setTwapTicks(0, 1_000); // longTwap=0, shortTwap=1000
|
||||
|
||||
optimizer.getLiquidityParams(); // must not revert
|
||||
}
|
||||
|
||||
function testMomentumFullBearAtNegMaxDelta() public {
|
||||
_configureSources(false);
|
||||
_seedVwapAtTick(0);
|
||||
mockPool.setCurrentTick(0);
|
||||
// shortTwap - longTwap = -1000 = -MAX_MOMENTUM_TICKS → momentum = 0
|
||||
mockPool.setTwapTicks(1_000, 0); // longTwap=1000, shortTwap=0
|
||||
|
||||
optimizer.getLiquidityParams(); // must not revert
|
||||
}
|
||||
|
||||
function testObserveRevertLeavesSlots34AsZero() public {
|
||||
_configureSources(false);
|
||||
_seedVwapAtTick(0);
|
||||
mockPool.setCurrentTick(0);
|
||||
mockPool.setRevertOnObserve(true); // triggers catch branch
|
||||
|
||||
// Must not revert — slots 3-4 remain 0 (calculateParams ignores them anyway)
|
||||
optimizer.getLiquidityParams();
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Slot 6: utilizationRate
|
||||
// =========================================================
|
||||
|
||||
function testUtilizationRateInRange() public {
|
||||
_configureSources(false);
|
||||
// Set anchor position in range [−100, 100]; current tick = 0 → in range → 1e18
|
||||
mockLm.setPosition(ANCHOR, 1e18, -100, 100);
|
||||
mockPool.setCurrentTick(0);
|
||||
mockPool.setRevertOnObserve(true);
|
||||
|
||||
optimizer.getLiquidityParams(); // must not revert
|
||||
}
|
||||
|
||||
function testUtilizationRateOutOfRange() public {
|
||||
_configureSources(false);
|
||||
// Anchor range [−100, 100]; current tick = 500 → out of range → 0
|
||||
mockLm.setPosition(ANCHOR, 1e18, -100, 100);
|
||||
mockPool.setCurrentTick(500);
|
||||
mockPool.setRevertOnObserve(true);
|
||||
|
||||
optimizer.getLiquidityParams(); // must not revert
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Data-source disabled: slots remain 0, no revert
|
||||
// =========================================================
|
||||
|
||||
function testNoDataSourcesNoRevert() public {
|
||||
// No sources configured — only slots 0,1 are set; rest are 0
|
||||
optimizer.getLiquidityParams();
|
||||
}
|
||||
|
||||
function testPoolOnlyNoVwapNoRevert() public {
|
||||
optimizer.setDataSources(address(0), address(mockPool), address(mockLm), false);
|
||||
mockPool.setCurrentTick(0);
|
||||
optimizer.getLiquidityParams(); // slots 2-4 remain 0 (no VWAP), slot 6 computed
|
||||
}
|
||||
|
||||
function testVwapOnlyNoPoolNoRevert() public {
|
||||
optimizer.setDataSources(address(mockVwap), address(0), address(0), false);
|
||||
_seedVwapAtTick(0);
|
||||
optimizer.getLiquidityParams(); // pool-dependent slots remain 0
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Fuzz: normalized outputs are always in [0, 1e18]
|
||||
// =========================================================
|
||||
|
||||
function testFuzzPricePositionInRange(int24 currentTick, int24 vwapTick) public {
|
||||
// Bound to ticks where priceX96 * 100e18 (VWAPTracker volume-weight) stays < uint256 max.
|
||||
// At tick 500 000: sqrtRatio ≈ 8.5e40, priceX96 ≈ 9e52, volume = 1e20 → product ≈ 9e72 < 1.16e77 ✓
|
||||
// Margin is ~4 orders of magnitude below overflow.
|
||||
int24 SAFE_MAX = 500_000;
|
||||
currentTick = int24(bound(int256(currentTick), -SAFE_MAX, SAFE_MAX));
|
||||
vwapTick = int24(bound(int256(vwapTick), -SAFE_MAX, SAFE_MAX));
|
||||
|
||||
_configureSources(false);
|
||||
_seedVwapAtTick(vwapTick);
|
||||
mockPool.setCurrentTick(currentTick);
|
||||
mockPool.setRevertOnObserve(true);
|
||||
|
||||
// getLiquidityParams must not revert regardless of tick values
|
||||
optimizer.getLiquidityParams();
|
||||
}
|
||||
}
|
||||
|
||||
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
||||
|
|
|
|||
27
onchain/test/mocks/MockLiquidityManagerPositions.sol
Normal file
27
onchain/test/mocks/MockLiquidityManagerPositions.sol
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
/**
|
||||
* @title MockLiquidityManagerPositions
|
||||
* @notice Mock LiquidityManager for testing utilizationRate (slot 6) normalization in Optimizer.
|
||||
* @dev Exposes positions(uint8) matching ThreePositionStrategy's public mapping getter.
|
||||
* Stage enum: FLOOR=0, ANCHOR=1, DISCOVERY=2.
|
||||
*/
|
||||
contract MockLiquidityManagerPositions {
|
||||
struct TokenPosition {
|
||||
uint128 liquidity;
|
||||
int24 tickLower;
|
||||
int24 tickUpper;
|
||||
}
|
||||
|
||||
mapping(uint8 => TokenPosition) private _positions;
|
||||
|
||||
function setPosition(uint8 stage, uint128 liquidity, int24 tickLower, int24 tickUpper) external {
|
||||
_positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper });
|
||||
}
|
||||
|
||||
function positions(uint8 stage) external view returns (uint128 liquidity, int24 tickLower, int24 tickUpper) {
|
||||
TokenPosition storage p = _positions[stage];
|
||||
return (p.liquidity, p.tickLower, p.tickUpper);
|
||||
}
|
||||
}
|
||||
76
onchain/test/mocks/MockPool.sol
Normal file
76
onchain/test/mocks/MockPool.sol
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
/**
|
||||
* @title MockPool
|
||||
* @notice Mock Uniswap V3 pool for testing normalized indicator computation in Optimizer.
|
||||
* @dev Implements slot0() and observe() with configurable return values.
|
||||
*
|
||||
* observe() models the TWAP cumulative-tick convention:
|
||||
* tickCumulative at time T = integral of tick dt from pool inception to T.
|
||||
* We set tickCumulative[now] = 0 and back-fill:
|
||||
* tickCumulative[longWindow ago] = -longTwapTick * LONG_WINDOW
|
||||
* tickCumulative[shortWindow ago] = -shortTwapTick * SHORT_WINDOW
|
||||
* This means:
|
||||
* longTwapTick = (cum[now] - cum[longAgo]) / LONG_WINDOW = longTwapTick ✓
|
||||
* shortTwapTick = (cum[now] - cum[shortAgo]) / SHORT_WINDOW = shortTwapTick ✓
|
||||
*/
|
||||
contract MockPool {
|
||||
int24 public currentTick;
|
||||
int24 public longTwapTick;
|
||||
int24 public shortTwapTick;
|
||||
bool public revertOnObserve;
|
||||
|
||||
uint32 internal constant LONG_WINDOW = 1_800;
|
||||
uint32 internal constant SHORT_WINDOW = 300;
|
||||
|
||||
function setCurrentTick(int24 _tick) external {
|
||||
currentTick = _tick;
|
||||
}
|
||||
|
||||
function setTwapTicks(int24 _longTwap, int24 _shortTwap) external {
|
||||
longTwapTick = _longTwap;
|
||||
shortTwapTick = _shortTwap;
|
||||
}
|
||||
|
||||
function setRevertOnObserve(bool _revert) external {
|
||||
revertOnObserve = _revert;
|
||||
}
|
||||
|
||||
function slot0()
|
||||
external
|
||||
view
|
||||
returns (
|
||||
uint160 sqrtPriceX96,
|
||||
int24 tick,
|
||||
uint16 observationIndex,
|
||||
uint16 observationCardinality,
|
||||
uint16 observationCardinalityNext,
|
||||
uint8 feeProtocol,
|
||||
bool unlocked
|
||||
)
|
||||
{
|
||||
return (0, currentTick, 0, 100, 100, 0, true);
|
||||
}
|
||||
|
||||
/// @notice Returns tick cumulatives for the three time points [LONG_WINDOW, SHORT_WINDOW, 0].
|
||||
/// @dev Only handles the exact 3-element call from Optimizer.getLiquidityParams().
|
||||
function observe(uint32[] calldata secondsAgos)
|
||||
external
|
||||
view
|
||||
returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s)
|
||||
{
|
||||
require(!revertOnObserve, "MockPool: observe reverts");
|
||||
require(secondsAgos.length == 3, "MockPool: expected 3 time points");
|
||||
|
||||
tickCumulatives = new int56[](3);
|
||||
secondsPerLiquidityCumulativeX128s = new uint160[](3);
|
||||
|
||||
// cum[now] = 0
|
||||
tickCumulatives[2] = 0;
|
||||
// cum[longAgo] = -longTwapTick * LONG_WINDOW
|
||||
tickCumulatives[0] = int56(-int256(longTwapTick)) * int56(int32(LONG_WINDOW));
|
||||
// cum[shortAgo] = -shortTwapTick * SHORT_WINDOW
|
||||
tickCumulatives[1] = int56(-int256(shortTwapTick)) * int56(int32(SHORT_WINDOW));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue