2025-08-19 11:41:02 +02:00
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
pragma solidity ^0.8.19;
|
|
|
|
|
|
|
|
|
|
|
|
import "../src/Optimizer.sol";
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
import "./mocks/MockKraiken.sol";
|
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
|
|
|
|
import "./mocks/MockLiquidityManagerPositions.sol";
|
|
|
|
|
|
import "./mocks/MockPool.sol";
|
2025-10-04 15:17:09 +02:00
|
|
|
|
import "./mocks/MockStake.sol";
|
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
|
|
|
|
import "./mocks/MockVWAPTracker.sol";
|
fix: Backtesting #5: Position tracking + P&L metrics (#319)
- Add PositionTracker.sol: tracks position lifecycle (open/close per
recenter), records tick ranges, liquidity, entry/exit blocks/timestamps,
token amounts (via LiquidityAmounts math), fees (proportional to
liquidity share), IL (LP exit value − HODL value at exit price), and
net P&L per position. Aggregates total fees, cumulative IL, net P&L,
rebalance count, Anchor time-in-range, and capital efficiency accumulators.
Logs with [TRACKER][TYPE] prefix; emits cumulative P&L every 500 blocks.
- Modify StrategyExecutor.sol: add IUniswapV3Pool + token0isWeth to
constructor (creates PositionTracker internally), call
tracker.notifyBlock() on every block for time-in-range, and call
tracker.recordRecenter() on each successful recenter. logSummary()
now delegates to tracker.logFinalSummary().
- Modify BacktestRunner.s.sol: pass sp.pool and token0isWeth to
StrategyExecutor constructor; log tracker address.
- forge fmt: reformat all backtesting scripts and affected src/test files
to project style (number_underscore=thousands, multiline_func_header=all).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:23:18 +00:00
|
|
|
|
|
|
|
|
|
|
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
|
2025-10-04 15:17:09 +02:00
|
|
|
|
import "forge-std/Test.sol";
|
|
|
|
|
|
import "forge-std/console.sol";
|
2026-02-26 02:41:02 +00:00
|
|
|
|
|
|
|
|
|
|
/// @dev Harness to expose internal _calculateAnchorWidth for direct coverage of the totalWidth < 10 path
|
|
|
|
|
|
contract OptimizerHarness is Optimizer {
|
|
|
|
|
|
function exposed_calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) external pure returns (uint24) {
|
|
|
|
|
|
return _calculateAnchorWidth(percentageStaked, averageTaxRate);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-19 11:41:02 +02:00
|
|
|
|
|
|
|
|
|
|
contract OptimizerTest is Test {
|
|
|
|
|
|
Optimizer optimizer;
|
|
|
|
|
|
MockStake mockStake;
|
|
|
|
|
|
MockKraiken mockKraiken;
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
function setUp() public {
|
|
|
|
|
|
// Deploy mocks
|
|
|
|
|
|
mockKraiken = new MockKraiken();
|
|
|
|
|
|
mockStake = new MockStake();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Deploy Optimizer implementation
|
|
|
|
|
|
Optimizer implementation = new Optimizer();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Deploy proxy and initialize
|
2025-10-04 15:17:09 +02:00
|
|
|
|
bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
|
|
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// For simplicity, we'll test the implementation directly
|
|
|
|
|
|
// In production, you'd use a proper proxy setup
|
|
|
|
|
|
optimizer = implementation;
|
|
|
|
|
|
optimizer.initialize(address(mockKraiken), address(mockStake));
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test that anchorWidth adjusts correctly for bull market conditions
|
|
|
|
|
|
* @dev High staking, low tax → narrow anchor (30-35%)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testBullMarketAnchorWidth() public {
|
|
|
|
|
|
// Set bull market conditions: high staking (80%), low tax (10%)
|
|
|
|
|
|
mockStake.setPercentageStaked(0.8e18);
|
|
|
|
|
|
mockStake.setAverageTaxRate(0.1e18);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 32 = -12) + tax_adj(4 - 10 = -6) = 22
|
|
|
|
|
|
assertEq(anchorWidth, 22, "Bull market should have narrow anchor width");
|
|
|
|
|
|
assertTrue(anchorWidth >= 20 && anchorWidth <= 35, "Bull market width should be 20-35%");
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test that anchorWidth adjusts correctly for bear market conditions
|
|
|
|
|
|
* @dev Low staking, high tax → wide anchor (60-80%)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testBearMarketAnchorWidth() public {
|
|
|
|
|
|
// Set bear market conditions: low staking (20%), high tax (70%)
|
|
|
|
|
|
mockStake.setPercentageStaked(0.2e18);
|
|
|
|
|
|
mockStake.setAverageTaxRate(0.7e18);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 8 = 12) + tax_adj(28 - 10 = 18) = 70
|
|
|
|
|
|
assertEq(anchorWidth, 70, "Bear market should have wide anchor width");
|
|
|
|
|
|
assertTrue(anchorWidth >= 60 && anchorWidth <= 80, "Bear market width should be 60-80%");
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test neutral market conditions
|
|
|
|
|
|
* @dev Medium staking, medium tax → balanced anchor (35-50%)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testNeutralMarketAnchorWidth() public {
|
|
|
|
|
|
// Set neutral conditions: medium staking (50%), medium tax (30%)
|
|
|
|
|
|
mockStake.setPercentageStaked(0.5e18);
|
|
|
|
|
|
mockStake.setAverageTaxRate(0.3e18);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(12 - 10 = 2) = 42
|
|
|
|
|
|
assertEq(anchorWidth, 42, "Neutral market should have balanced anchor width");
|
|
|
|
|
|
assertTrue(anchorWidth >= 35 && anchorWidth <= 50, "Neutral width should be 35-50%");
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test high volatility scenario
|
|
|
|
|
|
* @dev High staking with high tax (speculative frenzy) → moderate-wide anchor
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testHighVolatilityAnchorWidth() public {
|
|
|
|
|
|
// High staking (70%) but also high tax (80%) - speculative market
|
|
|
|
|
|
mockStake.setPercentageStaked(0.7e18);
|
|
|
|
|
|
mockStake.setAverageTaxRate(0.8e18);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 28 = -8) + tax_adj(32 - 10 = 22) = 54
|
|
|
|
|
|
assertEq(anchorWidth, 54, "High volatility should have moderate-wide anchor");
|
|
|
|
|
|
assertTrue(anchorWidth >= 50 && anchorWidth <= 60, "Volatile width should be 50-60%");
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test stable market conditions
|
|
|
|
|
|
* @dev Medium staking with very low tax → narrow anchor for fee optimization
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testStableMarketAnchorWidth() public {
|
|
|
|
|
|
// Medium staking (50%), very low tax (5%) - stable conditions
|
|
|
|
|
|
mockStake.setPercentageStaked(0.5e18);
|
|
|
|
|
|
mockStake.setAverageTaxRate(0.05e18);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(2 - 10 = -8) = 32
|
|
|
|
|
|
assertEq(anchorWidth, 32, "Stable market should have narrower anchor");
|
|
|
|
|
|
assertTrue(anchorWidth >= 30 && anchorWidth <= 40, "Stable width should be 30-40%");
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test minimum bound enforcement
|
|
|
|
|
|
* @dev Extreme conditions that would result in width < 10 should clamp to 10
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testMinimumWidthBound() public {
|
|
|
|
|
|
// Extreme bull: very high staking (95%), zero tax
|
|
|
|
|
|
mockStake.setPercentageStaked(0.95e18);
|
|
|
|
|
|
mockStake.setAverageTaxRate(0);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 38 = -18) + tax_adj(0 - 10 = -10) = 12
|
|
|
|
|
|
// But should be at least 10
|
|
|
|
|
|
assertEq(anchorWidth, 12, "Should not go below calculated value if above 10");
|
|
|
|
|
|
assertTrue(anchorWidth >= 10, "Width should never be less than 10");
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test maximum bound enforcement
|
|
|
|
|
|
* @dev Extreme conditions that would result in width > 80 should clamp to 80
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testMaximumWidthBound() public {
|
|
|
|
|
|
// Extreme bear: zero staking, maximum tax
|
|
|
|
|
|
mockStake.setPercentageStaked(0);
|
|
|
|
|
|
mockStake.setAverageTaxRate(1e18);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(40 - 10 = 30) = 90
|
|
|
|
|
|
// But should be clamped to 80
|
|
|
|
|
|
assertEq(anchorWidth, 80, "Should clamp to maximum of 80");
|
|
|
|
|
|
assertTrue(anchorWidth <= 80, "Width should never exceed 80");
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test edge case with exactly minimum staking and tax
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testEdgeCaseMinimumInputs() public {
|
|
|
|
|
|
mockStake.setPercentageStaked(0);
|
|
|
|
|
|
mockStake.setAverageTaxRate(0);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(0 - 10 = -10) = 50
|
|
|
|
|
|
assertEq(anchorWidth, 50, "Zero inputs should give moderate width");
|
|
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test edge case with exactly maximum staking and tax
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testEdgeCaseMaximumInputs() public {
|
|
|
|
|
|
mockStake.setPercentageStaked(1e18);
|
|
|
|
|
|
mockStake.setAverageTaxRate(1e18);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Expected: base(40) + staking_adj(20 - 40 = -20) + tax_adj(40 - 10 = 30) = 50
|
|
|
|
|
|
assertEq(anchorWidth, 50, "Maximum inputs should balance out to moderate width");
|
|
|
|
|
|
}
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test edge case with high staking and high tax rate
|
|
|
|
|
|
* @dev This specific case previously caused an overflow
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testHighStakingHighTaxEdgeCase() public {
|
|
|
|
|
|
// Set conditions that previously caused overflow
|
|
|
|
|
|
// ~94.6% staked, ~96.7% tax rate
|
2025-10-04 15:17:09 +02:00
|
|
|
|
mockStake.setPercentageStaked(946_350_908_835_331_692);
|
|
|
|
|
|
mockStake.setAverageTaxRate(966_925_542_613_630_263);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
|
|
|
|
|
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams();
|
|
|
|
|
|
|
|
|
|
|
|
// With very high staking (>92%) and high tax, sentiment reaches maximum (1e18)
|
|
|
|
|
|
// This results in zero capital inefficiency
|
|
|
|
|
|
assertEq(capitalInefficiency, 0, "Max sentiment should result in zero capital inefficiency");
|
|
|
|
|
|
|
|
|
|
|
|
// Anchor share should be at maximum
|
|
|
|
|
|
assertEq(anchorShare, 1e18, "Max sentiment should result in maximum anchor share");
|
|
|
|
|
|
|
|
|
|
|
|
// Anchor width should still be within bounds
|
|
|
|
|
|
assertTrue(anchorWidth >= 10 && anchorWidth <= 80, "Anchor width should be within bounds");
|
|
|
|
|
|
|
|
|
|
|
|
// Expected: base(40) + staking_adj(20 - 37 = -17) + tax_adj(38 - 10 = 28) = 51
|
|
|
|
|
|
assertEq(anchorWidth, 51, "Should calculate correct width for edge case");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Fuzz test to ensure anchorWidth always stays within bounds
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testFuzzAnchorWidthBounds(uint256 percentageStaked, uint256 averageTaxRate) public {
|
|
|
|
|
|
// Bound inputs to valid ranges
|
|
|
|
|
|
percentageStaked = bound(percentageStaked, 0, 1e18);
|
|
|
|
|
|
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
mockStake.setPercentageStaked(percentageStaked);
|
|
|
|
|
|
mockStake.setAverageTaxRate(averageTaxRate);
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Assert bounds are always respected
|
|
|
|
|
|
assertTrue(anchorWidth >= 10, "Width should never be less than 10");
|
|
|
|
|
|
assertTrue(anchorWidth <= 80, "Width should never exceed 80");
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-09-16 22:46:43 +02:00
|
|
|
|
// Edge cases (10 or 80) are valid and tested by assertions
|
2025-08-19 11:41:02 +02:00
|
|
|
|
}
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Test that other liquidity params are still calculated correctly
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testOtherLiquidityParams() public {
|
|
|
|
|
|
mockStake.setPercentageStaked(0.6e18);
|
|
|
|
|
|
mockStake.setAverageTaxRate(0.4e18);
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
|
|
|
|
|
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams();
|
|
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
uint256 sentiment = optimizer.getSentiment();
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Verify relationships
|
|
|
|
|
|
assertEq(capitalInefficiency, 1e18 - sentiment, "Capital inefficiency should be 1 - sentiment");
|
|
|
|
|
|
assertEq(anchorShare, sentiment, "Anchor share should equal sentiment");
|
|
|
|
|
|
assertEq(discoveryDepth, sentiment, "Discovery depth should equal sentiment");
|
2025-10-04 15:17:09 +02:00
|
|
|
|
|
2025-08-19 11:41:02 +02:00
|
|
|
|
// Verify anchor width is calculated independently
|
|
|
|
|
|
// Expected: base(40) + staking_adj(20 - 24 = -4) + tax_adj(16 - 10 = 6) = 42
|
|
|
|
|
|
assertEq(anchorWidth, 42, "Anchor width should be independently calculated");
|
|
|
|
|
|
}
|
2026-02-26 02:41:02 +00:00
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// COVERAGE TESTS: calculateSentiment direct call + mid-range tax + zero path
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Direct external call to calculateSentiment covers the function in coverage metrics
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testCalculateSentimentDirect() public view {
|
|
|
|
|
|
// 100% staked, any tax → high staking path → very low penalty
|
|
|
|
|
|
uint256 sentiment = optimizer.calculateSentiment(0, 1e18);
|
|
|
|
|
|
// deltaS = 0, penalty = 0, sentimentValue = 0
|
|
|
|
|
|
assertEq(sentiment, 0, "100% staked, 0 tax: penalty=0 so sentiment=0");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Cover the else-if (averageTaxRate <= 5e16) branch with a result > 0
|
|
|
|
|
|
* @dev averageTaxRate = 3e16 (in range (1e16, 5e16]), percentageStaked = 0
|
|
|
|
|
|
* baseSentiment = 1e18, ratePenalty = (2e16 * 1e18) / 4e16 = 5e17
|
|
|
|
|
|
* result = 1e18 - 5e17 = 5e17
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testCalculateSentimentMidRangeTax() public view {
|
|
|
|
|
|
uint256 sentiment = optimizer.calculateSentiment(3e16, 0);
|
|
|
|
|
|
assertEq(sentiment, 5e17, "Mid-range tax should apply partial penalty");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Cover the ternary zero path: baseSentiment > ratePenalty ? ... : 0
|
|
|
|
|
|
* @dev averageTaxRate = 5e16 (boundary), percentageStaked = 0
|
|
|
|
|
|
* baseSentiment = 1e18, ratePenalty = (4e16 * 1e18) / 4e16 = 1e18
|
|
|
|
|
|
* 1e18 > 1e18 is false → sentimentValue = 0
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testCalculateSentimentZeroPath() public view {
|
|
|
|
|
|
uint256 sentiment = optimizer.calculateSentiment(5e16, 0);
|
|
|
|
|
|
assertEq(sentiment, 0, "At boundary 5e16 ratePenalty equals baseSentiment so result is zero");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// COVERAGE TESTS: UUPS upgrade flow (_checkAdmin, _authorizeUpgrade, onlyAdmin)
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Deploy via ERC1967Proxy and call upgradeTo to cover _authorizeUpgrade + _checkAdmin
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testUUPSUpgrade() public {
|
|
|
|
|
|
Optimizer impl1 = new Optimizer();
|
fix: Backtesting #5: Position tracking + P&L metrics (#319)
- Add PositionTracker.sol: tracks position lifecycle (open/close per
recenter), records tick ranges, liquidity, entry/exit blocks/timestamps,
token amounts (via LiquidityAmounts math), fees (proportional to
liquidity share), IL (LP exit value − HODL value at exit price), and
net P&L per position. Aggregates total fees, cumulative IL, net P&L,
rebalance count, Anchor time-in-range, and capital efficiency accumulators.
Logs with [TRACKER][TYPE] prefix; emits cumulative P&L every 500 blocks.
- Modify StrategyExecutor.sol: add IUniswapV3Pool + token0isWeth to
constructor (creates PositionTracker internally), call
tracker.notifyBlock() on every block for time-in-range, and call
tracker.recordRecenter() on each successful recenter. logSummary()
now delegates to tracker.logFinalSummary().
- Modify BacktestRunner.s.sol: pass sp.pool and token0isWeth to
StrategyExecutor constructor; log tracker address.
- forge fmt: reformat all backtesting scripts and affected src/test files
to project style (number_underscore=thousands, multiline_func_header=all).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:23:18 +00:00
|
|
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(impl1), abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake)));
|
2026-02-26 02:41:02 +00:00
|
|
|
|
Optimizer proxyOptimizer = Optimizer(address(proxy));
|
|
|
|
|
|
|
|
|
|
|
|
// Deployer (this contract) is admin — upgrade should succeed
|
|
|
|
|
|
Optimizer impl2 = new Optimizer();
|
|
|
|
|
|
proxyOptimizer.upgradeTo(address(impl2));
|
|
|
|
|
|
|
|
|
|
|
|
// Verify proxy still works after upgrade
|
|
|
|
|
|
(,, uint24 w,) = proxyOptimizer.getLiquidityParams();
|
|
|
|
|
|
assertTrue(w >= 10 && w <= 80, "Params should still work after upgrade");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Cover the require revert branch in calculateSentiment (percentageStaked > 1e18)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testCalculateSentimentRevertsAbove100Percent() public {
|
|
|
|
|
|
vm.expectRevert("Invalid percentage staked");
|
|
|
|
|
|
optimizer.calculateSentiment(0, 1e18 + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Cover the totalWidth < 10 clamp via OptimizerHarness.
|
|
|
|
|
|
* @dev With percentageStaked = 1.5e18 and averageTaxRate = 0:
|
|
|
|
|
|
* stakingAdjustment = 20 - 60 = -40
|
|
|
|
|
|
* taxAdjustment = 0 - 10 = -10
|
|
|
|
|
|
* totalWidth = 40 - 40 - 10 = -10 → clamped to 10
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testAnchorWidthBelowTenClamp() public {
|
|
|
|
|
|
OptimizerHarness harness = new OptimizerHarness();
|
|
|
|
|
|
uint24 w = harness.exposed_calculateAnchorWidth(15e17, 0);
|
|
|
|
|
|
assertEq(w, 10, "totalWidth < 10 should be clamped to minimum of 10");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:41:39 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice calculateParams reverts when inputs[0].mantissa is negative
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testCalculateParamsRevertsOnNegativeMantissa0() public {
|
|
|
|
|
|
OptimizerInput[8] memory inputs;
|
2026-03-19 13:57:25 +00:00
|
|
|
|
inputs[0] = OptimizerInput({ mantissa: -1, shift: 0 });
|
2026-03-12 15:41:39 +00:00
|
|
|
|
vm.expectRevert("negative mantissa");
|
|
|
|
|
|
optimizer.calculateParams(inputs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice calculateParams reverts when inputs[1].mantissa is negative
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testCalculateParamsRevertsOnNegativeMantissa1() public {
|
|
|
|
|
|
OptimizerInput[8] memory inputs;
|
2026-03-19 13:57:25 +00:00
|
|
|
|
inputs[1] = OptimizerInput({ mantissa: -1, shift: 0 });
|
2026-03-12 15:41:39 +00:00
|
|
|
|
vm.expectRevert("negative mantissa");
|
|
|
|
|
|
optimizer.calculateParams(inputs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 18:24:18 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice calculateParams reverts when any of inputs[2..7].mantissa is negative
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testCalculateParamsRevertsOnNegativeMantissaSlots2to7() public {
|
|
|
|
|
|
for (uint256 k = 2; k < 8; k++) {
|
|
|
|
|
|
OptimizerInput[8] memory inputs;
|
2026-03-19 13:57:25 +00:00
|
|
|
|
inputs[k] = OptimizerInput({ mantissa: -1, shift: 0 });
|
2026-03-18 18:24:18 +00:00
|
|
|
|
vm.expectRevert("negative mantissa");
|
|
|
|
|
|
optimizer.calculateParams(inputs);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 11:42:50 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice calculateParams reverts when any slot has shift != 0
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testCalculateParamsRevertsOnNonZeroShift() public {
|
|
|
|
|
|
for (uint256 k = 0; k < 8; k++) {
|
|
|
|
|
|
OptimizerInput[8] memory inputs;
|
|
|
|
|
|
inputs[k] = OptimizerInput({ mantissa: 0, shift: 1 });
|
|
|
|
|
|
vm.expectRevert("shift not yet supported");
|
|
|
|
|
|
optimizer.calculateParams(inputs);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 02:41:02 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @notice Non-admin calling upgradeTo should revert with UnauthorizedAccount
|
|
|
|
|
|
*/
|
|
|
|
|
|
function testUnauthorizedUpgradeReverts() public {
|
|
|
|
|
|
Optimizer impl1 = new Optimizer();
|
fix: Backtesting #5: Position tracking + P&L metrics (#319)
- Add PositionTracker.sol: tracks position lifecycle (open/close per
recenter), records tick ranges, liquidity, entry/exit blocks/timestamps,
token amounts (via LiquidityAmounts math), fees (proportional to
liquidity share), IL (LP exit value − HODL value at exit price), and
net P&L per position. Aggregates total fees, cumulative IL, net P&L,
rebalance count, Anchor time-in-range, and capital efficiency accumulators.
Logs with [TRACKER][TYPE] prefix; emits cumulative P&L every 500 blocks.
- Modify StrategyExecutor.sol: add IUniswapV3Pool + token0isWeth to
constructor (creates PositionTracker internally), call
tracker.notifyBlock() on every block for time-in-range, and call
tracker.recordRecenter() on each successful recenter. logSummary()
now delegates to tracker.logFinalSummary().
- Modify BacktestRunner.s.sol: pass sp.pool and token0isWeth to
StrategyExecutor constructor; log tracker address.
- forge fmt: reformat all backtesting scripts and affected src/test files
to project style (number_underscore=thousands, multiline_func_header=all).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:23:18 +00:00
|
|
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(impl1), abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake)));
|
2026-02-26 02:41:02 +00:00
|
|
|
|
Optimizer proxyOptimizer = Optimizer(address(proxy));
|
|
|
|
|
|
|
|
|
|
|
|
// Deploy impl2 BEFORE the prank so the prank applies only to upgradeTo
|
|
|
|
|
|
Optimizer impl2 = new Optimizer();
|
|
|
|
|
|
address nonAdmin = makeAddr("nonAdmin");
|
|
|
|
|
|
vm.expectRevert(abi.encodeWithSelector(Optimizer.UnauthorizedAccount.selector, nonAdmin));
|
|
|
|
|
|
vm.prank(nonAdmin);
|
|
|
|
|
|
proxyOptimizer.upgradeTo(address(impl2));
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
// Normalized indicator tests (slots 2-6)
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @title OptimizerNormalizedInputsTest
|
|
|
|
|
|
* @notice Tests for the normalized indicator computation in getLiquidityParams.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Uses a harness that exposes input-slot values via a dedicated calculateParams
|
|
|
|
|
|
* override so we can observe what values the normalization logic writes into slots
|
|
|
|
|
|
* 2-6 without wiring a full protocol stack.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-19 13:57:25 +00:00
|
|
|
|
/// @dev Harness: exposes the internal _buildInputs() so tests can assert the
|
|
|
|
|
|
/// normalized slot values that getLiquidityParams feeds into calculateParams.
|
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
|
|
|
|
contract OptimizerInputCapture is Optimizer {
|
2026-03-19 13:57:25 +00:00
|
|
|
|
/// @notice Returns the mantissa of each normalized input slot.
|
|
|
|
|
|
function getComputedInputs() external view returns (int256[8] memory mantissas) {
|
|
|
|
|
|
OptimizerInput[8] memory inputs = _buildInputs();
|
|
|
|
|
|
for (uint256 i; i < 8; i++) {
|
|
|
|
|
|
mantissas[i] = inputs[i].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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Harness that exposes _vwapToTick for direct unit testing.
|
|
|
|
|
|
contract OptimizerVwapHarness is Optimizer {
|
|
|
|
|
|
function exposed_vwapToTick(uint256 vwapX96) external pure returns (int24) {
|
|
|
|
|
|
return _vwapToTick(vwapX96);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
contract OptimizerNormalizedInputsTest is Test {
|
2026-03-19 13:57:25 +00:00
|
|
|
|
OptimizerInputCapture capture;
|
|
|
|
|
|
Optimizer optimizer; // alias — points to the same proxy as `capture`
|
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
|
|
|
|
MockStake mockStake;
|
|
|
|
|
|
MockKraiken mockKraiken;
|
|
|
|
|
|
MockVWAPTracker mockVwap;
|
|
|
|
|
|
MockPool mockPool;
|
|
|
|
|
|
MockLiquidityManagerPositions mockLm;
|
|
|
|
|
|
|
|
|
|
|
|
// Stage.ANCHOR == 1
|
|
|
|
|
|
uint8 constant ANCHOR = 1;
|
|
|
|
|
|
|
|
|
|
|
|
function setUp() public {
|
|
|
|
|
|
mockKraiken = new MockKraiken();
|
|
|
|
|
|
mockStake = new MockStake();
|
|
|
|
|
|
mockVwap = new MockVWAPTracker();
|
|
|
|
|
|
mockPool = new MockPool();
|
|
|
|
|
|
mockLm = new MockLiquidityManagerPositions();
|
|
|
|
|
|
|
2026-03-19 13:57:25 +00:00
|
|
|
|
OptimizerInputCapture impl = new OptimizerInputCapture();
|
|
|
|
|
|
bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
|
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
|
|
|
|
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData);
|
2026-03-19 13:57:25 +00:00
|
|
|
|
capture = OptimizerInputCapture(address(proxy));
|
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
|
|
|
|
optimizer = Optimizer(address(proxy));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// Helpers
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Configure all data sources on the optimizer (token0 = WETH convention).
|
|
|
|
|
|
function _configureSources(bool _token0isWeth) internal {
|
|
|
|
|
|
optimizer.setDataSources(address(mockVwap), address(mockPool), address(mockLm), _token0isWeth);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @dev Seed the MockVWAPTracker with a price at a given tick.
|
|
|
|
|
|
/// Mirrors LiquidityManager._priceAtTick: priceX96 = sqrtRatio^2 / 2^96.
|
|
|
|
|
|
/// Uses Math.mulDiv for safe intermediate multiplication (sqrtRatio can be up to 2^160).
|
|
|
|
|
|
function _seedVwapAtTick(int24 adjTick) internal {
|
|
|
|
|
|
uint256 sqrtRatio = TickMath.getSqrtRatioAtTick(adjTick);
|
|
|
|
|
|
// Safe: sqrtRatio up to ~1.46e48 (uint160); sqrtRatio^2 / 2^96 may still overflow for
|
|
|
|
|
|
// very large ticks, so use mulDiv which handles 512-bit intermediate products.
|
|
|
|
|
|
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
|
|
|
|
|
|
mockVwap.recordVolumeAndPrice(priceX96, 1 ether);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// _vwapToTick round-trip tests (via OptimizerVwapHarness)
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
function testVwapToTickRoundTrip() public {
|
|
|
|
|
|
OptimizerVwapHarness harness = new OptimizerVwapHarness();
|
|
|
|
|
|
int24[] memory ticks = new int24[](5);
|
|
|
|
|
|
ticks[0] = 0;
|
|
|
|
|
|
ticks[1] = 1000;
|
|
|
|
|
|
ticks[2] = -1000;
|
2026-03-19 13:57:25 +00:00
|
|
|
|
ticks[3] = 100_000;
|
|
|
|
|
|
ticks[4] = -100_000;
|
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
|
|
|
|
|
|
|
|
|
|
for (uint256 i = 0; i < ticks.length; i++) {
|
|
|
|
|
|
int24 origTick = ticks[i];
|
|
|
|
|
|
uint256 sqrtRatio = TickMath.getSqrtRatioAtTick(origTick);
|
|
|
|
|
|
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
|
|
|
|
|
|
int24 recovered = harness.exposed_vwapToTick(priceX96);
|
|
|
|
|
|
// Allow ±1 tick error from integer sqrt truncation
|
2026-03-19 13:57:25 +00:00
|
|
|
|
assertTrue(recovered == origTick || recovered == origTick - 1 || recovered == origTick + 1, "round-trip tick error > 1");
|
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 normalization
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
function testTimeSinceRecenterZeroWhenNeverCalled() public {
|
|
|
|
|
|
// Without calling recordRecenter, slot 5 should be 0 (inputs default)
|
|
|
|
|
|
// We can't observe slot 5 directly, but we know calculateParams ignores it.
|
|
|
|
|
|
// Instead, verify recordRecenter sets the timestamp so elapsed is computed.
|
|
|
|
|
|
assertEq(optimizer.lastRecenterTimestamp(), 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testTimeSinceRecenterNormalized() public {
|
|
|
|
|
|
// Set recenter recorder to this test contract
|
|
|
|
|
|
optimizer.setRecenterRecorder(address(this));
|
|
|
|
|
|
optimizer.recordRecenter();
|
|
|
|
|
|
uint256 recorded = optimizer.lastRecenterTimestamp(); // capture AFTER recording
|
|
|
|
|
|
|
|
|
|
|
|
// Advance time by MAX_STALE_SECONDS / 2 — should give 0.5e18
|
|
|
|
|
|
vm.warp(recorded + 43_200); // half day
|
|
|
|
|
|
|
|
|
|
|
|
uint256 elapsed = block.timestamp - optimizer.lastRecenterTimestamp();
|
|
|
|
|
|
assertEq(elapsed, 43_200, "elapsed should be exactly half of MAX_STALE_SECONDS");
|
|
|
|
|
|
// 43200 * 1e18 / 86400 = 0.5e18
|
|
|
|
|
|
assertEq(elapsed * 1e18 / 86_400, 5e17, "half-stale should normalize to 0.5e18");
|
2026-03-19 13:57:25 +00:00
|
|
|
|
|
|
|
|
|
|
// Verify slot 5 via capture harness
|
|
|
|
|
|
int256[8] memory m = capture.getComputedInputs();
|
|
|
|
|
|
assertEq(m[5], int256(5e17), "slot 5 should be 0.5e18 at half-stale");
|
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 testTimeSinceRecenterSaturatesAt1e18() public {
|
|
|
|
|
|
optimizer.setRecenterRecorder(address(this));
|
|
|
|
|
|
optimizer.recordRecenter();
|
|
|
|
|
|
uint256 t0 = block.timestamp;
|
|
|
|
|
|
|
|
|
|
|
|
vm.warp(t0 + 200_000); // > 86400
|
|
|
|
|
|
uint256 elapsed = block.timestamp - optimizer.lastRecenterTimestamp();
|
|
|
|
|
|
assertTrue(elapsed >= 86_400, "should be past max stale");
|
|
|
|
|
|
// Normalized should be capped at 1e18
|
|
|
|
|
|
uint256 normalized = elapsed >= 86_400 ? 1e18 : elapsed * 1e18 / 86_400;
|
|
|
|
|
|
assertEq(normalized, 1e18, "over-stale should normalize to 1e18");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// Slot 2: pricePosition
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
function testPricePositionAtVwap() public {
|
|
|
|
|
|
_configureSources(false); // token0=KRK, so adjTick = poolTick
|
|
|
|
|
|
|
|
|
|
|
|
int24 targetTick = 500;
|
|
|
|
|
|
_seedVwapAtTick(targetTick);
|
|
|
|
|
|
mockPool.setCurrentTick(targetTick); // current == vwap → 0.5e18
|
2026-03-19 13:57:25 +00:00
|
|
|
|
mockPool.setRevertOnObserve(true); // disable volatility/momentum for isolation
|
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
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams();
|
2026-03-19 13:57:25 +00:00
|
|
|
|
|
|
|
|
|
|
// Verify slot 2 (pricePosition) is approximately 0.5e18 when current == vwap
|
|
|
|
|
|
int256[8] memory m = capture.getComputedInputs();
|
|
|
|
|
|
// Allow ±1 tick error from _vwapToTick integer sqrt truncation
|
|
|
|
|
|
assertTrue(m[2] > 4.5e17 && m[2] < 5.5e17, "pricePosition should be ~0.5e18 at VWAP");
|
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 testPricePositionBelowLowerBound() public {
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
|
|
|
|
|
|
int24 vwapTick = 0;
|
|
|
|
|
|
_seedVwapAtTick(vwapTick);
|
|
|
|
|
|
// Current tick is far below VWAP − PRICE_BOUND_TICKS (11 000 below) → pricePosition = 0
|
|
|
|
|
|
mockPool.setCurrentTick(-20_000); // 20 000 ticks below vwap
|
|
|
|
|
|
mockPool.setRevertOnObserve(true);
|
|
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams(); // must not revert
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testPricePositionAboveUpperBound() public {
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
|
|
|
|
|
|
int24 vwapTick = 0;
|
|
|
|
|
|
_seedVwapAtTick(vwapTick);
|
|
|
|
|
|
// Current tick is far above VWAP + PRICE_BOUND_TICKS → pricePosition = 1e18
|
|
|
|
|
|
mockPool.setCurrentTick(20_000);
|
|
|
|
|
|
mockPool.setRevertOnObserve(true);
|
|
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams(); // must not revert
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testPricePositionToken0IsWethFlipsSign() public {
|
|
|
|
|
|
// With token0isWeth=true, adjTick = poolTick (no negation).
|
|
|
|
|
|
// If poolTick=500 → adjTick=500 = vwapTick → pricePosition ≈ 0.5e18.
|
|
|
|
|
|
// Verifies that token0isWeth=true does NOT negate the pool tick.
|
|
|
|
|
|
_configureSources(true); // token0=WETH
|
|
|
|
|
|
|
|
|
|
|
|
int24 vwapAdjTick = 500;
|
|
|
|
|
|
_seedVwapAtTick(vwapAdjTick); // VWAP at adjTick=500
|
|
|
|
|
|
|
|
|
|
|
|
// Pool tick = 500 → adjTick = 500 = vwapTick → pricePosition ≈ 0.5e18
|
|
|
|
|
|
mockPool.setCurrentTick(500);
|
|
|
|
|
|
mockPool.setRevertOnObserve(true);
|
|
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams(); // must not revert
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// Slots 3-4: volatility and momentum
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
function testVolatilityZeroWhenFlatMarket() public {
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
_seedVwapAtTick(0);
|
|
|
|
|
|
mockPool.setCurrentTick(0);
|
|
|
|
|
|
// shortTwap == longTwap → volatility = 0, momentum = 0.5e18
|
|
|
|
|
|
mockPool.setTwapTicks(100, 100);
|
|
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams(); // must not revert
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testMomentumFullBullAtMaxDelta() public {
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
_seedVwapAtTick(0);
|
|
|
|
|
|
mockPool.setCurrentTick(0);
|
|
|
|
|
|
// shortTwap - longTwap = 1000 ticks = MAX_MOMENTUM_TICKS → momentum = 1e18
|
2026-03-19 13:57:25 +00:00
|
|
|
|
mockPool.setTwapTicks(0, 1000); // longTwap=0, shortTwap=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
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams(); // must not revert
|
2026-03-19 13:57:25 +00:00
|
|
|
|
|
|
|
|
|
|
// Verify slots 3 (volatility) and 4 (momentum) via capture harness.
|
|
|
|
|
|
// Note: with token0isWeth=false, pool ticks are negated into KRK-price space.
|
|
|
|
|
|
// Pool shortTwap=1000, longTwap=0 → KRK-space twapDelta = -1000 (max bear).
|
|
|
|
|
|
int256[8] memory m = capture.getComputedInputs();
|
|
|
|
|
|
assertEq(m[3], int256(1e18), "volatility should be 1e18 at max delta");
|
|
|
|
|
|
assertEq(m[4], int256(0), "momentum should be 0 (max bear in KRK-price space)");
|
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 testMomentumFullBearAtNegMaxDelta() public {
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
_seedVwapAtTick(0);
|
|
|
|
|
|
mockPool.setCurrentTick(0);
|
|
|
|
|
|
// shortTwap - longTwap = -1000 = -MAX_MOMENTUM_TICKS → momentum = 0
|
2026-03-19 13:57:25 +00:00
|
|
|
|
mockPool.setTwapTicks(1000, 0); // longTwap=1000, shortTwap=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
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams(); // must not revert
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testObserveRevertLeavesSlots34AsZero() public {
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
_seedVwapAtTick(0);
|
|
|
|
|
|
mockPool.setCurrentTick(0);
|
|
|
|
|
|
mockPool.setRevertOnObserve(true); // triggers catch branch
|
|
|
|
|
|
|
|
|
|
|
|
// Must not revert — slots 3-4 remain 0 (calculateParams ignores them anyway)
|
|
|
|
|
|
optimizer.getLiquidityParams();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// Slot 6: utilizationRate
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
function testUtilizationRateInRange() public {
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
// Set anchor position in range [−100, 100]; current tick = 0 → in range → 1e18
|
|
|
|
|
|
mockLm.setPosition(ANCHOR, 1e18, -100, 100);
|
|
|
|
|
|
mockPool.setCurrentTick(0);
|
|
|
|
|
|
mockPool.setRevertOnObserve(true);
|
|
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams(); // must not revert
|
2026-03-19 13:57:25 +00:00
|
|
|
|
|
|
|
|
|
|
// Verify slot 6 (utilizationRate) via capture harness
|
|
|
|
|
|
int256[8] memory m = capture.getComputedInputs();
|
|
|
|
|
|
assertEq(m[6], int256(1e18), "utilizationRate should be 1e18 when tick is in anchor range");
|
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 testUtilizationRateOutOfRange() public {
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
// Anchor range [−100, 100]; current tick = 500 → out of range → 0
|
|
|
|
|
|
mockLm.setPosition(ANCHOR, 1e18, -100, 100);
|
|
|
|
|
|
mockPool.setCurrentTick(500);
|
|
|
|
|
|
mockPool.setRevertOnObserve(true);
|
|
|
|
|
|
|
|
|
|
|
|
optimizer.getLiquidityParams(); // must not revert
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// Data-source disabled: slots remain 0, no revert
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
function testNoDataSourcesNoRevert() public {
|
|
|
|
|
|
// No sources configured — only slots 0,1 are set; rest are 0
|
|
|
|
|
|
optimizer.getLiquidityParams();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testPoolOnlyNoVwapNoRevert() public {
|
|
|
|
|
|
optimizer.setDataSources(address(0), address(mockPool), address(mockLm), false);
|
|
|
|
|
|
mockPool.setCurrentTick(0);
|
|
|
|
|
|
optimizer.getLiquidityParams(); // slots 2-4 remain 0 (no VWAP), slot 6 computed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testVwapOnlyNoPoolNoRevert() public {
|
|
|
|
|
|
optimizer.setDataSources(address(mockVwap), address(0), address(0), false);
|
|
|
|
|
|
_seedVwapAtTick(0);
|
|
|
|
|
|
optimizer.getLiquidityParams(); // pool-dependent slots remain 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// Fuzz: normalized outputs are always in [0, 1e18]
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
function testFuzzPricePositionInRange(int24 currentTick, int24 vwapTick) public {
|
|
|
|
|
|
// Bound to ticks where priceX96 * 100e18 (VWAPTracker volume-weight) stays < uint256 max.
|
|
|
|
|
|
// At tick 500 000: sqrtRatio ≈ 8.5e40, priceX96 ≈ 9e52, volume = 1e20 → product ≈ 9e72 < 1.16e77 ✓
|
|
|
|
|
|
// Margin is ~4 orders of magnitude below overflow.
|
|
|
|
|
|
int24 SAFE_MAX = 500_000;
|
|
|
|
|
|
currentTick = int24(bound(int256(currentTick), -SAFE_MAX, SAFE_MAX));
|
|
|
|
|
|
vwapTick = int24(bound(int256(vwapTick), -SAFE_MAX, SAFE_MAX));
|
|
|
|
|
|
|
|
|
|
|
|
_configureSources(false);
|
|
|
|
|
|
_seedVwapAtTick(vwapTick);
|
|
|
|
|
|
mockPool.setCurrentTick(currentTick);
|
|
|
|
|
|
mockPool.setRevertOnObserve(true);
|
|
|
|
|
|
|
|
|
|
|
|
// getLiquidityParams must not revert regardless of tick values
|
|
|
|
|
|
optimizer.getLiquidityParams();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
|
|
|
|
|
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
2026-03-20 02:05:25 +00:00
|
|
|
|
|