Skip to main content

Swap Overview

This section explains how swaps function in Uniswap V4, beginning with a basic test case.

// test/PoolManager.t.sol
function test_swap_succeedsIfInitialized() public {
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: true, settleUsingBurn: false});

vm.expectEmit(true, true, true, true);
emit Swap(
key.toId(), address(swapRouter), int128(-100), int128(98), 79228162514264329749955861424, 1e18, -1, 3000
);
swapRouter.swap(key, SWAP_PARAMS, testSettings, ZERO_BYTES);

Now let's examine src/test/PoolSwapTest.sol to understand how the swap function operates.

function swap(
PoolKey memory key,
IPoolManager.SwapParams memory params,
TestSettings memory testSettings,
bytes memory hookData
) external payable returns (BalanceDelta delta) {
delta = abi.decode(
manager.unlock(abi.encode(CallbackData(msg.sender, testSettings, key, params, hookData))), (BalanceDelta)
);

uint256 ethBalance = address(this).balance;
if (ethBalance > 0) CurrencyLibrary.ADDRESS_ZERO.transfer(msg.sender, ethBalance);
}

This is quite similar to the modifyLiquidity function introduced in the Provide Liquidity section. The core swap logic is also encapsulated within the unlockCallback function. Let's take a closer look at the unlockCallback function.

// src/test/PoolSwapTest/unlockCallback
require(msg.sender == address(manager));
CallbackData memory data = abi.decode(rawData, (CallbackData));
(,, int256 deltaBefore0) = _fetchBalances(data.key.currency0, data.sender, address(this));
(,, int256 deltaBefore1) = _fetchBalances(data.key.currency1, data.sender, address(this));

require(deltaBefore0 == 0, "deltaBefore0 is not equal to 0");
require(deltaBefore1 == 0, "deltaBefore1 is not equal to 0");
// src/test/PoolBase
function _fetchBalances(Currency currency, address user, address deltaHolder)
internal
view
returns (uint256 userBalance, uint256 poolBalance, int256 delta)
{
...
delta = manager.currencyDelta(deltaHolder, currency);
}
// src/libraries/TransientStateLibarary
function currencyDelta(IPoolManager manager, address target, Currency currency) internal view returns (int256) {
bytes32 key;
assembly ("memory-safe") {
mstore(0, and(target, 0xffffffffffffffffffffffffffffffffffffffff))
mstore(32, and(currency, 0xffffffffffffffffffffffffffffffffffffffff))
key := keccak256(0, 64)
}
return int256(uint256(manager.exttload(key)));
}

From the unlockCallback function, we can see that the first step involves calculating the delta value. This is done by hashing address(this) and the currency, then retrieving the data from the manager. Why is delta necessary? It ensures that flash accounting (which will be discussed in a later section) temporarily stores data adjustments without immediately transferring tokens, deferring the final transfer until the process concludes.

Since this is the initial step of the swap, no transfers occur yet. Therefore, it is essential to ensure that deltaBefore == 0. Now, let's delve into the detailed swap process.

// src/test/PoolSwapTest/unlockCallback
BalanceDelta delta = manager.swap(data.key, data.params, data.hookData);
// src/PoolManager
function swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)
external
onlyWhenUnlocked
noDelegateCall
returns (BalanceDelta swapDelta)
{
...
swapDelta = _swap(
pool,
id,
Pool.SwapParams({
tickSpacing: key.tickSpacing,
zeroForOne: params.zeroForOne,
amountSpecified: amountToSwap,
sqrtPriceLimitX96: params.sqrtPriceLimitX96,
lpFeeOverride: lpFeeOverride
}),
params.zeroForOne ? key.currency0 : key.currency1 // input token
);
...
_accountPoolBalanceDelta(key, swapDelta, msg.sender);
}

The core of this function is to invoke the _swap function. The _accountPoolBalanceDelta function temporarily records balance changes without directly affecting the user's token balance. This deferred settlement is finalized within the unlockCallback function, where all balance adjustments are processed collectively. The implementation details of _accountPoolBalanceDelta will be discussed in the "flash accounting" section.

Let's examine how the _swap function operates. The core logic of this function is to invoke the pool.swap() method. We will analyze this function in detail in the next section.

function _swap(Pool.State storage pool, PoolId id, Pool.SwapParams memory params, Currency inputCurrency)
internal
returns (BalanceDelta)
{
(BalanceDelta delta, uint256 amountToProtocol, uint24 swapFee, Pool.SwapResult memory result) =
pool.swap(params);

// the fee is on the input currency
if (amountToProtocol > 0) _updateProtocolFees(inputCurrency, amountToProtocol);
...
return delta;
}

The delta return parameters represent the token adjustments resulting from the swap. For example, if a user swaps 100 token0 for 98 token1, the delta would look like this:

delta = {
amount0: -100, // negative value represents the amount the user must pay
amount1: 98 // positive value represents the amount the user receives
}

amountToProtocol represents the fee for this swap, which is paid to the protocol contract (such as Uniswap). swapFee is the fee paid to liquidity providers. If amountToProtocol > 0, the protocolFeesAccrued is updated by protocolFeesAccrued[currency] += amount. Finally, the function returns the delta parameter.

UnlockCallback

After the swap function is executed, we obtain the delta information, indicating how many tokens the user needs to input and how many tokens they will receive in return. Now, let's review the remaining code of the unlockCallback function to understand how it is executed.

First, after the swap, we calculate the deltaAfter value based on the updated data. Next, we perform a scenario check. The zeroForOne parameter determines whether token0 is being swapped for token1 or vice versa. The amountSpecified parameter indicates the exact number of tokens the user wishes to receive or send to the pool (as specified by the frontend UI input).

This parameter decides whether the swap requires an exact input or an exact output. A negative value indicates an exact input is required. The condition deltaAfter0 >= data.params.amountSpecified ensures that the user's actual input does not exceed the amount shown in the frontend UI.

For example, suppose data.params.amountSpecified = -1000. If deltaAfter0 = -990, the user needs to pay 990 tokens. However, if deltaAfter0 = -1010, the user will not pay 1010 tokens because deltaAfter0 < data.params.amountSpecified, causing the swap to fail.

To ensure accuracy, we verify that token0 swap matches exactly: delta.amount0() == deltaAfter0. Similarly, if amountSpecified > 0, the user desires an exact output. In this case, the user will not receive more than the amountSpecified value. The condition deltaAfter1 <= data.params.amountSpecified enforces this rule.

(,, int256 deltaAfter0) = _fetchBalances(data.key.currency0, data.sender, address(this));
(,, int256 deltaAfter1) = _fetchBalances(data.key.currency1, data.sender, address(this));

if (data.params.zeroForOne) {
if (data.params.amountSpecified < 0) {
// exact input, 0 for 1
require(
deltaAfter0 >= data.params.amountSpecified,
"deltaAfter0 is not greater than or equal to data.params.amountSpecified"
);
require(delta.amount0() == deltaAfter0, "delta.amount0() is not equal to deltaAfter0");
require(deltaAfter1 >= 0, "deltaAfter1 is not greater than or equal to 0");
} else {
// exact output, 0 for 1
require(deltaAfter0 <= 0, "deltaAfter0 is not less than or equal to zero");
require(delta.amount1() == deltaAfter1, "delta.amount1() is not equal to deltaAfter1");
require(
deltaAfter1 <= data.params.amountSpecified,
"deltaAfter1 is not less than or equal to data.params.amountSpecified"
);
}
} else {
if (data.params.amountSpecified < 0) {
// exact input, 1 for 0
require(
deltaAfter1 >= data.params.amountSpecified,
"deltaAfter1 is not greater than or equal to data.params.amountSpecified"
);
require(delta.amount1() == deltaAfter1, "delta.amount1() is not equal to deltaAfter1");
require(deltaAfter0 >= 0, "deltaAfter0 is not greater than or equal to 0");
} else {
// exact output, 1 for 0
require(deltaAfter1 <= 0, "deltaAfter1 is not less than or equal to 0");
require(delta.amount0() == deltaAfter0, "delta.amount0() is not equal to deltaAfter0");
require(
deltaAfter0 <= data.params.amountSpecified,
"deltaAfter0 is not less than or equal to data.params.amountSpecified"
);
}
}

Next, we use the settle and take functions to handle token transfers. When the delta value is negative, it indicates that the pool will receive tokens from the user, triggering the settle function. Conversely, when the delta value is positive, it means the pool will send tokens to the user, and the take function is invoked.

if (deltaAfter0 < 0) {
data.key.currency0.settle(manager, data.sender, uint256(-deltaAfter0), data.testSettings.settleUsingBurn);
}
if (deltaAfter1 < 0) {
data.key.currency1.settle(manager, data.sender, uint256(-deltaAfter1), data.testSettings.settleUsingBurn);
}
if (deltaAfter0 > 0) {
data.key.currency0.take(manager, data.sender, uint256(deltaAfter0), data.testSettings.takeClaims);
}
if (deltaAfter1 > 0) {
data.key.currency1.take(manager, data.sender, uint256(deltaAfter1), data.testSettings.takeClaims);
}