Generalize Swapping

# Generalize 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 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 amount of tokens user wants to sell.

## Filling Orders #

Since, in Uniswap V3, liquidity is stored in multiple price ranges, Pool contract needs to find all liquidity that’s required to “fill an order” from user. This is done via iterating over initialized ticks in a direction chosen by 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 current swap’s state. amountSpecifiedRemaining tracks the remaining amount of tokens that needs 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 new current price and tick after a swap is done.

StepState maintains 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 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 provider 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 amount of tokens the price range can buy from user; step.amountOut is the related number of the other token the pool can sell to 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 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 amountRemaining of tokens. We’ll return to this function below.

After finding the new price, we can calculate 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 $\sqrt{P}$ given another $\sqrt{P}$, 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.

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”): $$\sqrt{P_{target}} = \frac{\sqrt{P}L}{\Delta x \sqrt{P} + L}$$

When it overflows, we use an alternative formula, which is less precise: $$\sqrt{P_{target}} = \frac{L}{\Delta x + \frac{L}{\sqrt{P}}}$$

Which is simply the previous formula with the numerator and the denominator divided by $\sqrt{P}$ 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 next initialized ticks, filled amountSpecified specified by user, calculated input and amount amounts, and found 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 contract’s state, send tokens to user, and get tokens in exchange.

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


First, we set new price and tick. Since this operation writes to 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 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’re exchanging tokens with user, depending on 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 #

Test won’t change significantly, we only need to pass amountSpecified and zeroForOne to 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 a 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 peeking at my tests if this feels difficult!