Tick / Liquidity Desynchronization

  • Name: Tick / liquidity desynchronization
  • Category: exploit pattern / concentrated-liquidity AMM accounting failure / precision-loss boundary bug
  • Summary: This exploit family breaks concentrated-liquidity AMMs when the protocol’s price state, current tick, and active liquidity stop agreeing about whether a tick boundary has been crossed. The dangerous version is not just rounding error in the abstract. It is a boundary-state desynchronization where swap math decides a tick was not crossed, but the final computed price lands beyond that tick anyway. Once that happens, a later reverse swap can activate liquidity that should already be active, count the same liquidity twice, or trade against liquidity that is only present on paper.
  • What it is:
    • Concentrated-liquidity AMMs track liquidity in tick ranges and change active liquidity only when swaps cross tick boundaries.
    • The protocol therefore relies on a tight invariant: currentSqrtP, currentTick, and active liquidity must describe the same side of every relevant boundary.
    • If custom swap math computes a final price slightly past a target tick while the control flow still believes the tick was not crossed, the state becomes internally inconsistent.
    • That inconsistency is especially dangerous when the protocol also maintains custom liquidity layers such as fee-reinvestment liquidity, linked-list tick traversal, or custom boundary helpers.
    • A follow-up swap in the opposite direction can then apply a boundary update that should already have happened, effectively double-adding or otherwise mis-accounting liquidity.

Three boundary-state promotions reviewers should not collapse

  1. Price geometry moved near or even beyond a boundary

    • A swap-step computation produced a nextSqrtP, implied tick, or other boundary-relative position very close to or slightly past the next initialized tick.
    • That is only a geometric fact about one arithmetic output. By itself it is not yet the same thing as the engine cleanly changed semantic boundary state or new liquidity is now safely spendable.
  2. The engine published one semantic answer about which side of the boundary is active

    • The protocol decided whether the tick was crossed, which liquidity set is active, and how custom liquidity overlays should now be interpreted.
    • This is already a stronger claim than price landed near or beyond the boundary, but it is still not automatically the same thing as downstream paths may already trust and spend against that answer.
  3. Later swap, reverse-path, and accounting consumers may spend against that boundary answer

    • The next step, the reverse swap, fee path, or any other money-moving consumer now honors the freshly published boundary state as real liquidity truth.
    • This is where the family actually cashes out. The pool did not merely compute or publish an incoherent boundary answer; it let a later path monetize that answer before any fail-closed check stopped settlement.

The recurring review target is to answer three separate questions cleanly: where did price land geometrically?, what exact semantic boundary state did the engine publish?, and which downstream paths were allowed to spend against that publication?

  • Typical preconditions:
    • The AMM is Uniswap-v3-like or otherwise tick-based, so swaps and liquidity activation are boundary-sensitive.
    • The pool uses custom price-update math, fee-reinvestment logic, or other nontrivial modifications beyond vanilla Uniswap v3.
    • The branch that decides whether a tick is crossed is separate from the branch that computes the final post-swap price.
    • Precision loss, rounding-direction mistakes, or an off-by-one boundary condition can make nextSqrtP overshoot the target while the code still treats the tick as un-crossed.
    • The attacker can move the pool into a thin-liquidity or zero-base-liquidity region, then seed a narrow attacker-controlled tick range where exact boundary behavior matters.
  • Common vulnerable surfaces:
    • concentrated-liquidity AMMs with custom computeSwapStep math
    • fee auto-compounding or reinvestment-liquidity overlays that modify swap equations
    • boundary-crossing helpers that depend on nextTick == currentTick, nearestCurrentTick, or similar cached state
    • pools that can sit in a vacuum zone with baseL = 0 while auxiliary liquidity terms remain nonzero
    • reverse-swap paths that assume earlier boundary transitions were applied correctly
  • Exploit sequence:
    1. The attacker pushes price into a zone with little or no active base liquidity so they can control the next meaningful boundary.
    2. The attacker mints a narrow liquidity position around the chosen tick range and may partially remove liquidity until the pool reaches an exact desired state.
    3. The attacker performs a carefully sized swap that lands just below the amount nominally required to cross the target tick.
    4. Due to a rounding-direction or boundary-state bug, the pool decides the tick was not crossed, but the computed final price actually lands beyond the target boundary.
    5. The pool now has inconsistent state: tick/price/liquidity no longer agree about which liquidity should already be active.
    6. The attacker immediately swaps in the opposite direction, triggering boundary-update logic that adds liquidity again or otherwise lets the pool trade against fictitious depth.
    7. The attacker exits with more of the valuable asset than honest pool accounting should allow, often financing the whole cycle with flash liquidity.
  • Variants:
    • Rounding-direction overshoot — the code intends to round one intermediate value up to keep final price conservative, but rounds down instead and lets the final price overshoot the boundary.
    • Same-tick reverse-cross bug — after the inconsistent state is created, a reverse swap treats the current tick as the next tick and applies a boundary transition that should already have happened.
    • Double-added liquidity — the most explicit profit path: liquidity is already active from minting, then boundary crossing adds it again.
    • Vacuum-zone staging — the attacker first moves the pool into a no-base-liquidity region so the only meaningful liquidity near the boundary is attacker-shaped.
    • Custom-liquidity-layer desync — reinvestment liquidity, fee liquidity, or another synthetic layer participates in price math differently from base liquidity updates, widening the gap between state variables.
  • Detection heuristics:
    • Assert the core invariant directly: if the code decides a target tick is not crossed, nextSqrtP must remain on the same side of that boundary.
    • Fuzz around amountToReachTick - 1, amountToReachTick, and amountToReachTick + 1; many exploitable states live exactly there.
    • Differential-test custom CLAMM math against a simpler reference model that enforces one source of truth for tick crossing.
    • Simulate swaps when baseL = 0 or near-zero but auxiliary liquidity terms remain positive.
    • Stress multi-sub-swap paths where the first sub-step crosses a boundary and a later dust-sized sub-step leaves sqrtP unchanged. KyberSwap Elastic Legacy showed that unchanged price but recomputed tick can be enough to create a one-tick boundary lie.
    • Reverse the swap immediately after every near-boundary state in tests; many desynchronization bugs only monetize on the way back.
    • Track whether currentTick, nextTick, and price-derived tick ever collapse into contradictory values inside one transaction.
  • Defenses:
    • Keep a single authoritative rule for boundary crossing and final price consistency; do not let control-flow and price math disagree.
    • Use conservative rounding that favors under-reaching a boundary when swap logic depends on whether the boundary was crossed.
    • Add explicit invariant checks after each swap step and revert if price, tick, and active liquidity disagree.
    • Differential-test any Uniswap-v3 modification, especially fee-reinvestment or auto-compounding logic, against adversarial ±1 wei boundary cases.
    • Stress-test vacuum-zone states, narrow attacker-controlled ranges, and reverse-swap sequences, not just ordinary forward swaps.
    • Treat every custom liquidity layer as part of the same accounting surface; if it affects price math, it must be reflected safely in crossing logic too.

What sound design looks like

  • Observation, semantic boundary publication, and spend authority remain distinct until the engine can fuse them safely. A healthy CLAMM does not treat price moved near or beyond a boundary, one crossed/not-crossed answer was published, and later flow may already spend against that answer as real liquidity truth as the same event unless its invariants prove they can collapse into one coherent state transition.
  • Price, current tick, and active liquidity are updated as one fused security object. The engine cannot persist a state where one of them says crossed and the others still say not crossed, because that disagreement is already tradable fictitious depth.
  • Boundary crossing and final-price computation share one authoritative notion of geometry. The code that decides whether a tick transition happened is the same logic that constrains where nextSqrtP may end up, so overshoot without crossing is structurally impossible rather than informally discouraged.
  • Custom liquidity layers remain subordinate to the same boundary truth as base liquidity. Reinvestment, fee, hook, or auto-compounding liquidity may influence pricing only if it also preserves the exact activation and deactivation rules the swap engine relies on.
  • Reverse-path safety is a design requirement, not an afterthought. Every forward step is assumed to be immediately challenged by an adversarial reverse swap, and no legitimate sequence can make a previously impossible liquidity state appear on the way back.
  • Impossible geometry fails closed in production. If a custom CLAMM cannot prove that a boundary state is coherent, it reverts rather than trying to continue through a partially updated ledger.
  • The healthy outcome is boring: at any moment, a reviewer can point to one sentence that explains why did not cross also guarantees did not activate new liquidity and did not move price beyond the boundary.

Failure-prevention checklist

  • Write the boundary invariant down in executable form: if the engine concludes target tick not crossed, then nextSqrtP, currentTick, and post-step active liquidity must all still describe the same side of that boundary.
  • Make one source of truth responsible for both did we cross? and what is the resulting price?. If one branch computes price and another separately decides whether liquidity updates apply, you already have a desynchronization seam.
  • Treat every custom layer added to a Uniswap-v3-style fork — reinvestment liquidity, fee liquidity, linked-list tick traversal, hook-adjusted math, custom fee curves — as a fresh solvency surface that must preserve vanilla boundary invariants.
  • Fuzz amountToCross - 1, amountToCross, and amountToCross + 1 at many ticks, many liquidity densities, and both swap directions. Boundary bugs often live exactly one wei away from the branch point developers mentally test.
  • Include vacuum-zone scenarios where base liquidity is zero or nearly zero, because those are the states where a tiny boundary mistake can promote attacker-shaped liquidity from narrow-range staging into fake global depth.
  • Test immediate reversal after every near-boundary step. A CLAMM that only looks broken on the return path is still broken.
  • Differential-test the full step function against a simpler reference model that enforces price/tick/liquidity coherence, even if the production code is more optimized.
  • Revert on impossible internal states rather than trying to continue. A temporary tick/price/liquidity disagreement is not a recoverable bookkeeping oddity; it is spendable fictitious liquidity.
  • Require new CLAMM features to come with explicit proofs or adversarial tests for monotonicity, non-overshoot, and no-double-activation properties before launch.

Audit prompts

  • Where exactly is the protocol’s canonical statement of crossed versus not crossed, and can any downstream helper compute a final price that contradicts it?
  • If one swap decomposes into multiple sub-swaps, can an intermediate boundary crossing be partially committed and then followed by a dust-sized step that leaves sqrtP unchanged but still refreshes currentTick, nextTick, or another boundary cursor from the boundary price?
  • For every arithmetic step near a target tick, which direction does rounding move nextSqrtP, and does that direction remain conservative for exact-in, exact-out, and both token directions?
  • Can the pool ever end a step with price > target boundary while the state machine still believes the earlier tick-cross side effects were not applied?
  • If base liquidity is zero and only reinvestment or auxiliary liquidity remains, does the swap engine still preserve the same boundary invariants it preserves in ordinary deep-liquidity states?
  • What happens if the attacker mints a very narrow range, partially withdraws, lands exactly one wei short of crossing, then immediately reverses the swap? Does any path double-activate or double-count liquidity?
  • Are currentTick, price-derived tick, cached next tick, and active-liquidity deltas all derived from one coherent notion of boundary position, or can they diverge inside one transaction?
  • Does any custom fee, reinvestment, hook, or compounding layer participate in price math without participating identically in crossing and activation logic?
  • Have custom CLAMM modifications been differential-tested against upstream Uniswap-v3 behavior for adversarial boundary states, or only for normal-volume swap correctness?
  • If an invariant check fails mid-step, does the pool safely revert, or can it persist a partially updated state that a reverse swap can monetize?
  • Which tests prove that a swap followed immediately by its reverse cannot manufacture value purely from boundary handling and rounding?

Lessons that should already be standard

  • Price touched the boundary, the engine published one semantic crossed/not-crossed answer, and later flow may already spend against that answer are different claims. Healthy CLAMMs keep them separate until one coherent state transition proves they can safely collapse.

  • In a concentrated-liquidity AMM, price, current tick, and active liquidity are one security object. If they can disagree even briefly, the pool can trade against liquidity that does not exist.

  • Precision loss is often too vague to be useful. The real question is whether a one-wei error can move the state machine onto the wrong side of a boundary and thereby activate the wrong liquidity.

  • Uniswap-v3 forks do not inherit safety just because the design is familiar. The moment a fork adds reinvestment curves or custom step math, it inherits the obligation to re-prove boundary coherence.

  • Reverse-swap tests are first-class security tests, not nice-to-have regression coverage. Many boundary-state bugs only cash out on the way back.

  • Zero-liquidity or near-zero-liquidity zones are solvency-critical adversarial states, not pathological edge cases to hand-wave away in review.

  • A CLAMM should prefer under-reaching a boundary to overshooting it silently. False negatives are painful; false positives can mint fictitious depth.

  • Any design that lets auxiliary liquidity affect price but not crossing logic is creating two ledgers for the same market state.

  • If the implementation cannot explain in one sentence why not crossed implies price stayed on the same side, the audit burden is not finished.

  • Boundary-state invariant checks belong in production code for custom AMMs. Reverting on impossible geometry is cheaper than funding the lesson on mainnet.

  • High-signal incidents:

    • tick-boundary desynchronization near miss (2023-04-17) — the best prevented-case precursor in this corpus: a whitehat showed that exact tick-boundary bookkeeping drift could make freshly minted liquidity get added twice, letting a flash-loan attacker drain a legacy Elastic pool if deposits had not been disabled first.
    • tick-liquidity desynchronization exploit (2023-11-22) — the canonical live exploit: incorrect rounding in reinvestment-liquidity math let a precisely sized swap overshoot a target tick without triggering a cross, so the reverse swap counted liquidity twice and drained multiple multichain pools.

Internal linkages

Control surface

  • The leverage sits in boundary-crossing logic, rounding direction, custom swap-step math, and the agreement between price state and active liquidity.
  • A CLAMM does not fail because one division rounded the wrong way in isolation. It fails because a one-wei boundary mistake can let the pool believe and trade against liquidity that should not exist.

Sources

Last reviewed

  • 2026-07-31 UTC