KyberSwap Elastic Reinvestment Rounding / Tick-Liquidity Desynchronization Exploit (2023-11-22)

  • Name: KyberSwap Elastic reinvestment rounding / tick-liquidity desynchronization exploit
  • Date: 2023-11-22
  • Protocol: KyberSwap Elastic
  • Chain: multichain (Ethereum, Optimism, Arbitrum, Polygon, Base, Avalanche, and others)
  • Category: concentrated-liquidity AMM accounting failure / precision-loss boundary bug / liquidity double-counting exploit
  • Loss: about 56.2 million of affected assets including temporarily inaccessible balances
  • Summary: Attackers exploited KyberSwap Elastic’s custom reinvestment-liquidity math so a carefully sized swap could make the pool’s final sqrtP land beyond a target tick even though the swap code concluded that the tick had not been crossed. That desynchronized price, tick, and active liquidity. A reverse swap then crossed the same boundary in the opposite direction and counted liquidity that should already have been active, letting the attacker trade against fictitious depth and drain concentrated-liquidity pools across multiple chains. The exploit path was not a generic flash-loan oracle move; it was a CLAMM boundary-state corruption caused by wrong rounding direction inside computeSwapStep().
  • Trigger / entry point:
    • The vulnerable surface sat in KyberSwap Elastic’s custom concentrated-liquidity pool math, specifically SwapMath.computeSwapStep() and the reinvestment-liquidity path layered on top of Uniswap-v3-style tick logic.
    • The critical bug was in estimateIncrementalLiquidity(): deltaL was supposed to be rounded up so the derived final price stayed conservative, but the implementation used floor rounding instead.
    • That meant the non-crossing branch of computeSwapStep() could return a nextSqrtP slightly above the target tick price even though control flow still believed the target tick had not been crossed.
    • Once that happened, KyberSwap’s later boundary-update logic could apply liquidity changes on the reverse swap as if the pool were crossing into a range for the first time, effectively double-counting liquidity.
  • Root cause:
    • KyberSwap Elastic modified Uniswap-v3-style swap math with a reinvestment curve that let fee-derived liquidity participate in swap calculations.
    • In the exact-input non-crossing branch, the protocol needed conservative rounding so the final price stayed on the correct side of the target tick.
    • Instead, deltaL was rounded down, which rounded the computed final price up.
    • That broke the core CLAMM invariant that currentSqrtP, currentTick, and active liquidity must agree about whether a boundary has been crossed.
    • The exploit therefore was not merely precision loss in the abstract; it was precision loss that corrupted tick-crossing state and made nonexistent liquidity temporarily tradeable.
  • Exploit path:
    1. The attacker used flash liquidity to push a victim pool into a region where there was effectively no base liquidity at the current price, leaving a controllable vacuum zone near the next active tick range.
    2. The attacker minted a narrow position around the chosen tick range, then partially removed liquidity until pool state and attacker-controlled liquidity matched a very specific target.
    3. The attacker executed a carefully sized swap that was exactly below the nominal amount required to cross the target tick. In the Ethereum ETHx/USDC example from Kyber’s postmortem, usedAmount was 244080034447360000000 while the attacker supplied 244080034447359999999, one wei less.
    4. Because of the wrong rounding direction in reinvestment-liquidity math, computeSwapStep() treated the swap as non-crossing even though the final computed nextSqrtP ended up slightly beyond the target tick.
    5. The pool now had inconsistent internal state: the price-derived tick said one thing, but the active-liquidity update path had not applied the corresponding boundary transition.
    6. The attacker immediately swapped in the opposite direction. That reverse swap invoked cross-tick logic as though the pool were entering the liquidity range for the first time, so the position’s liquidity was effectively added again.
    7. With the pool now believing it had much more active liquidity than it really did, the attacker swapped out large amounts of the valuable asset at an impossible price and drained the pool.
    8. The same pattern was repeated across multiple KyberSwap Elastic pools and chains, while front-run and copycat bots also copied the exploit path.
  • What the attacker needed to be true:
    • The pool had to use the vulnerable Elastic reinvestment-liquidity code path rather than a safer vanilla implementation.
    • The attacker had to be able to move the current price into a low-liquidity or zero-base-liquidity region where a narrow attacker-defined range controlled the next meaningful boundary behavior.
    • Boundary-crossing control flow had to remain separate from the final-price calculation so rounding could make them disagree.
    • The reverse-swap path had to trust the corrupted state and apply liquidity updates as if the earlier boundary had not already been passed.
    • The attacker needed enough flash liquidity and precision to shape the boundary state exactly; this was a highly parameterized exploit, not a blunt drain.
  • Technical anatomy worth remembering:
    • The decisive bug was not in an oracle, external callback, or governance path. The protocol’s own swap-step math manufactured false liquidity.
    • The important invariant was: if the code decides a tick is not crossed, the final price must stay on the correct side of that tick. Kyber violated that invariant.
    • Kyber’s reinvestment curve mattered because fee-derived liquidity participated in price math, so rounding deltaL the wrong way could move nextSqrtP past the boundary.
    • In BlockSec’s Ethereum frxETH/WETH walk-through, the attacker first pushed the pool into a no-base-liquidity zone, then used a narrow range and a swap of 387.170 WETH for 0.06 frxETH to create the boundary mismatch, and finally used a reverse swap where only 3 wei of frxETH were needed to reach the tick that had effectively already been crossed.
    • The exploit is a clean example of how CLAMMs can fail when boundary state, price state, and active-liquidity state are allowed to diverge even slightly.

Pattern tags

  • liquidity desynchronization

  • roundtrip non-invariance

  • Representative transactions and addresses:

    • Example Ethereum attack tx from Kyber’s postmortem: 0x396a83df7361519416a6dc960d394e689dd0f158095cbc6a6c387640716f5475
    • Example Ethereum attack tx from BlockSec’s detailed walk-through: 0x485e08dc2b6a4b3aeadcb89c3d18a37666dc7d9424961a2091d6b3696792f0f3
    • Primary exploiter address tracked by Kyber: 0x50275E0B7261559cE1644014d4b78D4AA63BE836
    • Example vulnerable Ethereum pool used in BlockSec’s analysis: 0xfd7b111aa83b9b6f547e617c7601efd997f64703
    • Example ETHx/USDC pool from Kyber’s transaction-level explanation: 0xcbec1e9407f1910c86f261eaeac27d85c0479e8c

Three CLAMM-boundary promotions this incident collapsed

  1. A near-crossing boundary state existed in a vacuum-zone pool configuration

    • The attacker first pushed victim pools into regions with little or no base liquidity, then installed narrow ranges so the next important tick transition was dominated by attacker-shaped state.
    • At this stage the strongest true fact was only one highly fragile near-boundary pool state exists and one more swap may matter, not the pool has already earned a new active-liquidity regime.
  2. Kyber promoted that fragile local state into canonical pool truth even though price, tick, and active-liquidity answers had already diverged

    • The decisive failure happened when computeSwapStep() took the non-crossing branch yet still returned a nextSqrtP beyond the target boundary because reinvestment-liquidity rounding moved the final price too far.
    • That meant the pool quietly promoted almost crossed into canonical current state is coherent enough for downstream logic, even though sqrtP, derived tick, and active-liquidity activation semantics no longer described the same boundary reality.
  3. That accepted but incoherent state still inherited immediate reserve-spend authority on the reverse path

    • Once the reverse swap ran, Kyber still let the crossing logic consume the corrupted state as if the boundary were being entered for the first time and the narrow-range liquidity had not already become active.
    • In practice the protocol let one arithmetic disagreement become spendable fiction: the pool sold real assets against liquidity depth that only existed because the earlier state publication had been wrong.
  • Response / mitigation:
    • KyberSwap suspended liquidity additions across Elastic pools on all affected chains to stop further exploitation.
    • The team published onchain messages offering exploiters a 10% bounty for fund return and negotiated partial returns from some front-run bot operators.
    • Kyber later reported about $5.7 million recovered from front-run bots and additional recovery of most locked assets that had become inaccessible in affected pools.
    • The protocol ultimately used a treasury grant program to make affected users whole after the incident.

Why the exploit worked

  • Kyber let one geometric fact, one semantic boundary fact, and one spend-right fact collapse into one unchecked step. A swap that was supposed to be not crossed was still allowed to leave nextSqrtP beyond the target boundary, and later paths were allowed to spend against that incoherent state immediately.
  • The reinvestment-liquidity branch did not merely round the wrong way locally; it published a false answer about which liquidity set should already be active. Once the reverse path honored that answer, the pool could sell real assets against liquidity that only existed on paper.
  • The exploit staging mattered because the attacker first created a vacuum zone with little or no base liquidity, then installed a narrow attacker-shaped range at the next important boundary. That made a one-wei boundary error economically meaningful instead of harmless dust.
  • Kyber’s custom Elastic math was the actual trust boundary. The exploit did not need governance failure, compromised keys, or a bad external feed; it only needed the pool’s own step engine to disagree with itself about crossed versus not crossed for one spendable state transition.

Detection and audit pivots

  • Treat amountToCross - 1, amountToCross, and amountToCross + 1 as explicit boundary-state test vectors, not as generic fuzzing residue. Kyber’s exploitable state lived exactly at the edge where one wei should have controlled whether the tick transition happened at all.
  • Reconstruct the full three-object boundary state after every near-crossing swap: nextSqrtP, derived currentTick, and active-liquidity set. If any test can produce price says crossed while liquidity-update logic says not crossed, the pool is already publishing spendable fiction.
  • Force reverse-path monetization tests, not just forward-path sanity checks. The profitable step in Kyber was almost-cross on the way in, then collect double-counted liquidity on the way back, so a review that only checks one directional step misses the actual payout surface.
  • Deliberately stage vacuum-zone and near-zero-base-liquidity pool states in audit harnesses. Kyber’s bug became economically meaningful because the attacker first moved the pool into a region where a narrow attacker-controlled range dominated the next boundary transition.
  • Diff every custom helper layer against the underlying CLAMM boundary truth. If reinvestment or fee-derived liquidity influences computeSwapStep() but crossing and activation semantics are maintained somewhere else, reviewers should assume the protocol now has two ledgers until proven otherwise.
  • Add production monitoring for impossible geometry, not just abnormal volume: did not cross decisions followed by post-swap price beyond the target tick, unusually tiny reverse-swap input needed to trigger a boundary, or sudden active-liquidity jumps after an almost-crossing swap should all page operators immediately.

Why this incident matters

  • KyberSwap Elastic is the clean live-fire example of a concentrated-liquidity AMM failing because of boundary-state desynchronization rather than because of a simpler reserve or oracle bug.
  • It shows why precision loss can be a misleadingly soft label: the real failure was that a one-wei rounding mistake let the pool trade against liquidity that should not have existed.
  • It also demonstrates the audit burden of Uniswap-v3 modifications. Once a fork adds reinvestment curves, custom tick traversal, or new liquidity layers, the boundary invariants have to be re-proved rather than assumed.

Failure-prevention checklist

  • Encode the CLAMM boundary invariant directly in code and tests: if computeSwapStep() concludes the target tick was not crossed, the returned nextSqrtP, derived tick, and active-liquidity view must all remain on the same side of that boundary.
  • Keep crossing logic and price math fused. Kyber’s bug existed because the branch deciding crossed vs not crossed could disagree with the final-price calculation after reinvestment-liquidity rounding.
  • Differential-test every custom Uniswap-v3 modification against a simpler reference model, especially around amountToCross - 1, amountToCross, and amountToCross + 1 cases where a one-wei change should deterministically control whether the tick transition happens.
  • Stress-test vacuum-zone states where base liquidity is zero or near zero but reinvestment or fee-derived liquidity still influences price math. Those are not pathological tails; they are where boundary mistakes become spendable fictitious depth.
  • Treat immediate reverse swaps as a first-class security test. If a forward step can be monetized only on the way back, the pool is still broken.
  • Require any auxiliary liquidity layer that participates in price math to preserve the same activation semantics as base liquidity. Price sees it and crossing logic sees it cannot be separate ledgers.
  • Revert on impossible geometry instead of tolerating temporary desynchronization. In a CLAMM, a brief disagreement between price, tick, and active liquidity is already a solvency failure.
  • Keep chain-wide and pool-level kill switches operationally ready. Multichain CLAMM incidents spread fast once a reusable boundary-state corruption recipe exists.

Audit prompts

  • If the swap engine returns did not cross, what exact proof shows the computed nextSqrtP cannot already lie beyond the target tick?
  • Which arithmetic steps in reinvestment-liquidity or fee-liquidity math choose a rounding direction, and is each direction still conservative for both swap directions and both exact-in / exact-out semantics?
  • Can the pool be pushed into a baseL = 0 or near-zero-base-liquidity zone where attacker-controlled narrow ranges dominate the next meaningful boundary transition?
  • What happens at amountToCross - 1, amountToCross, and amountToCross + 1 for the same pool state, and do price, current tick, next tick, and active liquidity stay coherent in all three cases?
  • If an attacker mints a narrow position, partially withdraws, executes the almost-crossing swap, and immediately reverses it, can any path double-add liquidity or otherwise honor liquidity that should already have been active?
  • Do custom helper layers reuse the same boundary truth as the underlying CLAMM, or can reinvestment liquidity influence price without participating identically in crossing and activation logic?
  • What production invariant checks or runtime circuit breakers detect price/tick/liquidity disagreement before a second swap can monetize it?
  • On a multichain deployment, how quickly can operators disable new liquidity, swaps, or specific pools once one chain proves the exploit recipe is live?

Lessons that should already be standard

  • Precision loss is often an evasive description. The real question is whether the rounding mistake can put the state machine on the wrong side of a boundary and thereby print tradeable liquidity.
  • In a CLAMM, price, tick, and active liquidity are one security object. If they can disagree even briefly, the pool can sell assets against depth that does not exist.
  • Uniswap-v3 familiarity is not inherited safety. Every reinvestment layer, fee overlay, or traversal optimization reopens the need to prove boundary coherence from scratch.
  • Reverse-path tests are mandatory. Kyber’s drain only paid because the corrupted forward step was immediately consumable by a reverse swap that re-applied liquidity updates.
  • Zero-base-liquidity and near-empty states are adversarial staging grounds, not weird corners to ignore in review.
  • Multichain deployment multiplies math bugs into operational races. Once one pool shows the recipe, pausing and blast-radius controls matter almost as much as understanding the arithmetic.

Internal linkages

Control surface

  • The decisive leverage sat in KyberSwap Elastic’s custom computeSwapStep() logic, the rounding direction of reinvestment-liquidity math, and the boundary-update path that trusted the corrupted state on the reverse swap.
  • Kyber did not just have a tiny math bug. It had a solvency-critical state machine where price, tick, and active liquidity could briefly disagree — and in a CLAMM, that is enough to print spendable depth out of thin air.

Sources

Last reviewed

  • 2026-08-04 UTC