Generalized Swapping

This will be the hardest chapter of this milestone. Before updating the code, we need to understand how the algorithm of swapping in Uniswap V3 works.

You can think of a swap as of filling of an order: a user submits an order to buy a specified amount of tokens from a pool. The pool will use the available liquidity to “convert” the input amount into an output amount of the other token. If there’s not enough liquidity in the current price range, it’ll try to find liquidity in other price ranges (using the function we implemented in the previous chapter).

We’re now going to implement this logic in the swap function, however going to stay only within the current price range for now–we’ll implement cross-tick swaps in the next milestone.

function swap(
    address recipient,
    bool zeroForOne,
    uint256 amountSpecified,
    bytes calldata data
) public returns (int256 amount0, int256 amount1) {
    ...

In the swap function, we add two new parameters: zeroForOne and amountSpecified. zeroForOne is the flag that controls swap direction: when true, token0 is traded in for token1; when false, it’s the opposite. For example, if token0 is ETH and token1 is USDC, setting zeroForOne to true means buying USDC for ETH. amountSpecified is the number of tokens the user wants to sell.

Filling Orders

Since, in Uniswap V3, liquidity is stored in multiple price ranges, the Pool contract needs to find all liquidity that’s required to “fill an order” from the user. This is done via iterating over initialized ticks in a direction chosen by the user.

Before continuing, we need to define two new structures:

struct SwapState {
    uint256 amountSpecifiedRemaining;
    uint256 amountCalculated;
    uint160 sqrtPriceX96;
    int24 tick;
}

struct StepState {
    uint160 sqrtPriceStartX96;
    int24 nextTick;
    uint160 sqrtPriceNextX96;
    uint256 amountIn;
    uint256 amountOut;
}

SwapState maintains the current swap’s state. amountSpecifiedRemaining tracks the remaining amount of tokens that need to be bought by the pool. When it’s zero, the swap is done. amountCalculated is the out amount calculated by the contract. sqrtPriceX96 and tick are the new current price and tick after a swap is done.

StepState maintains the current swap step’s state. This structure tracks the state of one iteration of an “order filling”. sqrtPriceStartX96 tracks the price the iteration begins with. nextTick is the next initialized tick that will provide liquidity for the swap and sqrtPriceNextX96 is the price at the next tick. amountIn and amountOut are amounts that can be provided by the liquidity of the current iteration.

After we implement cross-tick swaps (that is, swaps that happen across multiple price ranges), the idea of iterating will be clearer.

// src/UniswapV3Pool.sol

function swap(...) {
    Slot0 memory slot0_ = slot0;

    SwapState memory state = SwapState({
        amountSpecifiedRemaining: amountSpecified,
        amountCalculated: 0,
        sqrtPriceX96: slot0_.sqrtPriceX96,
        tick: slot0_.tick
    });
    ...

Before filling an order, we initialize a SwapState instance. We’ll loop until amountSpecifiedRemaining is 0, which will mean that the pool has enough liquidity to buy amountSpecified tokens from the user.

...
while (state.amountSpecifiedRemaining > 0) {
    StepState memory step;

    step.sqrtPriceStartX96 = state.sqrtPriceX96;

    (step.nextTick, ) = tickBitmap.nextInitializedTickWithinOneWord(
        state.tick,
        1,
        zeroForOne
    );

    step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.nextTick);

In the loop, we set up a price range that should provide liquidity for the swap. The range is from state.sqrtPriceX96 to step.sqrtPriceNextX96, where the latter is the price at the next initialized tick (as returned by nextInitializedTickWithinOneWord–we know this function from a previous chapter).

(state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath
    .computeSwapStep(
        state.sqrtPriceX96,
        step.sqrtPriceNextX96,
        liquidity,
        state.amountSpecifiedRemaining
    );

Next, we’re calculating the amounts that can be provided by the current price range, and the new current price the swap will result in.

    state.amountSpecifiedRemaining -= step.amountIn;
    state.amountCalculated += step.amountOut;
    state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}

The final step in the loop is updating the SwapState. step.amountIn is the number of tokens the price range can buy from the user; step.amountOut is the related number of the other token the pool can sell to the user. state.sqrtPriceX96 is the current price that will be set after the swap (recall that trading changes current price).

SwapMath Contract

Let’s look closer at SwapMath.computeSwapStep.

// src/lib/SwapMath.sol
function computeSwapStep(
    uint160 sqrtPriceCurrentX96,
    uint160 sqrtPriceTargetX96,
    uint128 liquidity,
    uint256 amountRemaining
)
    internal
    pure
    returns (
        uint160 sqrtPriceNextX96,
        uint256 amountIn,
        uint256 amountOut
    )
{
    ...

This is the core logic of swapping. The function calculates swap amounts within one price range and respecting available liquidity. It’ll return: the new current price and input and output token amounts. Even though the input amount is provided by the user, we still calculate it to know how much of the user-specified input amount was processed by one call to computeSwapStep.

bool zeroForOne = sqrtPriceCurrentX96 >= sqrtPriceTargetX96;

sqrtPriceNextX96 = Math.getNextSqrtPriceFromInput(
    sqrtPriceCurrentX96,
    liquidity,
    amountRemaining,
    zeroForOne
);

By checking the price, we can determine the direction of the swap. Knowing the direction, we can calculate the price after swapping the amountRemaining of tokens. We’ll return to this function below.

After finding the new price, we can calculate the input and output amounts of the swap using the function we already have ( the same functions we used to calculate token amounts from liquidity in the mint function):

amountIn = Math.calcAmount0Delta(
    sqrtPriceCurrentX96,
    sqrtPriceNextX96,
    liquidity
);
amountOut = Math.calcAmount1Delta(
    sqrtPriceCurrentX96,
    sqrtPriceNextX96,
    liquidity
);

And swap the amounts if the direction is opposite:

if (!zeroForOne) {
    (amountIn, amountOut) = (amountOut, amountIn);
}

That’s it for computeSwapStep!

Finding Price by Swap Amount

Let’s now look at Math.getNextSqrtPriceFromInput–the function calculates a given another , liquidity, and input amount. It tells what the price will be after swapping the specified input amount of tokens, given the current price and liquidity.

The good news is that we already know the formulas: recall how we calculated price_next in Python:

# When amount_in is token0
price_next = int((liq * q96 * sqrtp_cur) // (liq * q96 + amount_in * sqrtp_cur))
# When amount_in is token1
price_next = sqrtp_cur + (amount_in * q96) // liq

We’re going to implement this in Solidity:

// src/lib/Math.sol
function getNextSqrtPriceFromInput(
    uint160 sqrtPriceX96,
    uint128 liquidity,
    uint256 amountIn,
    bool zeroForOne
) internal pure returns (uint160 sqrtPriceNextX96) {
    sqrtPriceNextX96 = zeroForOne
        ? getNextSqrtPriceFromAmount0RoundingUp(
            sqrtPriceX96,
            liquidity,
            amountIn
        )
        : getNextSqrtPriceFromAmount1RoundingDown(
            sqrtPriceX96,
            liquidity,
            amountIn
        );
}

The function handles swapping in both directions. Since calculations are different, we’ll implement them in separate functions.

function getNextSqrtPriceFromAmount0RoundingUp(
    uint160 sqrtPriceX96,
    uint128 liquidity,
    uint256 amountIn
) internal pure returns (uint160) {
    uint256 numerator = uint256(liquidity) << FixedPoint96.RESOLUTION;
    uint256 product = amountIn * sqrtPriceX96;

    if (product / amountIn == sqrtPriceX96) {
        uint256 denominator = numerator + product;
        if (denominator >= numerator) {
            return
                uint160(
                    mulDivRoundingUp(numerator, sqrtPriceX96, denominator)
                );
        }
    }

    return
        uint160(
            divRoundingUp(numerator, (numerator / sqrtPriceX96) + amountIn)
        );
}

In this function, we’re implementing two formulas. At the first return, it implements the same formula we implemented in Python. This is the most precise formula, but it can overflow when multiplying amountIn by sqrtPriceX96. The formula is (we discussed it in “Output Amount Calculation”):

When it overflows, we use an alternative formula, which is less precise:

Which is simply the previous formula with the numerator and the denominator divided by to get rid of the multiplication in the numerator.

The other function has simpler math:

function getNextSqrtPriceFromAmount1RoundingDown(
    uint160 sqrtPriceX96,
    uint128 liquidity,
    uint256 amountIn
) internal pure returns (uint160) {
    return
        sqrtPriceX96 +
        uint160((amountIn << FixedPoint96.RESOLUTION) / liquidity);
}

Finishing the Swap

Now, let’s return to the swap function and finish it.

By this moment, we have looped over the next initialized ticks, filled amountSpecified specified by the user, calculated input and amount amounts, and found a new price and tick. Since, in this milestone, we’re implementing only swaps within one price range, this is enough. We now need to update the contract’s state, send tokens to the user, and get tokens in exchange.

if (state.tick != slot0_.tick) {
    (slot0.sqrtPriceX96, slot0.tick) = (state.sqrtPriceX96, state.tick);
}

First, we set a new price and tick. Since this operation writes to the contract’s storage, we want to do it only if the new tick is different, to optimize gas consumption.

(amount0, amount1) = zeroForOne
    ? (
        int256(amountSpecified - state.amountSpecifiedRemaining),
        -int256(state.amountCalculated)
    )
    : (
        -int256(state.amountCalculated),
        int256(amountSpecified - state.amountSpecifiedRemaining)
    );

Next, we calculate swap amounts based on the swap direction and the amounts calculated during the swap loop.

if (zeroForOne) {
    IERC20(token1).transfer(recipient, uint256(-amount1));

    uint256 balance0Before = balance0();
    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(
        amount0,
        amount1,
        data
    );
    if (balance0Before + uint256(amount0) > balance0())
        revert InsufficientInputAmount();
} else {
    IERC20(token0).transfer(recipient, uint256(-amount0));

    uint256 balance1Before = balance1();
    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(
        amount0,
        amount1,
        data
    );
    if (balance1Before + uint256(amount1) > balance1())
        revert InsufficientInputAmount();
}

Next, we exchange tokens with the user, depending on the swap direction. This piece is identical to what we had in Milestone 2, only handling of the other swap direction was added.

That’s it! Swapping is done!

Testing

The tests won’t change significantly, we only need to pass the amountSpecified and zeroForOne to the swap function. Output amount will change insignificantly though, because it’s now calculated in Solidity.

We can now test swapping in the opposite direction! I’ll leave this for you, as homework (just be sure to choose a small input amount so the whole swap can be handled by our single price range). Don’t hesitate to peek at my tests if this feels difficult!