Merge pull request 'fix: fix: remove MAX_ANCHOR_WIDTH clamp in ThreePositionStrategy (#783)' (#785) from fix/issue-783 into master

This commit is contained in:
johba 2026-03-15 00:49:05 +01:00
commit fb92beea9d
4 changed files with 15 additions and 19 deletions

View file

@ -33,3 +33,4 @@
- [2026-03-14] bootstrap.sh anvil_setCode guard now targets correct feeDest 0xf6a3... (#760)
- [2026-03-14] llm_contrarian.push3 AW=150/250 clamped to 100 — three rounds unaddressed (#756)
- [2026-03-14] bootstrap.sh hardcodes BASE_SEPOLIA_LOCAL_FORK even on mainnet forks (#746)
- [2026-03-14] remove MAX_ANCHOR_WIDTH clamp in ThreePositionStrategy (#783)

View file

@ -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()

View file

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

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