harb/onchain/test/Optimizer.t.sol
openhands bb150671ea fix: OptimizerInputCapture test harness is structurally broken (#652)
The pure override in OptimizerInputCapture could not write to storage,
and getLiquidityParams calls calculateParams via staticcall which
prevents both storage writes and event emissions.

Fix: extract the input-building normalization from getLiquidityParams
into _buildInputs() (internal view, behavior-preserving refactor).
The test harness now exposes _buildInputs() via getComputedInputs(),
allowing tests to assert actual normalized slot values.

Updated tests for pricePosition, timeSinceRecenter, volatility,
momentum, and utilizationRate to assert non-zero captured values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:57:25 +00:00

706 lines
29 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "../src/Optimizer.sol";
import "./mocks/MockKraiken.sol";
import "./mocks/MockLiquidityManagerPositions.sol";
import "./mocks/MockPool.sol";
import "./mocks/MockStake.sol";
import "./mocks/MockVWAPTracker.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/Test.sol";
import "forge-std/console.sol";
/// @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);
}
}
contract OptimizerTest is Test {
Optimizer optimizer;
MockStake mockStake;
MockKraiken mockKraiken;
function setUp() public {
// Deploy mocks
mockKraiken = new MockKraiken();
mockStake = new MockStake();
// Deploy Optimizer implementation
Optimizer implementation = new Optimizer();
// Deploy proxy and initialize
bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
// 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));
}
/**
* @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);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// 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%");
}
/**
* @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);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// 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%");
}
/**
* @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);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// 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%");
}
/**
* @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);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// 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%");
}
/**
* @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);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// 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%");
}
/**
* @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);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// 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");
}
/**
* @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);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// 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");
}
/**
* @notice Test edge case with exactly minimum staking and tax
*/
function testEdgeCaseMinimumInputs() public {
mockStake.setPercentageStaked(0);
mockStake.setAverageTaxRate(0);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(0 - 10 = -10) = 50
assertEq(anchorWidth, 50, "Zero inputs should give moderate width");
}
/**
* @notice Test edge case with exactly maximum staking and tax
*/
function testEdgeCaseMaximumInputs() public {
mockStake.setPercentageStaked(1e18);
mockStake.setAverageTaxRate(1e18);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// 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");
}
/**
* @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
mockStake.setPercentageStaked(946_350_908_835_331_692);
mockStake.setAverageTaxRate(966_925_542_613_630_263);
(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");
}
/**
* @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);
mockStake.setPercentageStaked(percentageStaked);
mockStake.setAverageTaxRate(averageTaxRate);
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
// Assert bounds are always respected
assertTrue(anchorWidth >= 10, "Width should never be less than 10");
assertTrue(anchorWidth <= 80, "Width should never exceed 80");
// Edge cases (10 or 80) are valid and tested by assertions
}
/**
* @notice Test that other liquidity params are still calculated correctly
*/
function testOtherLiquidityParams() public {
mockStake.setPercentageStaked(0.6e18);
mockStake.setAverageTaxRate(0.4e18);
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams();
uint256 sentiment = optimizer.getSentiment();
// 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");
// 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");
}
// =========================================================
// 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();
ERC1967Proxy proxy = new ERC1967Proxy(address(impl1), abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake)));
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");
}
/**
* @notice calculateParams reverts when inputs[0].mantissa is negative
*/
function testCalculateParamsRevertsOnNegativeMantissa0() public {
OptimizerInput[8] memory inputs;
inputs[0] = OptimizerInput({ mantissa: -1, shift: 0 });
vm.expectRevert("negative mantissa");
optimizer.calculateParams(inputs);
}
/**
* @notice calculateParams reverts when inputs[1].mantissa is negative
*/
function testCalculateParamsRevertsOnNegativeMantissa1() public {
OptimizerInput[8] memory inputs;
inputs[1] = OptimizerInput({ mantissa: -1, shift: 0 });
vm.expectRevert("negative mantissa");
optimizer.calculateParams(inputs);
}
/**
* @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;
inputs[k] = OptimizerInput({ mantissa: -1, shift: 0 });
vm.expectRevert("negative mantissa");
optimizer.calculateParams(inputs);
}
}
/**
* @notice Non-admin calling upgradeTo should revert with UnauthorizedAccount
*/
function testUnauthorizedUpgradeReverts() public {
Optimizer impl1 = new Optimizer();
ERC1967Proxy proxy = new ERC1967Proxy(address(impl1), abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake)));
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));
}
}
// =============================================================================
// Normalized indicator tests (slots 2-6)
// =============================================================================
/**
* @title OptimizerNormalizedInputsTest
* @notice Tests for the normalized indicator computation in getLiquidityParams.
*
* Uses a harness that exposes input-slot values via a dedicated calculateParams
* override so we can observe what values the normalization logic writes into slots
* 2-6 without wiring a full protocol stack.
*/
/// @dev Harness: exposes the internal _buildInputs() so tests can assert the
/// normalized slot values that getLiquidityParams feeds into calculateParams.
contract OptimizerInputCapture is Optimizer {
/// @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;
}
}
}
/// @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 {
OptimizerInputCapture capture;
Optimizer optimizer; // alias — points to the same proxy as `capture`
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();
OptimizerInputCapture impl = new OptimizerInputCapture();
bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData);
capture = OptimizerInputCapture(address(proxy));
optimizer = Optimizer(address(proxy));
}
// =========================================================
// Helpers
// =========================================================
/// @dev Configure all data sources on the optimizer (token0 = WETH convention).
function _configureSources(bool _token0isWeth) internal {
optimizer.setDataSources(address(mockVwap), address(mockPool), address(mockLm), _token0isWeth);
}
/// @dev Seed the MockVWAPTracker with a price at a given tick.
/// Mirrors LiquidityManager._priceAtTick: priceX96 = sqrtRatio^2 / 2^96.
/// Uses Math.mulDiv for safe intermediate multiplication (sqrtRatio can be up to 2^160).
function _seedVwapAtTick(int24 adjTick) internal {
uint256 sqrtRatio = TickMath.getSqrtRatioAtTick(adjTick);
// Safe: sqrtRatio up to ~1.46e48 (uint160); sqrtRatio^2 / 2^96 may still overflow for
// very large ticks, so use mulDiv which handles 512-bit intermediate products.
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
mockVwap.recordVolumeAndPrice(priceX96, 1 ether);
}
// =========================================================
// _vwapToTick round-trip tests (via OptimizerVwapHarness)
// =========================================================
function testVwapToTickRoundTrip() public {
OptimizerVwapHarness harness = new OptimizerVwapHarness();
int24[] memory ticks = new int24[](5);
ticks[0] = 0;
ticks[1] = 1000;
ticks[2] = -1000;
ticks[3] = 100_000;
ticks[4] = -100_000;
for (uint256 i = 0; i < ticks.length; i++) {
int24 origTick = ticks[i];
uint256 sqrtRatio = TickMath.getSqrtRatioAtTick(origTick);
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
int24 recovered = harness.exposed_vwapToTick(priceX96);
// Allow ±1 tick error from integer sqrt truncation
assertTrue(recovered == origTick || recovered == origTick - 1 || recovered == origTick + 1, "round-trip tick error > 1");
}
}
// =========================================================
// Slot 5: timeSinceRecenter normalization
// =========================================================
function testTimeSinceRecenterZeroWhenNeverCalled() public {
// Without calling recordRecenter, slot 5 should be 0 (inputs default)
// We can't observe slot 5 directly, but we know calculateParams ignores it.
// Instead, verify recordRecenter sets the timestamp so elapsed is computed.
assertEq(optimizer.lastRecenterTimestamp(), 0);
}
function testTimeSinceRecenterNormalized() public {
// Set recenter recorder to this test contract
optimizer.setRecenterRecorder(address(this));
optimizer.recordRecenter();
uint256 recorded = optimizer.lastRecenterTimestamp(); // capture AFTER recording
// Advance time by MAX_STALE_SECONDS / 2 — should give 0.5e18
vm.warp(recorded + 43_200); // half day
uint256 elapsed = block.timestamp - optimizer.lastRecenterTimestamp();
assertEq(elapsed, 43_200, "elapsed should be exactly half of MAX_STALE_SECONDS");
// 43200 * 1e18 / 86400 = 0.5e18
assertEq(elapsed * 1e18 / 86_400, 5e17, "half-stale should normalize to 0.5e18");
// 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");
}
function testTimeSinceRecenterSaturatesAt1e18() public {
optimizer.setRecenterRecorder(address(this));
optimizer.recordRecenter();
uint256 t0 = block.timestamp;
vm.warp(t0 + 200_000); // > 86400
uint256 elapsed = block.timestamp - optimizer.lastRecenterTimestamp();
assertTrue(elapsed >= 86_400, "should be past max stale");
// Normalized should be capped at 1e18
uint256 normalized = elapsed >= 86_400 ? 1e18 : elapsed * 1e18 / 86_400;
assertEq(normalized, 1e18, "over-stale should normalize to 1e18");
}
// =========================================================
// Slot 2: pricePosition
// =========================================================
function testPricePositionAtVwap() public {
_configureSources(false); // token0=KRK, so adjTick = poolTick
int24 targetTick = 500;
_seedVwapAtTick(targetTick);
mockPool.setCurrentTick(targetTick); // current == vwap → 0.5e18
mockPool.setRevertOnObserve(true); // disable volatility/momentum for isolation
optimizer.getLiquidityParams();
// 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");
}
function testPricePositionBelowLowerBound() public {
_configureSources(false);
int24 vwapTick = 0;
_seedVwapAtTick(vwapTick);
// Current tick is far below VWAP PRICE_BOUND_TICKS (11 000 below) → pricePosition = 0
mockPool.setCurrentTick(-20_000); // 20 000 ticks below vwap
mockPool.setRevertOnObserve(true);
optimizer.getLiquidityParams(); // must not revert
}
function testPricePositionAboveUpperBound() public {
_configureSources(false);
int24 vwapTick = 0;
_seedVwapAtTick(vwapTick);
// Current tick is far above VWAP + PRICE_BOUND_TICKS → pricePosition = 1e18
mockPool.setCurrentTick(20_000);
mockPool.setRevertOnObserve(true);
optimizer.getLiquidityParams(); // must not revert
}
function testPricePositionToken0IsWethFlipsSign() public {
// With token0isWeth=true, adjTick = poolTick (no negation).
// If poolTick=500 → adjTick=500 = vwapTick → pricePosition ≈ 0.5e18.
// Verifies that token0isWeth=true does NOT negate the pool tick.
_configureSources(true); // token0=WETH
int24 vwapAdjTick = 500;
_seedVwapAtTick(vwapAdjTick); // VWAP at adjTick=500
// Pool tick = 500 → adjTick = 500 = vwapTick → pricePosition ≈ 0.5e18
mockPool.setCurrentTick(500);
mockPool.setRevertOnObserve(true);
optimizer.getLiquidityParams(); // must not revert
}
// =========================================================
// Slots 3-4: volatility and momentum
// =========================================================
function testVolatilityZeroWhenFlatMarket() public {
_configureSources(false);
_seedVwapAtTick(0);
mockPool.setCurrentTick(0);
// shortTwap == longTwap → volatility = 0, momentum = 0.5e18
mockPool.setTwapTicks(100, 100);
optimizer.getLiquidityParams(); // must not revert
}
function testMomentumFullBullAtMaxDelta() public {
_configureSources(false);
_seedVwapAtTick(0);
mockPool.setCurrentTick(0);
// shortTwap - longTwap = 1000 ticks = MAX_MOMENTUM_TICKS → momentum = 1e18
mockPool.setTwapTicks(0, 1000); // longTwap=0, shortTwap=1000
optimizer.getLiquidityParams(); // must not revert
// 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)");
}
function testMomentumFullBearAtNegMaxDelta() public {
_configureSources(false);
_seedVwapAtTick(0);
mockPool.setCurrentTick(0);
// shortTwap - longTwap = -1000 = -MAX_MOMENTUM_TICKS → momentum = 0
mockPool.setTwapTicks(1000, 0); // longTwap=1000, shortTwap=0
optimizer.getLiquidityParams(); // must not revert
}
function testObserveRevertLeavesSlots34AsZero() public {
_configureSources(false);
_seedVwapAtTick(0);
mockPool.setCurrentTick(0);
mockPool.setRevertOnObserve(true); // triggers catch branch
// Must not revert — slots 3-4 remain 0 (calculateParams ignores them anyway)
optimizer.getLiquidityParams();
}
// =========================================================
// Slot 6: utilizationRate
// =========================================================
function testUtilizationRateInRange() public {
_configureSources(false);
// Set anchor position in range [100, 100]; current tick = 0 → in range → 1e18
mockLm.setPosition(ANCHOR, 1e18, -100, 100);
mockPool.setCurrentTick(0);
mockPool.setRevertOnObserve(true);
optimizer.getLiquidityParams(); // must not revert
// 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");
}
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";