// 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";