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);
}