Section 7 of 9

Build
+15 Shields

Swap Across Ticks + Fee Tier System

Swap Across Ticks + Fee Tier System

What You Are Building

The swap function is the most complex piece of the entire protocol. Unlike a constant product AMM where a single formula computes the output, a concentrated liquidity swap must walk through multiple tick ranges, each potentially having different amounts of active liquidity. You will build this step by step loop and the core math function that powers each step.

Why Swaps Are Different Here

In Uniswap V2, a swap follows one curve: x * y = k. The output amount is a single calculation. In concentrated liquidity, the swap follows a curve defined by the currently active liquidity, but that liquidity can change at every initialized tick. A swap selling 1 ETH might process 0.3 ETH in the first tick range (with high liquidity and low price impact), then cross a tick where some LPs' ranges end, reducing liquidity, and process the remaining 0.7 ETH in a second range with higher price impact.

This means the swap function is a loop, not a formula.

The Two Structs

SwapState persists across the entire swap. It tracks:

  • amountRemaining: how much input is left to consume
  • amountCalculated: accumulated output so far
  • sqrtPriceX96: the price as it moves during the swap
  • tick: the current tick as it updates
  • feeGrowthGlobalX128: fee growth counter for the input token
  • liquidity: the active liquidity (changes at tick crossings)

StepState is created fresh for each iteration. It tracks:

  • sqrtPriceStartX96: price at the start of this step
  • tickNext: the next initialized tick in the swap direction
  • sqrtPriceNextX96: the price at that next tick
  • amountIn, amountOut, feeAmount: results of the step computation

computeSwapStep(): The Core Math

This function answers: "Given the current price, a target price (next tick), the active liquidity, and the remaining input, what happens?"

Step 1: Fee deduction. The fee is taken from the input before any price math. For a 0.3% fee tier (feePips = 3000):

amountRemainingAfterFee = amountRemaining * (1,000,000 - 3000) / 1,000,000

The fee uses a denominator of 1,000,000 (one million), where feePips represents hundredths of a basis point.

Step 2: Can we reach the next tick? Calculate how much input is needed to move the price from current to target. If amountRemainingAfterFee is enough, the price reaches the target exactly. If not, the price moves partway and the swap ends (or continues from a new range).

Step 3: Compute the output. Based on how far the price actually moved, calculate the output amount. The relationship between price movement, liquidity, and token amounts comes from the concentrated liquidity math:

amount0 = liquidity * (1/sqrtPriceA - 1/sqrtPriceB)
amount1 = liquidity * (sqrtPriceB - sqrtPriceA)

The starter code simplifies this to a linear approximation for clarity.

Step 4: Fee accounting. If the step reached the target price exactly, the fee is whatever input remains after the computed amountIn. If the step ran out of input before reaching the target, the fee is calculated as amountIn * feePips / (1e6 - feePips).

The Swap Loop

The main swap() function initializes SwapState from the pool's current state, then enters a while loop:

while (amountRemaining > 0 && sqrtPriceX96 != sqrtPriceLimitX96)

Each iteration:

  1. Finds the next initialized tick in the swap direction (simplified to tick +/- tickSpacing in the starter code; production V3 uses a bitmap for O(1) lookup)
  2. Gets the sqrt price at that tick
  3. Clamps the target to the price limit (so the swap never overshoots)
  4. Calls computeSwapStep() to get amounts for this range
  5. Updates state: subtracts consumed input, adds output, moves the price
  6. Accumulates fees into feeGrowthGlobalX128 (fee amount divided by active liquidity)
  7. If the price reached the next tick exactly, crosses it: flips the tick's fee growth, adjusts active liquidity by liquidityNet

After the loop, the function writes the final state back to storage and emits a Swap event.

Fee Tiers in Practice

The fee tier is set when the pool is created and cannot change. Each pool with the same token pair but different fee tier is a separate contract. In practice, most volume concentrates on one fee tier per pair. The Factory contract controls which fee tiers are allowed and their corresponding tick spacings.

Direction: zeroForOne

The zeroForOne boolean determines swap direction. When true, the user is selling token0 for token1. The price decreases (sqrtPriceX96 goes down), and the swap moves through ticks in the negative direction. When false, the opposite. This flag affects:

  • Which fee growth counter to update
  • Which direction to search for the next tick
  • The sign of liquidityNet when crossing ticks
  • The sign convention of the final output amounts

Your Task

Implement the SwapState and StepState structs, the computeSwapStep() function, and the swap() loop. The starter code has 11 TODO blocks that walk you through each piece. Start with the structs and computeSwapStep() before attempting the full loop.

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

SwapState struct defined
SwapState has amountRemaining
StepState struct defined
computeSwapStep deducts fee from input
computeSwapStep uses liquidity for price math
swap function validates price limit
swap loop with while condition
Fee growth accumulation in swap loop
Tick crossing updates liquidity
Pool state updated after swap loop
Swap event emitted
zeroForOne direction flag used