fix(PositionPlanner): cap liquidity by per-tick headroom instead of a plan-wide budget#165
Open
EperezOk wants to merge 1 commit into
Open
Conversation
…wide budget resolve() bounded V4's per-tick maxLiquidityPerTick with a single plan-wide budget decremented after each position, conservatively assuming all positions share one tick boundary. That skipped positions V4 would accept, squeezing out one-sided and laddered layouts. liquidityGross only accrues at a position's two boundary ticks, so positions compete only when they share an exact boundary. Track gross per boundary and cap each candidate (and the full-range fallback) to maxLiquidityPerTick minus the gross already at its tighter boundary. Positions sharing no boundary now each get full per-tick headroom. The guarantee that no mint exceeds the cap on a fresh pool is preserved (proof in _headroom NatSpec: maxGross <= cap by induction, so no underflow), and degradation stays graceful (shrink/skip, never revert). Adds a RED-GREEN test for the independent-boundary case and updates the clamp characterization test to the new (intended) behavior. Worst-case planning overhead at 10 positions ~ +26.7k gas; standard 2-position plan ~ +2.3k gas (< 0.3% of an ~830k-gas migration). Measured A/B vs the prior implementation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PositionPlanner.resolvebounded Uniswap v4's per-tickmaxLiquidityPerTickwith a single plan-wide budget decremented after each position, conservatively treating the whole plan as if every position shared one tick boundary (which skips positions v4 would accept).liquidityGrossper boundary tick and caps each position (and the implicit full-range fallback) to the headroom remaining at its own boundaries — matching v4's actual per-tick rule.Background
In v4,
maxLiquidityPerTickis enforced per tick against that tick'sliquidityGross, andliquidityGrossonlyaccrues at a position's two boundary ticks (
tickLower/tickUpper). Two positions therefore compete for the cap only when they share an exact boundary; positions on distinct ticks are independent and may each hold up tomaxLiquidityPerTick.The previous guard collapsed this into one plan-wide budget (
Σ(all plan liquidity) ≤ maxLiquidityPerTick). It guaranteed no mint could overflow a tick on a fresh pool, but at the cost of dropping valid positions.Change
_headroom(...)sums theliquidityGrossalready contributed by created positions at each of a candidate's twoboundaries and returns
maxLiquidityPerTick − max(grossLower, grossUpper). Each candidate (and the fallback) is capped to that value before its liquidity is quoted.Properties preserved:
maxLiquidityPerTick(by induction, somaxLiquidityPerTick − maxGrosscan't underflow). Enforcing the per-tick gross bound is also what keeps the active-liquidityuint128accumulator from overflowing.resolvestill can't underflow.Behavior change
Positions on independent boundaries now each receive their own per-tick headroom instead of competing for one budget, so plans that previously had positions silently dropped will now mint them. The existing
test_resolve_clampsPositionsAboveMaxLiquidityPerTickcharacterization test was updated to reflect this (a clamped explicit position now coexists with the full-range fallback rather than starving it).Gas
Measured A/B of
resolve(old vs new) on identical inputs where neither version clamps, isolating the per-tick scan cost:End-to-end, the standard 2-position migration snapshot moves 803,742 → 806,053 (+2,311 gas, < 0.3%); the worst case (10 positions) adds ~+26.7k to an ~830k-gas migration (~3%). The
LBPStrategy_E2E_Test.jsongas snapshot is updated accordingly.