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 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-14 23:21:30 +00:00
parent cbd0ada9d7
commit ac4aa745f6
3 changed files with 14 additions and 19 deletions

View file

@ -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");
}
}