KyberSwap Elastic (2023)
Smart contract security audit by TierZero · 2024-01-15
Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.
Findings summary
What the protocol did
KyberSwap Elastic is the concentrated-liquidity AMM in the Kyber Network stack, live on Ethereum and more than a dozen EVM chains since 2022. It follows the tick-based design popularized by Uniswap v3 — liquidity providers commit capital to discrete price ranges (ticks), and the pool tracks how much liquidity is active as the spot price crosses tick boundaries. KyberSwap's fork adds a feature Uniswap v3 doesn't have: automatic fee reinvestment. Instead of accumulating swap fees as separate claimable tokens, KyberSwap mints fees back into the position as additional liquidity through an internal reinvestment curve, compounding LP returns without a manual claim transaction. That reinvestment layer sits directly on top of the core tick-crossing accounting, and on 22–23 November 2023 an attacker used it to drain roughly $48–54 million from pools across Ethereum, Arbitrum, Optimism, Base, Polygon and other deployments in one coordinated operation.
The vulnerability
The bug class is tick-boundary liquidity double-counting. KyberSwap Elastic tracks two liquidity components per pool: base liquidity supplied directly by LPs, and a reinvestment-liquidity term derived from compounded fees. When a swap crosses a tick, the contract nets out the liquidityNet delta for that tick and folds the reinvestment liquidity back into the pool's active liquidity figure. The flaw was in how that fold-in was sequenced relative to tick crossings: under a specific, attacker-constructed sequence of swaps that walked price back and forth across a narrow, freshly-initialized tick range within a single transaction, the reinvestment liquidity got credited to the pool's active liquidity more than once for the same underlying fee accrual. The contract's internal state ended up believing it had more liquidity backing the current price than the token reserves actually supported.
Simplified illustration of the flawed pattern:
// simplified — not the actual KyberSwap source
function crossTick(int24 tick) internal {
LiquidityDelta memory d = ticks[tick].liquidityNet;
poolLiquidity += d.baseDelta;
// reinvestment liquidity folded in on every crossing,
// with no check for whether this tick's accrued fees
// were already reinvested earlier in the same call
poolLiquidity += reinvestmentLiquidity(tick);
}
Because the reinvestment credit wasn't idempotent per transaction, oscillating price across the same boundary let the attacker accumulate a liquidity balance in the contract's bookkeeping with no corresponding token backing.
How the exploit worked
- Flash-borrow a large amount of the pool's quote asset to gain price-moving power without committing capital.
- Mint a new, narrow-range liquidity position just outside the current tick, positioned so a small swap would immediately cross into it.
- Execute a precise sequence of swaps that walked the pool's active tick back and forth across that boundary, repeatedly re-triggering the reinvestment fold-in logic inside one transaction.
- Each crossing inflated the pool's recorded liquidity without a matching increase in actual reserves, because the same fee-derived liquidity was credited more than once.
- With the pool's accounting now overstating its backing, execute a final swap that the flawed logic priced as valid — extracting tokens far beyond what the real reserve balance supported.
- Repeat the pattern across every affected pool and chain, repaying the flash loans from the proceeds and keeping the difference.
Kyber's post-incident disclosure, and the attacker's subsequent on-chain messages demanding governance control of the DAO in exchange for returning funds, confirm the mechanism was deliberate and reproducible rather than a one-off rounding accident — the sequence was rehearsed on multiple pools before the pools were drained.
How an audit catches this
This is a state-invariant problem, not a syntax problem, so it's caught by testing the invariant rather than reading the function in isolation. The concrete checks we run on any concentrated-liquidity or tick-based AMM:
- Conservation-invariant fuzzing. Assert that
sum(token reserves)can always reconstruct tracked pool liquidity at the current price, across every reachable sequence of mint/burn/swap calls — not just the sequence the team's own tests exercise. Property-based fuzzing (Foundry invariant tests or Echidna) that specifically generates tick-oscillating, same-transaction swap sequences is what surfaces this bug class; unit tests that only swap in one direction never will. - Idempotency checks on fee and reinvestment credit. Any code path folding accrued fees or reinvestment yield back into active liquidity has to be checked for double-crediting when the same tick is crossed more than once per transaction or per block.
- Differential review against the upstream fork. KyberSwap Elastic diverged from Uniswap v3's audited tick-crossing logic specifically to add reinvestment — every line of custom logic layered on a battle-tested primitive is exactly where a focused code review audit earns its cost, the same kind of subtle state-machine drift covered in our piece on gasless approval flows and EVM sniping-bot exposure.
- Machine-checked invariants, not just tests. For protocols where this failure mode is catastrophic, we push toward formally specified liquidity-accounting invariants rather than coverage alone — the same discipline we describe in our write-up on formal verification for trading programs, applied here to a Solidity liquidity state machine instead of a Solana program.
Remediation
Kyber's fix, and the general remediation for this bug class, comes down to three controls: make reinvestment-liquidity credit idempotent per tick per transaction (track which ticks have already had accrued fees folded in during the current call); separate reinvestment accounting from the core liquidityNet delta so a bug in one can't silently inflate the other; and add an explicit solvency check before any swap completes — reject the trade if the computed output would leave tracked liquidity unbacked by actual token balances. None of these are exotic controls. They're the kind of invariant that should be checked before a fork of an audited AMM ships a new feature, not retrofitted after nine figures leave the pools.
If your protocol forks or extends an existing AMM's liquidity accounting, this is exactly the class of bug that unit tests miss and invariant fuzzing catches — talk to us about a smart contract audit before you ship the modification.
Need your contracts audited?
Manual review + tooling across TON, Solana and EVM — certificate and full report included.
Get an audit