2025-02-01 21:49:15 +01:00
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
pragma solidity ^0.8.19;
|
|
|
|
|
|
|
2026-03-19 18:22:43 +00:00
|
|
|
|
import { IOptimizer, OptimizerInput, BEAR_CAPITAL_INEFFICIENCY, BEAR_ANCHOR_SHARE, BEAR_ANCHOR_WIDTH, BEAR_DISCOVERY_DEPTH } from "./IOptimizer.sol";
|
2026-03-19 13:57:25 +00:00
|
|
|
|
import { Kraiken } from "./Kraiken.sol";
|
|
|
|
|
|
import { Stake } from "./Stake.sol";
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2026-03-19 13:57:25 +00:00
|
|
|
|
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";
|
2026-03-10 23:13:57 +00:00
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 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)
|
2026-03-10 23:13:57 +00:00
|
|
|
|
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);
|
2026-03-10 23:13:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
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)
|
2026-03-10 23:13:57 +00:00
|
|
|
|
interface IUniswapV3PoolSlot0 {
|
|
|
|
|
|
function slot0()
|
|
|
|
|
|
external
|
|
|
|
|
|
view
|
|
|
|
|
|
returns (
|
|
|
|
|
|
uint160 sqrtPriceX96,
|
|
|
|
|
|
int24 tick,
|
|
|
|
|
|
uint16 observationIndex,
|
|
|
|
|
|
uint16 observationCardinality,
|
|
|
|
|
|
uint16 observationCardinalityNext,
|
|
|
|
|
|
uint8 feeProtocol,
|
|
|
|
|
|
bool unlocked
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-02-01 21:49:15 +01:00
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-01 21:49:15 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* @title Optimizer
|
2026-03-10 23:13:57 +00:00
|
|
|
|
* @notice Calculates liquidity parameters for the LiquidityManager using an
|
|
|
|
|
|
* 8-slot dyadic rational input interface (Push3's native format).
|
2025-10-04 15:17:09 +02:00
|
|
|
|
*
|
2026-03-10 23:13:57 +00:00
|
|
|
|
* @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).
|
2025-10-04 15:17:09 +02:00
|
|
|
|
*
|
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.
|
2025-10-04 15:17:09 +02:00
|
|
|
|
*
|
2026-03-10 23:13:57 +00:00
|
|
|
|
* 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
|
2025-02-01 21:49:15 +01:00
|
|
|
|
*/
|
2026-03-14 05:08:32 +00:00
|
|
|
|
contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
|
2025-08-19 11:05:08 +02:00
|
|
|
|
Kraiken private kraiken;
|
2025-02-01 21:49:15 +01:00
|
|
|
|
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 ----
|
2026-03-10 23:13:57 +00:00
|
|
|
|
// These are optional; unset addresses leave the corresponding slots as 0.
|
2026-03-19 13:57:25 +00:00
|
|
|
|
address public vwapTracker; // slots 2-4 source (VWAPTracker)
|
|
|
|
|
|
address public pool; // slots 2-4, 6 source (Uniswap V3 pool)
|
2026-03-10 23:13:57 +00:00
|
|
|
|
uint256 public lastRecenterTimestamp; // slot 5 source (updated via recordRecenter)
|
2026-03-19 13:57:25 +00:00
|
|
|
|
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.
|
2026-03-19 13:57:25 +00:00
|
|
|
|
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.
|
2026-03-19 13:57:25 +00:00
|
|
|
|
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).
|
2026-03-19 13:57:25 +00:00
|
|
|
|
uint32 internal constant LONG_TWAP_WINDOW = 1800;
|
2026-03-10 23:13:57 +00:00
|
|
|
|
|
2025-02-01 21:49:15 +01:00
|
|
|
|
/// @dev Reverts if the caller is not the admin.
|
|
|
|
|
|
error UnauthorizedAccount(address account);
|
|
|
|
|
|
|
2026-03-13 01:05:37 +00:00
|
|
|
|
/// @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.
|
2026-03-15 00:28:12 +00:00
|
|
|
|
/// 500 000 gives ~33x headroom over the current seed (~15 k gas) while
|
2026-03-13 01:05:37 +00:00
|
|
|
|
/// preventing unbounded growth from blocking recenter().
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Note (EIP-150 / 63-64 rule): the outer getLiquidityParams() call must
|
2026-03-15 00:00:56 +00:00
|
|
|
|
/// arrive with at least ⌈500_000 × 64/63⌉ ≈ 507_937 gas for the inner
|
|
|
|
|
|
/// staticcall to actually receive 500 000. Callers with exactly 500–508 k
|
2026-03-13 01:05:37 +00:00
|
|
|
|
/// gas will see a spurious bear-defaults fallback. This is not a practical
|
|
|
|
|
|
/// concern from recenter(), which always has abundant gas.
|
2026-03-15 00:00:56 +00:00
|
|
|
|
uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 500_000;
|
2026-03-13 01:05:37 +00:00
|
|
|
|
|
2025-02-01 21:49:15 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Initialize the Optimizer.
|
2025-08-19 11:05:08 +02:00
|
|
|
|
* @param _kraiken The address of the Kraiken token.
|
2025-02-01 21:49:15 +01:00
|
|
|
|
* @param _stake The address of the Stake contract.
|
|
|
|
|
|
*/
|
2025-08-19 11:05:08 +02:00
|
|
|
|
function initialize(address _kraiken, address _stake) public initializer {
|
2025-02-01 21:49:15 +01:00
|
|
|
|
// Set the admin for upgradeability (using ERC1967Upgrade _changeAdmin)
|
|
|
|
|
|
_changeAdmin(msg.sender);
|
2025-08-19 11:05:08 +02:00
|
|
|
|
kraiken = Kraiken(_kraiken);
|
2025-02-01 21:49:15 +01:00
|
|
|
|
stake = Stake(_stake);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modifier onlyAdmin() {
|
|
|
|
|
|
_checkAdmin();
|
|
|
|
|
|
_;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _checkAdmin() internal view virtual {
|
|
|
|
|
|
if (_getAdmin() != msg.sender) {
|
|
|
|
|
|
revert UnauthorizedAccount(msg.sender);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 13:57:25 +00:00
|
|
|
|
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { }
|
2026-03-10 23:13:57 +00:00
|
|
|
|
|
|
|
|
|
|
// ---- 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).
|
2026-03-10 23:13:57 +00:00
|
|
|
|
*/
|
2026-03-19 13:57:25 +00:00
|
|
|
|
function setDataSources(address _vwapTracker, address _pool, address _liquidityManager, bool _token0isWeth) external onlyAdmin {
|
2026-03-10 23:13:57 +00:00
|
|
|
|
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;
|
2026-03-10 23:13:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @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) {
|
2026-03-19 13:57:25 +00:00
|
|
|
|
return OptimizerInput({ mantissa: value, shift: 0 });
|
2026-03-10 23:13:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 00:25:49 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Safe bear-mode defaults returned when calculateParams exceeds its
|
2026-03-13 01:05:37 +00:00
|
|
|
|
* gas budget or reverts.
|
2026-03-19 18:22:43 +00:00
|
|
|
|
* @dev Constants defined in IOptimizer.sol, shared with LiquidityManager.recenter().
|
2026-03-13 00:25:49 +00:00
|
|
|
|
*/
|
2026-03-19 13:57:25 +00:00
|
|
|
|
function _bearDefaults() internal pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
|
2026-03-19 18:22:43 +00:00
|
|
|
|
return (BEAR_CAPITAL_INEFFICIENCY, BEAR_ANCHOR_SHARE, BEAR_ANCHOR_WIDTH, BEAR_DISCOVERY_DEPTH);
|
2026-03-13 00:25:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
|
// ---- Core computation ----
|
2025-02-01 21:49:15 +01:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @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.
|
|
|
|
|
|
*/
|
2026-03-19 13:57:25 +00:00
|
|
|
|
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
|
2025-02-01 21:49:15 +01:00
|
|
|
|
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;
|
2025-02-01 21:49:15 +01:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @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)
|
|
|
|
|
|
*/
|
2025-10-04 15:17:09 +02:00
|
|
|
|
function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) internal pure returns (uint24) {
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Base width: 40% is our neutral starting point
|
|
|
|
|
|
int256 baseWidth = 40;
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Staking adjustment: -20% to +20% based on staking percentage
|
|
|
|
|
|
int256 stakingAdjustment = 20 - int256(percentageStaked * 40 / 1e18);
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Tax rate adjustment: -10% to +30% based on average tax rate
|
|
|
|
|
|
int256 taxAdjustment = int256(averageTaxRate * 40 / 1e18) - 10;
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Combine all adjustments
|
|
|
|
|
|
int256 totalWidth = baseWidth + stakingAdjustment + taxAdjustment;
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Clamp to safe bounds (10 to 80)
|
|
|
|
|
|
if (totalWidth < 10) {
|
|
|
|
|
|
return 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (totalWidth > 80) {
|
|
|
|
|
|
return 80;
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
return uint24(uint256(totalWidth));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-01 21:49:15 +01:00
|
|
|
|
/**
|
2026-03-10 23:13:57 +00:00
|
|
|
|
* @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.
|
2025-10-04 15:17:09 +02:00
|
|
|
|
*
|
2026-03-10 23:13:57 +00:00
|
|
|
|
* @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)
|
2025-10-04 15:17:09 +02:00
|
|
|
|
*
|
2026-03-10 23:13:57 +00:00
|
|
|
|
* @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).
|
2025-02-01 21:49:15 +01:00
|
|
|
|
*/
|
2026-03-10 23:13:57 +00:00
|
|
|
|
function calculateParams(OptimizerInput[8] memory inputs)
|
|
|
|
|
|
public
|
|
|
|
|
|
pure
|
2026-03-12 06:47:35 +00:00
|
|
|
|
virtual
|
2026-03-10 23:13:57 +00:00
|
|
|
|
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
|
|
|
|
|
{
|
2026-03-20 11:42:50 +00:00
|
|
|
|
// Guard against non-zero shift and negative mantissa.
|
|
|
|
|
|
// shift is reserved for future use; uint256() cast silently wraps negatives.
|
2026-03-18 18:24:18 +00:00
|
|
|
|
for (uint256 k; k < 8; k++) {
|
2026-03-20 11:42:50 +00:00
|
|
|
|
require(inputs[k].shift == 0, "shift not yet supported");
|
2026-03-18 18:24:18 +00:00
|
|
|
|
require(inputs[k].mantissa >= 0, "negative mantissa");
|
|
|
|
|
|
}
|
2026-03-10 23:13:57 +00:00
|
|
|
|
// 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
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
|
uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
if (sentiment > 1e18) {
|
|
|
|
|
|
sentiment = 1e18;
|
|
|
|
|
|
}
|
2026-03-10 23:13:57 +00:00
|
|
|
|
|
2025-02-01 21:49:15 +01:00
|
|
|
|
capitalInefficiency = 1e18 - sentiment;
|
|
|
|
|
|
anchorShare = sentiment;
|
2025-08-19 11:41:02 +02:00
|
|
|
|
anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate);
|
2025-02-01 21:49:15 +01:00
|
|
|
|
discoveryDepth = sentiment;
|
|
|
|
|
|
}
|
2026-03-10 23:13:57 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @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.
|
2026-03-10 23:13:57 +00:00
|
|
|
|
*
|
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
|
2026-03-10 23:13:57 +00:00
|
|
|
|
*
|
|
|
|
|
|
*/
|
2026-03-19 13:57:25 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @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) {
|
2026-03-10 23:13:57 +00:00
|
|
|
|
// 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.
|
2026-03-10 23:13:57 +00:00
|
|
|
|
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)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 23:13:57 +00:00
|
|
|
|
|
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);
|
2026-03-19 13:57:25 +00:00
|
|
|
|
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
|
2026-03-19 13:57:25 +00:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-03-19 13:57:25 +00:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 23:13:57 +00:00
|
|
|
|
|
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.
|
2026-03-10 23:13:57 +00:00
|
|
|
|
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));
|
2026-03-10 23:13:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
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)
|
2026-03-19 13:57:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 17:08:26 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @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)
|
|
|
|
|
|
*/
|
2026-03-19 13:57:25 +00:00
|
|
|
|
function getLiquidityParams()
|
|
|
|
|
|
external
|
|
|
|
|
|
view
|
|
|
|
|
|
override
|
|
|
|
|
|
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
|
|
|
|
|
{
|
|
|
|
|
|
OptimizerInput[8] memory inputs = _buildInputs();
|
2026-03-10 23:13:57 +00:00
|
|
|
|
|
2026-03-13 00:25:49 +00:00
|
|
|
|
// 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.
|
2026-03-19 13:57:25 +00:00
|
|
|
|
(bool ok, bytes memory ret) = address(this).staticcall{ gas: CALCULATE_PARAMS_GAS_LIMIT }(abi.encodeCall(this.calculateParams, (inputs)));
|
2026-03-13 00:25:49 +00:00
|
|
|
|
if (!ok) return _bearDefaults();
|
2026-03-13 01:05:37 +00:00
|
|
|
|
// 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();
|
2026-03-19 13:57:25 +00:00
|
|
|
|
(capitalInefficiency, anchorShare, anchorWidth, discoveryDepth) = abi.decode(ret, (uint256, uint256, uint24, uint256));
|
2026-03-13 03:47:49 +00:00
|
|
|
|
// 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;
|
2026-03-10 23:13:57 +00:00
|
|
|
|
}
|
2025-02-01 21:49:15 +01:00
|
|
|
|
}
|