All audits
PassedEthereumIndependent

Uniswap v3 Concentrated Liquidity

Smart contract security audit by TierZero · 2023-05-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

0
Critical
0
High
1
Medium
2
Low
4
Informational

Independent research — not a commissioned audit.

What it is

Uniswap v3 replaced the full-range x*y=k curve from v2 with concentrated liquidity: LPs deposit into a price range [pa, pb] instead of the whole curve, and the pool tracks liquidity as a function of price rather than as flat token reserves. Price lives on a discrete grid of ticks, where each tick i maps to price = 1.0001^i. The pool stores an active liquidity value L and a sqrtPriceX96 (a Q64.96 fixed-point square root of price), and within any single tick the pool behaves like a constant-product curve using virtual reserves derived from L and the current price. When a swap pushes price past a tick boundary, the pool applies that tick's liquidityNet delta and continues stepping.

This is the core primitive underneath most of the fee-tier and range-order tooling that exists on Ethereum today, and it's the reference implementation most v3-style forks and aggregators build against — which is exactly why understanding its invariants matters for anyone doing a smart contract audit of code that touches it.

Threat model

For a design this central, the relevant attack surface isn't "can someone break the AMM math" — the math has held up since April 2021 — it's:

  • Price manipulation within a transaction or block to distort a spot-price read used elsewhere (lending collateral valuation, liquidation triggers, other protocols' oracles).
  • Reentrancy during a swap's callback step, where the pool optimistically hands control to msg.sender before payment is confirmed.
  • Tick-crossing edge cases — thin liquidity around the active tick, or rounding at tick boundaries — that could let an attacker get a better price than the curve implies.
  • Integration-layer trust failures, where a router or vault trusts data from something claiming to be a v3 pool without checking it actually is one.

None of these are Uniswap v3 core bugs at this point; they're the recurring failure modes in code built around it.

Why the design holds (key invariants & mitigations)

Atomic, callback-gated settlement. A swap computes its entire multi-tick path in memory before any token movement, then transfers out and calls back into msg.sender for payment, checking balances before/after:

function swap(...) external returns (int256 amount0, int256 amount1) {
    require(slot0.unlocked, "LOK");
    slot0.unlocked = false;
    // ... step across ticks entirely in memory (SwapState) ...
    // update slot0.sqrtPriceX96, tick, liquidity BEFORE the callback
    if (zeroForOne) {
        token1.transfer(recipient, amountOut);
        uint balBefore = token0.balanceOf(address(this));
        IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(...);
        require(token0.balanceOf(address(this)) >= balBefore + amountIn, "IIA");
    }
    slot0.unlocked = true;
}

Two details matter here: state (slot0) is written before the callback fires, so a reentering caller reads post-trade price rather than stale data, and the unlocked guard reverts any reentrant call into the same pool. This is what "flash accounting" buys v3 — the debt is optimistic and short-lived, but it's enforced by a balance check in the same transaction, not by trust.

Deterministic, gas-bounded tick math. getSqrtRatioAtTick/getTickAtSqrtRatio use bit-shift magic constants against a fixed MIN_TICK/MAX_TICK range specifically so price-to-tick conversion is exact and doesn't drift with rounding across repeated calls — a property naive reimplementations frequently lose.

Manipulation cost scales with real depth. Moving price across N ticks costs the attacker the liquidity sitting in every tick crossed, same economic logic as v2's slippage curve, just now sensitive to how liquidity happens to be distributed rather than assumed to be uniform. Thin concentrated ranges around the current price are cheaper to push through than v2's illusion of infinite depth suggested — which is a liquidity-provisioning risk, not a protocol flaw.

TWAP over spot. The oracle records one cumulative tick observation per block (first call only), so distorting the time-weighted average requires holding a manipulated price across multiple blocks — expensive and visible, unlike a single-block spot read.

Where implementers get it wrong

The recurring finding in integrations, not in core, is trusting msg.sender inside a swap callback without verifying it's a genuine pool deployed by the canonical factory for that exact (token0, token1, fee) triple. An attacker deploys a contract that mimics the callback interface, gets a router to treat it as the pool, and the router — believing it owes a payment — pulls the user's pre-approved tokens. This is the same trust-boundary mistake as unchecked cross-program invocation targets in other ecosystems; see our note on cross-program invocation risk in Solana DeFi for the general pattern. The fix is always the same one line: recompute the expected pool address from the factory and compare it to msg.sender before honoring any callback.

Second most common: reading slot0.sqrtPriceX96 directly for anything economically sensitive instead of the TWAP oracle, then being surprised when a flash-loaned single-block swap moves it. Third: routers that don't validate sqrtPriceLimitX96 direction correctly, silently allowing unbounded price impact instead of the slippage bound the caller intended. Approval hygiene around routers matters here too — the same optimistic-transfer pattern is why gasless approval schemes need equal scrutiny; see our review of Permit2-style gasless approvals and EVM sniping-bot exposure.

Takeaways

Uniswap v3's core resists manipulation because settlement is atomic and balance-checked, tick math is deterministic and bounded, and the oracle design makes sustained manipulation costly rather than free. The risk has moved up the stack: to routers, vaults, and lending protocols that consume v3 state without re-deriving trust in it. That's precisely the boundary a code review and audit engagement should focus on when a client integrates against v3 rather than deploys it.

If you're shipping something that reads Uniswap v3 state or wraps its callbacks, it's worth having a second set of eyes on the integration — talk to us about a smart contract audit.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit