From ac4aa745f69908381cb3897fa98e0d19aa56af04 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 14 Mar 2026 23:21:30 +0000 Subject: [PATCH] fix: fix: remove MAX_ANCHOR_WIDTH clamp in ThreePositionStrategy (#783) Remove the MAX_ANCHOR_WIDTH=100 constant and the corresponding clamp on anchorWidth in LiquidityManager.recenter(). The optimizer is now free to choose any anchor width; evolution run 7 immediately exploited AW=153. Update IOptimizer.sol NatSpec to reflect no clamping. Update the testAnchorWidthAbove100IsClamped test to testAnchorWidthAbove100IsNotClamped, asserting the tick range matches the full AW=150 width. Co-Authored-By: Claude Sonnet 4.6 --- onchain/src/IOptimizer.sol | 2 +- onchain/src/LiquidityManager.sol | 6 +----- onchain/test/LiquidityManager.t.sol | 25 ++++++++++++------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/onchain/src/IOptimizer.sol b/onchain/src/IOptimizer.sol index d7dc264..ccb68d5 100644 --- a/onchain/src/IOptimizer.sol +++ b/onchain/src/IOptimizer.sol @@ -22,7 +22,7 @@ interface IOptimizer { * @notice Returns the four liquidity parameters used by LiquidityManager.recenter(). * @return capitalInefficiency Capital buffer level (0..1e18, clamped to MAX_PARAM_SCALE). CI=0 is safest. * @return anchorShare Fraction of non-floor ETH in anchor (0..1e18, clamped to MAX_PARAM_SCALE). - * @return anchorWidth Anchor position width in tick units (uint24); max 100 ticks (MAX_ANCHOR_WIDTH), enforced by LiquidityManager. + * @return anchorWidth Anchor position width in tick units (uint24); passed through to ThreePositionStrategy without clamping. * @return discoveryDepth Discovery liquidity density (0..1e18, clamped to MAX_PARAM_SCALE). */ function getLiquidityParams() diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 0daccf9..baa56ef 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -34,10 +34,6 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { /// @notice Uniswap V3 fee tier (1%) - 10,000 basis points uint24 internal constant FEE = uint24(10_000); - /// @notice Maximum anchor width (in ticks) accepted from the optimizer. - /// Any optimizer-returned value above this ceiling is silently clamped down. - uint24 internal constant MAX_ANCHOR_WIDTH = 100; - /// @notice Upper bound (inclusive) for scale-1 optimizer parameters: capitalInefficiency, /// anchorShare, and discoveryDepth. Values above this ceiling are silently clamped. uint256 internal constant MAX_PARAM_SCALE = 10 ** 18; @@ -208,7 +204,7 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { PositionParams memory params = PositionParams({ capitalInefficiency: (capitalInefficiency > MAX_PARAM_SCALE) ? MAX_PARAM_SCALE : capitalInefficiency, anchorShare: (anchorShare > MAX_PARAM_SCALE) ? MAX_PARAM_SCALE : anchorShare, - anchorWidth: (anchorWidth > MAX_ANCHOR_WIDTH) ? MAX_ANCHOR_WIDTH : anchorWidth, + anchorWidth: anchorWidth, discoveryDepth: (discoveryDepth > MAX_PARAM_SCALE) ? MAX_PARAM_SCALE : discoveryDepth }); diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index 174c472..1957cee 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -1118,17 +1118,16 @@ contract LiquidityManagerTest is UniSwapHelper { } /** - * @notice Optimizer returning anchorWidth > 100 is clamped to MAX_ANCHOR_WIDTH (100), not rejected. - * @dev Verifies that LiquidityManager silently truncates oversized anchorWidth values from - * the optimizer rather than reverting. The resulting anchor position tick range must - * equal exactly 100 * TICK_SPACING ticks wide. + * @notice Optimizer returning anchorWidth > 100 is passed through unchanged (no clamping). + * @dev Verifies that LiquidityManager does NOT clamp oversized anchorWidth values from + * the optimizer. The resulting anchor position tick range must match anchorWidth=150 exactly. */ - function testAnchorWidthAbove100IsClamped() public { - // Deploy a ConfigurableOptimizer that returns anchorWidth = 150 (above the 100-tick ceiling) + function testAnchorWidthAbove100IsNotClamped() public { + // Deploy a ConfigurableOptimizer that returns anchorWidth = 150 ConfigurableOptimizer highWidthOptimizer = new ConfigurableOptimizer( 0, // capitalInefficiency = 0 (safest) 3e17, // anchorShare = 30% - 150, // anchorWidth = 150 — intentionally above MAX_ANCHOR_WIDTH + 150, // anchorWidth = 150 3e17 // discoveryDepth = 30% ); @@ -1146,20 +1145,20 @@ contract LiquidityManagerTest is UniSwapHelper { address(highWidthOptimizer) ); - // recenter() must succeed (value clamped, not rejected) + // recenter() must succeed vm.prank(RECENTER_CALLER); customLm.recenter(); - // Anchor position must exist and its tick range must match anchorWidth=100 (clamped), not 150. + // Anchor position must exist and its tick range must match anchorWidth=150 (not clamped to 100). // The anchor spacing formula is: anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100) // For anchorWidth=100: anchorSpacing = 200 + 34*100*200/100 = 7000; tickWidth = 2*7000 = 14000 // For anchorWidth=150: anchorSpacing = 200 + 34*150*200/100 = 10400; tickWidth = 2*10400 = 20800 (uint128 liquidity, int24 tickLower, int24 tickUpper) = customLm.positions(ThreePositionStrategy.Stage.ANCHOR); assertTrue(liquidity > 0, "anchor position should have been placed"); int24 tickWidth = tickUpper - tickLower; - int24 expectedClamped = 2 * (TICK_SPACING + (34 * int24(100) * TICK_SPACING / 100)); // 14000 - int24 unclamped150 = 2 * (TICK_SPACING + (34 * int24(150) * TICK_SPACING / 100)); // 20800 - assertEq(tickWidth, expectedClamped, "anchorWidth above 100 must be clamped to MAX_ANCHOR_WIDTH=100"); - assertTrue(tickWidth < unclamped150, "clamped width must be narrower than unclamped anchorWidth=150 would produce"); + int24 expectedUnclamped = 2 * (TICK_SPACING + (34 * int24(150) * TICK_SPACING / 100)); // 20800 + int24 oldClamped100 = 2 * (TICK_SPACING + (34 * int24(100) * TICK_SPACING / 100)); // 14000 + assertEq(tickWidth, expectedUnclamped, "anchorWidth=150 must not be clamped - tick range must match 150"); + assertTrue(tickWidth > oldClamped100, "unclamped width must be wider than the old MAX_ANCHOR_WIDTH=100 range"); } }