harb/onchain/src/Optimizer.sol

536 lines
26 KiB
Solidity
Raw Normal View History

// 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).
// ---------------------------------------------------------------------------
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>
2026-03-13 07:53:46 +01:00
// Minimal interface for VWAPTracker (slots 2-4 computation)
interface IVWAPTracker {
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>
2026-03-13 07:53:46 +01:00
function getVWAP() external view returns (uint256);
}
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>
2026-03-13 07:53:46 +01:00
// 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
);
}
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>
2026-03-13 07:53:46 +01:00
// 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).
*
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>
2026-03-13 07:53:46 +01:00
* 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;
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>
2026-03-13 07:53:46 +01:00
// ---- 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)
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>
2026-03-13 07:53:46 +01:00
// ---- 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;
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>
2026-03-13 07:53:46 +01:00
/// @notice Maximum tick trend signal (shortTwap - longTwap) for momentum saturation.
/// 1 000 ticks ≈ 10% price trend.
int256 internal constant MAX_MOMENTUM_TICKS = 1000;
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>
2026-03-13 07:53:46 +01:00
/// @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 500508 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) ----
/**
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>
2026-03-13 07:53:46 +01:00
* @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;
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>
2026-03-13 07:53:46 +01:00
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);
}
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>
2026-03-13 07:53:46 +01:00
// ---- 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) {
2025-09-16 22:46:43 +02:00
// 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.
2025-09-16 22:46:43 +02:00
// 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);
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>
2026-03-13 07:53:46 +01:00
* 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.
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>
2026-03-13 07:53:46 +01:00
* 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);
2025-09-16 22:46:43 +02:00
uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked);
2025-09-16 22:46:43 +02:00
if (sentiment > 1e18) {
sentiment = 1e18;
}
capitalInefficiency = 1e18 - sentiment;
anchorShare = sentiment;
anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate);
discoveryDepth = sentiment;
}
/**
* @notice Returns liquidity parameters for the LiquidityManager.
*
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>
2026-03-13 07:53:46 +01:00
* @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.
*
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>
2026-03-13 07:53:46 +01:00
* 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()));
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>
2026-03-13 07:53:46 +01:00
// Slots 2-4 (pricePosition, volatility, momentum) and slot 6 (utilizationRate)
// all require the pool address. Read slot0 once and reuse.
if (pool != address(0)) {
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>
2026-03-13 07:53:46 +01:00
(, 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)));
}
}
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>
2026-03-13 07:53:46 +01:00
// ---- 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
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>
2026-03-13 07:53:46 +01:00
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)));
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>
2026-03-13 07:53:46 +01:00
// 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;
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>
2026-03-13 07:53:46 +01:00
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));
}
}
}
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>
2026-03-13 07:53:46 +01:00
// Slot 5: timeSinceRecenter normalized to [0, 1e18].
// 0 = just recentered, 1e18 = MAX_STALE_SECONDS or more have elapsed.
if (lastRecenterTimestamp > 0) {
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>
2026-03-13 07:53:46 +01:00
uint256 elapsed = block.timestamp - lastRecenterTimestamp;
uint256 normalized = elapsed >= MAX_STALE_SECONDS ? 1e18 : elapsed * 1e18 / MAX_STALE_SECONDS;
inputs[5] = _toDyadic(int256(normalized));
}
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>
2026-03-13 07:53:46 +01:00
// 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;
}
}