本节作者:@Web3Pignard
这一讲我们正式走进 Uniswap 的代码解析,准确来说是 Uniswap V3,通过阅读本节,你可以了解到:
- 什么是 Uniswap,Uniswap 推出了哪些版本,为什么要解析 V3 版本;
- Uniswap V3 由哪些合约构成,每个合约的主要功能和核心流程的解析。
解析 Uniswap 的代码可以帮助我们更好的理解后续的课程,当然你也可以直接跳转到后面的实战开发中,需要的时候再来查阅这一讲,但是我们还是建议你可以花一些时间学习一下 Uniswap 的代码实现,为后续的课程做准备。另外 Uniswap 的代码中会包含一些复杂的数学计算逻辑,可能不是很好理解,你也可以在后面的课程中持续学习。如果你只是想要学习基础的去中心化应用开发,可以适当跳过那部分复杂的数学计算逻辑,我们的课程中也会直接使用 Uniswap V3 的一些代码库,降低课程的复杂度。
Uniswap 是以太坊上最大的去中心化交易所(DEX),我们在上一讲中提到了,Uniswap 这样的去中心化交易所采用的不是订单薄交易的方式,而是由 LP 提供流动性来交易,这个流动性池子中的代币如何定价则成为了去中心化交易所的关键。Uniswap 在其流动性池上构建了一种特定的自动做市商(AMM)机制。称为恒定乘积做市商(Constant Product Market Makers,CPMM)。顾名思义,其核心是一个非常简单的乘积公式:
流动性池是一个持有两种不同 token 的合约,
另外,我们一般说的 token0 的价格是指在流动性池中相对于 token1 的价格,价格与数量互为倒数,因此公式为:
就比如说我作为 LP 在池子中放了 1 个 ETH(token0) 和 3000 个 USDT(token1),那么 k 就是 1*3000=3000,ETH 价格就是 3000/1 = 3000U。那你作为交易方就可以把大概 30 USDT 放进去,拿出来 0.01 个 ETH。然后池子里面就变成了 3030 个 USDT 和 0.99 个 ETH,价格变 3030/0.99≈3030U。ETH 涨价了,这样是不是就解决了定价的问题,有人要换 ETH,ETH 变得稀缺,所以涨价了,下次要换 ETH 就需要更多的 USDT,只要保证池子中的 ETH * USDT 等于一个常量,这样自然就会此消彼长,当 ETH 变少时,你要通过 USDT 换取 ETH 时候就需要消耗更多 USDT,反之亦然。
当然上面的例子没有考虑滑点、手续费、取整等细节,实际合约实现时也有很多细节需要考虑。这里只是为了让大家理解基础逻辑,具体的细节会在后面展开。
Uniswap 到目前已经迭代了好几个版本,下面是各个版本的发展历程:
2018 年 11 月 Uniswap V1 发布,创新性地采用了上述 CPMM,支持 ERC-20 和 ETH 的兑换,为后续版本的 Uniswap 奠定了基础,并成为其他 AMM 协议的启示;
2020 年 5 月 Uniswap V2 发布, 在 V1 的基础上引入了 ERC-20 之间的兑换,以及时间加权平均价格(TWAP)预言机,增加交易对的灵活性,巩固了 Uniswap 在 DEX 的领先地位;
2021 年 5 月 Uniswap V3 发布,引入了集中流动性(Concentrated Liquidity),允许 LP 在交易对中定义特定的价格范围,以实现更精确的价格控制,提升了 LP 的资产利用率;
2023 年 6 月 Uniswap V4 公开了白皮书的草稿版本,引入了 Hook、Singleton、Flash Accounting 和原生 ETH 等多个优化,其中 Hook 是最重要的创新机制,给开发者提供了高度自定义性。
由于 Uniswap V4 截止目前(2024.05.11)尚未上线主网,并且其代码在 BSL 1.1 的许可下发布,该许可证将持续四年,并限制协议仅供经过治理批准的实体使用。虽然 V3 也使用 BSL 1.1 ,但许可证已于 2023 年 4 月 到期,因此本课程使用 V3 版本。
如上所说,Uniswap 核心就是要基于 CPMM 来实现一个自动化做市商,除了用户调用的交易合约外,还需要有提供给 LP 管理流动性池子的合约,以及对流动性的管理。这些功能在不同的合约中实现,在 Uniswap 的架构中,Uniswap V3 的合约大概被分为两类,分别存储在不同的仓库中:
- Uniswap v3-periphery:面向用户的接口代码,如头寸管理、swap 路由等功能,Uniswap 的前端界面与 periphery 合约交互,主要包含三个合约:
- NonfungiblePositionManager.sol:对应头寸管理功能,包含交易池(又称为流动性池或池子,后文统一用交易池表示)创建以及流动性的添加删除;
- NonfungibleTokenPositionDescriptor.sol:对头寸的描述信息;
- SwapRouter.sol:对应 swap 路由的功能,包含单交易池 swap 和多交易池 swap。
- Uniswap v3-core:Uniswap v3 的核心代码,实现了协议定义的所有功能,外部合约可直接与 core 合约交互,主要包含三个合约;
- UniswapV3Factory.sol:工厂合约,用来创建交易池,设置 Owner 和手续费等级;
- UniswapV3PoolDeployer.sol:工厂合约的基类,封装了部署交易池合约的功能;
- UniswapV3Pool.sol:交易池合约,持有实际的 Token,实现价格和流动性的管理,以及在当前交易池中 swap 的功能。
我们主要解析核心流程,包括以下:
- 部署交易池;
- 创建/添加/减少流动性;
- swap。
其中 1 和 2 都是合约提供给 LP 操作的功能,通过部署交易池和管理流动性来提供和管理流动性。而 3 则是提供给普通用户使用 Uniswap 的核心功能(甚至可以说是唯一的功能)swap,也就是交易。接下来我们讲依次讲解 Uniswap 中的相关代码。
在 Uniswap V3 中,通过合约 UniswapV3Pool 来定义一个交易池子,Uniswap 最核心的交易功能在最底层就是调用了该合约的 swap 方法。
而不同的交易对,以及不同的费率和价格区间(后面会具体讲到 tickSpacing)都会部署不同的 UniswapV3Pool
合约实例来负责交易。部署交易池则是针对某一对 token 以及指定费率的和价格区间来部署一个对应的交易池,当部署完成后再次出现同样条件下的交易池则不再需要重复部署了。
部署交易池调用的是 NonfungiblePositionManager
合约的 createAndInitializePoolIfNecessary,参数为:
- token0:token0 的地址,需要小于 token1 的地址且不为零地址;
- token1:token1 的地址;
- fee:以 1,000,000 为基底的手续费费率,Uniswap v3 前端界面支持四种手续费费率(0.01%,0.05%、0.30%、1.00%),对于一般的交易对推荐 0.30%,fee 取值即 3000;
- sqrtPriceX96:当前交易对价格的算术平方根左移 96 位的值,目的是为了方便合约中的计算。
代码为:
/// @inheritdoc IPoolInitializer
function createAndInitializePoolIfNecessary(
address token0,
address token1,
uint24 fee,
uint160 sqrtPriceX96
) external payable override returns (address pool) {
require(token0 < token1);
pool = IUniswapV3Factory(factory).getPool(token0, token1, fee);
if (pool == address(0)) {
pool = IUniswapV3Factory(factory).createPool(token0, token1, fee);
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
} else {
(uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0();
if (sqrtPriceX96Existing == 0) {
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
}
}
}
逻辑非常直观,首先将 token0,token1 和 fee 作为三元组取出交易池的地址 pool,如果取出的是零地址则创建交易池然后初始化,否则继续判断是否初始化过(当前价格),未初始化过则初始化。
我们分别看创建交易池的方法和初始化交易池的方法。
创建交易池调用的是 UniswapV3Factory
合约的 createPool,参数为:
- token0:token0 的地址
- token1 地址:token1 的地址;
- fee:手续费费率。
代码为:
/// @inheritdoc IUniswapV3Factory
function createPool(
address token0,
address token1,
uint24 fee
) external override noDelegateCall returns (address pool) {
require(token0 != token1);
(address token0, address token1) = token0 < token1 ? (token0, token1) : (token1, token0);
require(token0 != address(0));
int24 tickSpacing = feeAmountTickSpacing[fee];
require(tickSpacing != 0);
require(getPool[token0][token1][fee] == address(0));
pool = deploy(address(this), token0, token1, fee, tickSpacing);
getPool[token0][token1][fee] = pool;
// populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses
getPool[token1][token0][fee] = pool;
emit PoolCreated(token0, token1, fee, tickSpacing, pool);
}
通过 fee 获取对应的 tickSpacing,要解释 tickSpacing 必须先解释 tick。
int24 tickSpacing = feeAmountTickSpacing[fee];
tick 是 V3 中价格的表示,如下图所示:
在 V3,整个价格区间由离散的、均匀分布的 ticks 进行标定。因为在 Uniswap V3 中 LP 添加流动性时都会提供一个价格的范围(为了 LP 可以更好的管理头寸),要让不同价格范围的流动性可以更好的管理和利用,需要 ticks 来将价格划分为一个一个的区间,每个 tick 有一个 index 和对应的价格:
V3 规定只有被 tickSpacing 整除的 tick 才允许被初始化,tickSpacing 越大,每个 tick 流动性越多,tick 之间滑点越大,但会节省跨 tick 操作的 gas。
随后确认对应的交易池合约尚未被创建,调用 deploy,参数为工厂合约地址,token0
地址,token1
地址,fee
,以及上面提到的 tickSpacing
。
pool = deploy(address(this), token0, token1, fee, tickSpacing);
deploy
的代码如下:
/// @dev Deploys a pool with the given parameters by transiently setting the parameters storage slot and then
/// clearing it after deploying the pool.
/// @param factory The contract address of the Uniswap V3 factory
/// @param token0 The first token of the pool by address sort order
/// @param token1 The second token of the pool by address sort order
/// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
/// @param tickSpacing The spacing between usable ticks
function deploy(
address factory,
address token0,
address token1,
uint24 fee,
int24 tickSpacing
) internal returns (address pool) {
parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});
pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
delete parameters;
}
deploy
方法会先临时存储交易池合约初始化参数 parameters ,临时存储 parameters 的目的是为了让交易池合约的构造方法反向获取工厂合约的 parameters 变量从而完成参数的传递。
交易池合约的构造方法代码如下:
constructor() {
int24 _tickSpacing;
(factory, token0, token1, fee, _tickSpacing) = IUniswapV3PoolDeployer(msg.sender).parameters();
tickSpacing = _tickSpacing;
maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(_tickSpacing);
}
回到 deploy
,然后使用 new
方法中传递 salt 参数实现 CREATE2
操作码创建交易池合约,使用 CREATE2
的目的是确保相同 token0,token1 和 fee 能计算出相同且唯一的地址。
最后,保存交易池合约地址到 getPool
变量中:
getPool[token0][token1][fee] = pool;
// populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses
getPool[token1][token0][fee] = pool;
至此完成了交易池合约的创建。
初始化交易池调用的是 UniswapV3Factory
合约的 initialize,参数为当前价格 sqrtPriceX96,含义上面已经介绍过了。
代码如下:
/// @inheritdoc IUniswapV3PoolActions
/// @dev not locked because it initializes unlocked
function initialize(uint160 sqrtPriceX96) external override {
require(slot0.sqrtPriceX96 == 0, 'AI');
int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
(uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp());
slot0 = Slot0({
sqrtPriceX96: sqrtPriceX96,
tick: tick,
observationIndex: 0,
observationCardinality: cardinality,
observationCardinalityNext: cardinalityNext,
feeProtocol: 0,
unlocked: true
});
emit Initialize(sqrtPriceX96, tick);
}
首先从 sqrtPriceX96 换算出 tick 的值。
int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
然后初始化预言机,cardinality 表示当前预言机的观测点数组容量, cardinalityNext 表示预言机扩容后的观测点数组容量,这里不详细解释。
(uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp());
最后初始化 slot0 变量,用于记录交易池的全局状态,这里主要就是记录价格和预言机的状态。
slot0 = Slot0({
sqrtPriceX96: sqrtPriceX96,
tick: tick,
observationIndex: 0,
observationCardinality: cardinality,
observationCardinalityNext: cardinalityNext,
feeProtocol: 0,
unlocked: true
});
Slot0结构如下,源码中已经有了详细的注释。
struct Slot0 {
// the current price
uint160 sqrtPriceX96;
// the current tick
int24 tick;
// the most-recently updated index of the observations array
uint16 observationIndex;
// the current maximum number of observations that are being stored
uint16 observationCardinality;
// the next maximum number of observations to store, triggered in observations.write
uint16 observationCardinalityNext;
// the current protocol fee as a percentage of the swap fee taken on withdrawal
// represented as an integer denominator (1/x)%
uint8 feeProtocol;
// whether the pool is locked
bool unlocked;
}
至此完成了交易池合约的初始化。
创建/添加/减少流动性也就是对应 Uniswap 的 UI 中 https://app.uniswap.org/pool 这部分页面的操作内容,是提供给 LP 管理流动性的功能。
创建流动性调用的是 NonfungiblePositionManager
合约的 mint。
参数如下:
struct MintParams {
address token0; // token0 地址
address token1; // token1 地址
uint24 fee; // 费率
int24 tickLower; // 流动性区间下界
int24 tickUpper; // 流动性区间上界
uint256 amount0Desired; // 添加流动性中 token0 数量
uint256 amount1Desired; // 添加流动性中 token1 数量
uint256 amount0Min; // 最小添加 token0 数量
uint256 amount1Min; // 最小添加 token1 数量
address recipient; // 头寸接受者的地址
uint256 deadline; // 过期的区块号
}
代码如下:
/// @inheritdoc INonfungiblePositionManager
function mint(MintParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (
uint256 tokenId,
uint128 liquidity,
uint256 amount0,
uint256 amount1
)
{
IUniswapV3Pool pool;
(liquidity, amount0, amount1, pool) = addLiquidity(
AddLiquidityParams({
token0: params.token0,
token1: params.token1,
fee: params.fee,
recipient: address(this),
tickLower: params.tickLower,
tickUpper: params.tickUpper,
amount0Desired: params.amount0Desired,
amount1Desired: params.amount1Desired,
amount0Min: params.amount0Min,
amount1Min: params.amount1Min
})
);
_mint(params.recipient, (tokenId = _nextId++));
bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);
// idempotent set
uint80 poolId =
cachePoolKey(
address(pool),
PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee})
);
_positions[tokenId] = Position({
nonce: 0,
operator: address(0),
poolId: poolId,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
liquidity: liquidity,
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
feeGrowthInside1LastX128: feeGrowthInside1LastX128,
tokensOwed0: 0,
tokensOwed1: 0
});
emit IncreaseLiquidity(tokenId, liquidity, amount0, amount1);
}
梳理下整体逻辑,首先是 addLiquidity
添加流动性,然后调用 _mint
发送凭证(NFT)给头寸接受者,接着计算一个自增的 poolId,跟交易池地址互相索引,最后将所有信息记录到头寸的结构体中。
addLiquidity
方法定义在这里,核心是计算出 liquidity 然后调用交易池合约 mint
方法。
(amount0, amount1) = pool.mint(
params.recipient,
params.tickLower,
params.tickUpper,
liquidity,
abi.encode(MintCallbackData({poolKey: poolKey, payer: msg.sender}))
);
liquidity ,即流动性,跟 tick 一样,也是 V3 中的重要概念。
在 V2 中,如果我们设定乘积
V2 流动性池的流动性是分布在 0 到正无穷,如下图所示:
在 v3 中,每个头寸提供了一个价格区间,假设 token0 的价格在价格上界 a 和价格下界 b 之间波动,为了实现集中流动性,那么曲线必须在 x/y 轴进行平移,使得 a/b 点和 x/y 轴重合,如下图:
我们忽略推导过程,直接给出数学公式:
我们将图中的曲线分为两部分:起始点左边和起始点右边。在swap
过程中,当前价格会朝着某个方向移动:升高或降低。对于价格的移动,仅有一种 token 会起作用:当前价格升高时,swap
仅需要 token0;当前价格降低时,swap
仅需要 token1。
当流动性提供者提供了
当流动性提供者提供了
如果当前价格超过价格区间属于只能添加单边流动性的情况。
当前价格小于下界 b 时,只有
当前价格大于上界 a 时,只有
回到代码,计算 liquidity 的步骤如下:
- 如果价格在价格区间内,分别计算出两边流动性然后取最小值;
- 如果当前价格超过价格区间则是计算出单边流动性。
交易池合约的 mint方法。
参数为:
- recipient:头寸接收者地址
- tickLower:流动性区间下界
- tickUpper:流动性区间上界
- amount:流动性数量
- data:回调参数
代码为:
/// @inheritdoc IUniswapV3PoolActions
/// @dev noDelegateCall is applied indirectly via _modifyPosition
function mint(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount,
bytes calldata data
) external override lock returns (uint256 amount0, uint256 amount1) {
require(amount > 0);
(, int256 amount0Int, int256 amount1Int) =
_modifyPosition(
ModifyPositionParams({
owner: recipient,
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: int256(amount).toInt128()
})
);
amount0 = uint256(amount0Int);
amount1 = uint256(amount1Int);
uint256 balance0Before;
uint256 balance1Before;
if (amount0 > 0) balance0Before = balance0();
if (amount1 > 0) balance1Before = balance1();
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);
if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');
if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');
emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1);
}
首先调用 _modifyPosition
方法修改当前价格区间的流动性,这个方法相对复杂,放到后面专门讲。其返回的 amount0Int 和 amount1Int 表示 amount 流动性对应的 token0 和 token1 的代币数量。
调用 mint
方法的合约需要实现 IUniswapV3MintCallback
接口完成代币的转入操作:
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);
IUniswapV3MintCallback
的实现在 periphery 仓库的 LiquidityManagement.sol 中。目的是通知调用方向交易池合约转入 amount0 个 token0 和 amount1 个 token2。
/// @inheritdoc IUniswapV3MintCallback
function uniswapV3MintCallback(
uint256 amount0Owed,
uint256 amount1Owed,
bytes calldata data
) external override {
MintCallbackData memory decoded = abi.decode(data, (MintCallbackData));
CallbackValidation.verifyCallback(factory, decoded.poolKey);
if (amount0Owed > 0) pay(decoded.poolKey.token0, decoded.payer, msg.sender, amount0Owed);
if (amount1Owed > 0) pay(decoded.poolKey.token1, decoded.payer, msg.sender, amount1Owed);
}
回调完成后会检查交易池合约的对应余额是否发生变化,并且增量应该大于 amount0 和 amount1:这意味着调用方确实转入了所需的资产。
if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');
if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');
至此完成了流动性的创建。
添加流动性调用的是 NonfungiblePositionManager
合约的 increaseLiquidity。
参数如下:
struct IncreaseLiquidityParams {
uint256 tokenId; // 头寸 id
uint256 amount0Desired; // 添加流动性中 token0 数量
uint256 amount1Desired; // 添加流动性中 token1 数量
uint256 amount0Min; // 最小添加 token0 数量
uint256 amount1Min; // 最小添加 token1 数量
uint256 deadline; // 过期的区块号
}
代码如下:
/// @inheritdoc INonfungiblePositionManager
function increaseLiquidity(IncreaseLiquidityParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (
uint128 liquidity,
uint256 amount0,
uint256 amount1
)
{
Position storage position = _positions[params.tokenId];
PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];
IUniswapV3Pool pool;
(liquidity, amount0, amount1, pool) = addLiquidity(
AddLiquidityParams({
token0: poolKey.token0,
token1: poolKey.token1,
fee: poolKey.fee,
tickLower: position.tickLower,
tickUpper: position.tickUpper,
amount0Desired: params.amount0Desired,
amount1Desired: params.amount1Desired,
amount0Min: params.amount0Min,
amount1Min: params.amount1Min,
recipient: address(this)
})
);
bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper);
// this is now updated to the current transaction
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);
position.tokensOwed0 += uint128(
FullMath.mulDiv(
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
position.liquidity,
FixedPoint128.Q128
)
);
position.tokensOwed1 += uint128(
FullMath.mulDiv(
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
position.liquidity,
FixedPoint128.Q128
)
);
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
position.liquidity += liquidity;
emit IncreaseLiquidity(params.tokenId, liquidity, amount0, amount1);
}
整体逻辑跟 mint
类似,先从 tokeinId 拿到头寸,然后 addLiquidity
添加流动性,返回添加成功的流动性 liquidity,所消耗的 amount0 和 amount1,以及交易池合约 pool。根据 pool 对象里的最新头寸信息,更新头寸状态。
减少流动性调用的是 NonfungiblePositionManager
合约的 decreaseLiquidity。
参数如下:
struct DecreaseLiquidityParams {
uint256 tokenId; // 头寸 id
uint128 liquidity; // 减少流动性数量
uint256 amount0Min; // 最小减少 token0 数量
uint256 amount1Min; // 最小减少 token1 数量
uint256 deadline; // 过期的区块号
}
代码如下:
/// @inheritdoc INonfungiblePositionManager
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
external
payable
override
isAuthorizedForToken(params.tokenId)
checkDeadline(params.deadline)
returns (uint256 amount0, uint256 amount1)
{
require(params.liquidity > 0);
Position storage position = _positions[params.tokenId];
uint128 positionLiquidity = position.liquidity;
require(positionLiquidity >= params.liquidity);
PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];
IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
(amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity);
require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');
bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper);
// this is now updated to the current transaction
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);
position.tokensOwed0 +=
uint128(amount0) +
uint128(
FullMath.mulDiv(
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
positionLiquidity,
FixedPoint128.Q128
)
);
position.tokensOwed1 +=
uint128(amount1) +
uint128(
FullMath.mulDiv(
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
positionLiquidity,
FixedPoint128.Q128
)
);
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
// subtraction is safe because we checked positionLiquidity is gte params.liquidity
position.liquidity = positionLiquidity - params.liquidity;
emit DecreaseLiquidity(params.tokenId, params.liquidity, amount0, amount1);
}
跟 increaseLiquidity
是反向操作,核心逻辑是调用交易池合约的 burn
方法。
(amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity);
burn 的参数为流动性区间下界 tickLower,流动性区间上界 tickUpper 和流动性数量 amount,代码如下:
/// @inheritdoc IUniswapV3PoolActions
/// @dev noDelegateCall is applied indirectly via _modifyPosition
function burn(
int24 tickLower,
int24 tickUpper,
uint128 amount
) external override lock returns (uint256 amount0, uint256 amount1) {
(Position.Info storage position, int256 amount0Int, int256 amount1Int) =
_modifyPosition(
ModifyPositionParams({
owner: msg.sender,
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: -int256(amount).toInt128()
})
);
amount0 = uint256(-amount0Int);
amount1 = uint256(-amount1Int);
if (amount0 > 0 || amount1 > 0) {
(position.tokensOwed0, position.tokensOwed1) = (
position.tokensOwed0 + uint128(amount0),
position.tokensOwed1 + uint128(amount1)
);
}
emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1);
}
也是调用 _modifyPosition
方法修改当前价格区间的流动性,返回的 amount0Int 和 amount1Int 表示 amount 流动性对应的 token0 和 token1 的代币数量,position 表示用户的头寸信息,在这里主要作用是用来记录待取回代币数量。
if (amount0 > 0 || amount1 > 0) {
(position.tokensOwed0, position.tokensOwed1) = (
position.tokensOwed0 + uint128(amount0),
position.tokensOwed1 + uint128(amount1)
);
}
用户可以通过主动调用 collect
方法取出自己头寸信息记录的 tokensOwed0 数量的 token0 和 tokensOwed1 数量对应的 token1。
collect
方法在下一节展开。
取出待领取代币调用的是 NonfungiblePositionManager
合约的 collect。
参数如下:
struct CollectParams {
uint256 tokenId; // 头寸 id
address recipient; // 接收者地址
uint128 amount0Max; // 最大 token0 数量
uint128 amount1Max; // 最大 token1 数量
}
代码如下:
/// @inheritdoc INonfungiblePositionManager
function collect(CollectParams calldata params)
external
payable
override
isAuthorizedForToken(params.tokenId)
returns (uint256 amount0, uint256 amount1)
{
require(params.amount0Max > 0 || params.amount1Max > 0);
// allow collecting to the nft position manager address with address 0
address recipient = params.recipient == address(0) ? address(this) : params.recipient;
Position storage position = _positions[params.tokenId];
PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];
IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
(uint128 tokensOwed0, uint128 tokensOwed1) = (position.tokensOwed0, position.tokensOwed1);
// trigger an update of the position fees owed and fee growth snapshots if it has any liquidity
if (position.liquidity > 0) {
pool.burn(position.tickLower, position.tickUpper, 0);
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) =
pool.positions(PositionKey.compute(address(this), position.tickLower, position.tickUpper));
tokensOwed0 += uint128(
FullMath.mulDiv(
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
position.liquidity,
FixedPoint128.Q128
)
);
tokensOwed1 += uint128(
FullMath.mulDiv(
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
position.liquidity,
FixedPoint128.Q128
)
);
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
}
// compute the arguments to give to the pool#collect method
(uint128 amount0Collect, uint128 amount1Collect) =
(
params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max,
params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max
);
// the actual amounts collected are returned
(amount0, amount1) = pool.collect(
recipient,
position.tickLower,
position.tickUpper,
amount0Collect,
amount1Collect
);
// sometimes there will be a few less wei than expected due to rounding down in core, but we just subtract the full amount expected
// instead of the actual amount so we can burn the token
(position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);
emit Collect(params.tokenId, recipient, amount0Collect, amount1Collect);
}
首先获取待取回代币数量,如果该头寸含有流动性,则触发一次头寸状态的更新,这里调用了交易池合约的burn
方法,但是传入的流动性参数为 0。这是因为 V3 只在 mint
和 burn
时才更新头寸状态,而 collect
方法可能在 swap
之后被调用,可能会导致头寸状态不是最新的。最后调用了交易池合约的 collect
方法取回代币。
// the actual amounts collected are returned
(amount0, amount1) = pool.collect(
recipient,
position.tickLower,
position.tickUpper,
amount0Collect,
amount1Collect
);
交易池合约的 collect 的逻辑比较简单,这里就不展开了,参数 amount0Requested 为请求取回 token0 的数量,amount1Requested 即请求取回 token1 的数量。如果 amount0Requested 大于 position.tokensOwed0,则取回所有的 token0,取回 token1 也同理。
_modifyPosition 方法是 mint
和 burn
的核心方法。
参数如下:
struct ModifyPositionParams {
// the address that owns the position
address owner;
// the lower and upper tick of the position
int24 tickLower;
int24 tickUpper;
// any change in liquidity
int128 liquidityDelta;
}
代码如下:
/// @dev Effect some changes to a position
/// @param params the position details and the change to the position's liquidity to effect
/// @return position a storage pointer referencing the position with the given owner and tick range
/// @return amount0 the amount of token0 owed to the pool, negative if the pool should pay the recipient
/// @return amount1 the amount of token1 owed to the pool, negative if the pool should pay the recipient
function _modifyPosition(ModifyPositionParams memory params)
private
noDelegateCall
returns (
Position.Info storage position,
int256 amount0,
int256 amount1
)
{
checkTicks(params.tickLower, params.tickUpper);
Slot0 memory _slot0 = slot0; // SLOAD for gas optimization
position = _updatePosition(
params.owner,
params.tickLower,
params.tickUpper,
params.liquidityDelta,
_slot0.tick
);
if (params.liquidityDelta != 0) {
if (_slot0.tick < params.tickLower) {
// current tick is below the passed range; liquidity can only become in range by crossing from left to
// right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it
amount0 = SqrtPriceMath.getAmount0Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
} else if (_slot0.tick < params.tickUpper) {
// current tick is inside the passed range
uint128 liquidityBefore = liquidity; // SLOAD for gas optimization
// write an oracle entry
(slot0.observationIndex, slot0.observationCardinality) = observations.write(
_slot0.observationIndex,
_blockTimestamp(),
_slot0.tick,
liquidityBefore,
_slot0.observationCardinality,
_slot0.observationCardinalityNext
);
amount0 = SqrtPriceMath.getAmount0Delta(
_slot0.sqrtPriceX96,
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
_slot0.sqrtPriceX96,
params.liquidityDelta
);
liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);
} else {
// current tick is above the passed range; liquidity can only become in range by crossing from right to
// left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
}
}
}
先通过 _updatePosition
更新头寸信息,接着分别计算出 liquidityDelta 流动性需要提供的 token0 数量 amount0 和 token1 数量 amount1,流动性的计算公式在创建流动性时已经介绍了。
_updatePosition 方法代码如下:
/// @dev Gets and updates a position with the given liquidity delta
/// @param owner the owner of the position
/// @param tickLower the lower tick of the position's tick range
/// @param tickUpper the upper tick of the position's tick range
/// @param tick the current tick, passed to avoid sloads
function _updatePosition(
address owner,
int24 tickLower,
int24 tickUpper,
int128 liquidityDelta,
int24 tick
) private returns (Position.Info storage position) {
position = positions.get(owner, tickLower, tickUpper);
uint256 _feeGrowthGlobal0X128 = feeGrowthGlobal0X128; // SLOAD for gas optimization
uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128; // SLOAD for gas optimization
// if we need to update the ticks, do it
bool flippedLower;
bool flippedUpper;
if (liquidityDelta != 0) {
uint32 time = _blockTimestamp();
(int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) =
observations.observeSingle(
time,
0,
slot0.tick,
slot0.observationIndex,
liquidity,
slot0.observationCardinality
);
flippedLower = ticks.update(
tickLower,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
false,
maxLiquidityPerTick
);
flippedUpper = ticks.update(
tickUpper,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
true,
maxLiquidityPerTick
);
if (flippedLower) {
tickBitmap.flipTick(tickLower, tickSpacing);
}
if (flippedUpper) {
tickBitmap.flipTick(tickUpper, tickSpacing);
}
}
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128);
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
// clear any tick data that is no longer needed
if (liquidityDelta < 0) {
if (flippedLower) {
ticks.clear(tickLower);
}
if (flippedUpper) {
ticks.clear(tickUpper);
}
}
}
ticktickCumulative 和 secondsPerLiquidityCumulativeX128 是预言机观察点相关的两个变量,这里不详细解释。
(int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) =
observations.observeSingle(
time,
0,
slot0.tick,
slot0.observationIndex,
liquidity,
slot0.observationCardinality
);
接着使用 ticks.update
分别更新价格区间低点和价格区间高点的状态。如果对应 tick 的流动性从从无到有,或从有到无,则表示该 tick 需要被翻转。
flippedLower = ticks.update(
tickLower,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
false,
maxLiquidityPerTick
);
flippedUpper = ticks.update(
tickUpper,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
true,
maxLiquidityPerTick
);
随后计算该价格区间的累积的流动性手续费。
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128);
最后更新头寸信息,并判断是否 tick 被翻转,如果 tick 被翻转则调用 ticks.clear
清空 tick 状态。
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
// clear any tick data that is no longer needed
if (liquidityDelta < 0) {
if (flippedLower) {
}
if (flippedUpper) {
ticks.clear(tickUpper);
}
}
至此完成更新头寸流程。
swap 也就指交易,是 Uniswap 中最常用的也是最核心的功能。对应 https://app.uniswap.org/swap 中的相关操作,接下来让我们看看 Uniswap 的合约是如何实现 swap 的。
SwapRouter
合约包含了以下四个交换代币的方法:
exactInput
:多池交换,用户指定输入代币数量,尽可能多地获得输出代币;exactInputSingle
:单池交换,用户指定输入代币数量,尽可能多地获得输出代币;exactOutput
:多池交换,用户指定输出代币数量,尽可能少地提供输入代币;exactOutputSingle
:单池交换,用户指定输出代币数量,尽可能少地提供输入代币。
这里分成"指定输入代币数量"和"指定输出代币数量"分别介绍。
exactInput 方法负责多池交换,指定 swap 路径以及输入代币数量,尽可能多地获得输出代币。
参数如下:
struct ExactInputParams {
bytes path; // swap 路径,可以解析成一个或多个交易池
address recipient; // 接收者地址
uint256 deadline; // 过期的区块号
uint256 amountIn; // 输入代币数量
uint256 amountOutMinimum; // 最少输出代币数量
}
代码如下:
/// @inheritdoc ISwapRouter
function exactInput(ExactInputParams memory params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountOut)
{
address payer = msg.sender; // msg.sender pays for the first hop
while (true) {
bool hasMultiplePools = params.path.hasMultiplePools();
// the outputs of prior swaps become the inputs to subsequent ones
params.amountIn = exactInputInternal(
params.amountIn,
hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies
0,
SwapCallbackData({
path: params.path.getFirstPool(), // only the first pool in the path is necessary
payer: payer
})
);
// decide whether to continue or terminate
if (hasMultiplePools) {
payer = address(this); // at this point, the caller has paid
params.path = params.path.skipToken();
} else {
amountOut = params.amountIn;
break;
}
}
require(amountOut >= params.amountOutMinimum, 'Too little received');
}
在多池 swap 中,会按照 swap 路径,拆成多个单池 swap,循环进行,直到路径结束。如果是第一步 swap。payer 为合约调用方,否则 payer 为当前 SwapRouter
合约。
exactInputSingle方法负责单池交换,指定输入代币数量,尽可能多地获得输出代币。
参数如下,指定了输入代币地址和输出代币地址:
struct ExactInputSingleParams {
address tokenIn; // 输入代币地址
address tokenOut; // 输出代币地址
uint24 fee; // 手续费费率
address recipient; // 接收者地址
uint256 deadline; // 过期的区块号
uint256 amountIn; // 输入代币数量
uint256 amountOutMinimum; // 最少输出代币数量
uint160 sqrtPriceLimitX96; // 限定价格,值为0则不限价
}
代码如下:
/// @inheritdoc ISwapRouter
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountOut)
{
amountOut = exactInputInternal(
params.amountIn,
params.recipient,
params.sqrtPriceLimitX96,
SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender})
);
require(amountOut >= params.amountOutMinimum, 'Too little received');
}
实际调用 exactInputInternal,代码如下:
/// @dev Performs a single exact input swap
function exactInputInternal(
uint256 amountIn,
address recipient,
uint160 sqrtPriceLimitX96,
SwapCallbackData memory data
) private returns (uint256 amountOut) {
// allow swapping to the router address with address 0
if (recipient == address(0)) recipient = address(this);
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
bool zeroForOne = tokenIn < tokenOut;
(int256 amount0, int256 amount1) =
getPool(tokenIn, tokenOut, fee).swap(
recipient,
zeroForOne,
amountIn.toInt256(),
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encode(data)
);
return uint256(-(zeroForOne ? amount1 : amount0));
}
如果没有指定接收者地址,则默认为当前 SwapRouter
合约地址。这个目的是在多池交易中,将中间代币保存在 SwapRouter
合约中。
if (recipient == address(0)) recipient = address(this);
接着解析出交易路由信息 tokenIn,tokenOut 和 fee。并比较 tokenIn 和 tokenOut 的地址得到 zeroForOne,表示在当前交易池是否是 token0 交换 token1。
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
bool zeroForOne = tokenIn < tokenOut;
最后调用交易池合约的 swap
方法,获取完成本次交换所需的 amount0 和 amount1,再根据 zeroForOne 返回 amountOut,进一步判断 amountOut 满足最少输出代币数量的要求,完成 swap。swap
方法相对复杂,放到后面专门讲。
exactOutput 方法负责多池交换,指定 swap 路径以及输出代币数量,尽可能少地提供输入代币。
参数如下:
struct ExactOutputParams {
bytes path; // swap 路径,可以解析成一个或多个交易池
address recipient; // 接收者地址
uint256 deadline; // 过期的区块号
uint256 amountOut; // 输出代币数量
uint256 amountInMaximum; // 最多输入代币数量
}
代码如下:
/// @inheritdoc ISwapRouter
function exactOutput(ExactOutputParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountIn)
{
// it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output
// swap, which happens first, and subsequent swaps are paid for within nested callback frames
exactOutputInternal(
params.amountOut,
params.recipient,
0,
SwapCallbackData({path: params.path, payer: msg.sender})
);
amountIn = amountInCached;
require(amountIn <= params.amountInMaximum, 'Too much requested');
amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}
在多池 swap 中,会按照 swap 路径,拆成多个单池 swap,循环进行,直到路径结束。如果是第一步 swap。payer 为合约调用方,否则 payer 为当前 SwapRouter
合约。
exactOutputSingle方法负责单池交换,指定输出代币数量,尽可能少地提供输入代币。
参数如下,指定了输入代币地址和输出代币地址:
struct ExactOutputSingleParams {
address tokenIn; // 输入代币地址
address tokenOut; // 输出代币地址
uint24 fee; // 手续费费率
address recipient; // 接收者地址
uint256 deadline; // 过期的区块号
uint256 amountOut; // 输出代币数量
uint256 amountInMaximum; // 最多输入代币数量
uint160 sqrtPriceLimitX96; // 限定价格,值为0则不限价
}
代码如下:
/// @inheritdoc ISwapRouter
function exactOutputSingle(ExactOutputSingleParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountIn)
{
// avoid an SLOAD by using the swap return data
amountIn = exactOutputInternal(
params.amountOut,
params.recipient,
params.sqrtPriceLimitX96,
SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender})
);
require(amountIn <= params.amountInMaximum, 'Too much requested');
// has to be reset even though we don't use it in the single hop case
amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}
实际调用 exactOutputInternal,代码如下:
/// @dev Performs a single exact output swap
function exactOutputInternal(
uint256 amountOut,
address recipient,
uint160 sqrtPriceLimitX96,
SwapCallbackData memory data
) private returns (uint256 amountIn) {
// allow swapping to the router address with address 0
if (recipient == address(0)) recipient = address(this);
(address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();
bool zeroForOne = tokenIn < tokenOut;
(int256 amount0Delta, int256 amount1Delta) =
getPool(tokenIn, tokenOut, fee).swap(
recipient,
zeroForOne,
-amountOut.toInt256(),
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encode(data)
);
uint256 amountOutReceived;
(amountIn, amountOutReceived) = zeroForOne
? (uint256(amount0Delta), uint256(-amount1Delta))
: (uint256(amount1Delta), uint256(-amount0Delta));
// it's technically possible to not receive the full output amount,
// so if no price limit has been specified, require this possibility away
if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);
}
跟 exactInputInternal
的逻辑几乎完全一致,除了因为指定输出代币数量,调用交易池合约 swap
方法使用 -amountOut.toInt256() 作为参数。
(int256 amount0Delta, int256 amount1Delta) =
getPool(tokenIn, tokenOut, fee).swap(
recipient,
zeroForOne,
-amountOut.toInt256(),
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encode(data)
);
返回的 amount0Delta 和 amount1Delta 为完成本次 swap 所需的 token0 数量和实际输出的 token1 数量,进一步判断 amountOut 满足最少输出代币数量的要求,完成 swap。
一个通常的 V3 交易池存在很多互相重叠的价格区间的头寸,如下图所示:
每个交易池都会跟踪当前的价格,以及所有包含现价的价格区间提供的总流动性 liquidity。在每个区间的边界的 tick 上记录下
在一个 tick 内的流动性是常数, swap 公式如下:
从上面公式,可以通过输入 token1 的数量
如果是跨 tick 交易则需要拆解成多个 tick 内的交易:如果当前 tick 的流动性不能满足要求,价格会移动到当前区间的边界处。此时,使离开的区间休眠,并激活下一个区间。并且会开始下一个循环并且寻找下一个有流动性的 tick,直到用户需求的数量被满足。
讲完理论,回到代码。swap 方法是交易对 swap 最核心的方法,也是最复杂的方法。
参数为:
- recipient:接收者的地址;
- zeroForOne:如果从 token0 交换 token1 则为 true,从 token1 交换 token0 则为 false;
- amountSpecified:指定的代币数量,指定输入的代币数量则为正数,指定输出的代币数量则为负数;
- sqrtPriceLimitX96:限定价格,如果从 token0 交换 token1 则限定价格下限,从 token1 交换 token0 则限定价格上限;
- data:回调参数。
代码为:
/// @inheritdoc IUniswapV3PoolActions
function swap(
address recipient,
bool zeroForOne,
int256 amountSpecified,
uint160 sqrtPriceLimitX96,
bytes calldata data
) external override noDelegateCall returns (int256 amount0, int256 amount1) {
require(amountSpecified != 0, 'AS');
Slot0 memory slot0Start = slot0;
require(slot0Start.unlocked, 'LOK');
require(
zeroForOne
? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.MIN_SQRT_RATIO
: sqrtPriceLimitX96 > slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 < TickMath.MAX_SQRT_RATIO,
'SPL'
);
slot0.unlocked = false;
SwapCache memory cache =
SwapCache({
liquidityStart: liquidity,
blockTimestamp: _blockTimestamp(),
feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4),
secondsPerLiquidityCumulativeX128: 0,
tickCumulative: 0,
computedLatestObservation: false
});
bool exactInput = amountSpecified > 0;
SwapState memory state =
SwapState({
amountSpecifiedRemaining: amountSpecified,
amountCalculated: 0,
sqrtPriceX96: slot0Start.sqrtPriceX96,
tick: slot0Start.tick,
feeGrowthGlobalX128: zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128,
protocolFee: 0,
liquidity: cache.liquidityStart
});
// continue swapping as long as we haven't used the entire input/output and haven't reached the price limit
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
StepComputations memory step;
step.sqrtPriceStartX96 = state.sqrtPriceX96;
(step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
state.tick,
tickSpacing,
zeroForOne
);
// ensure that we do not overshoot the min/max tick, as the tick bitmap is not aware of these bounds
if (step.tickNext < TickMath.MIN_TICK) {
step.tickNext = TickMath.MIN_TICK;
} else if (step.tickNext > TickMath.MAX_TICK) {
step.tickNext = TickMath.MAX_TICK;
}
// get the price for the next tick
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
state.sqrtPriceX96,
(zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96)
? sqrtPriceLimitX96
: step.sqrtPriceNextX96,
state.liquidity,
state.amountSpecifiedRemaining,
fee
);
if (exactInput) {
state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());
} else {
state.amountSpecifiedRemaining += step.amountOut.toInt256();
state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());
}
// if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee
if (cache.feeProtocol > 0) {
uint256 delta = step.feeAmount / cache.feeProtocol;
step.feeAmount -= delta;
state.protocolFee += uint128(delta);
}
// update global fee tracker
if (state.liquidity > 0)
state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);
// shift tick if we reached the next price
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
// if the tick is initialized, run the tick transition
if (step.initialized) {
// check for the placeholder value, which we replace with the actual value the first time the swap
// crosses an initialized tick
if (!cache.computedLatestObservation) {
(cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observations.observeSingle(
cache.blockTimestamp,
0,
slot0Start.tick,
slot0Start.observationIndex,
cache.liquidityStart,
slot0Start.observationCardinality
);
cache.computedLatestObservation = true;
}
int128 liquidityNet =
ticks.cross(
step.tickNext,
(zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
(zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),
cache.secondsPerLiquidityCumulativeX128,
cache.tickCumulative,
cache.blockTimestamp
);
// if we're moving leftward, we interpret liquidityNet as the opposite sign
// safe because liquidityNet cannot be type(int128).min
if (zeroForOne) liquidityNet = -liquidityNet;
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
}
state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
// recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
}
// update tick and write an oracle entry if the tick change
if (state.tick != slot0Start.tick) {
(uint16 observationIndex, uint16 observationCardinality) =
observations.write(
slot0Start.observationIndex,
cache.blockTimestamp,
slot0Start.tick,
cache.liquidityStart,
slot0Start.observationCardinality,
slot0Start.observationCardinalityNext
);
(slot0.sqrtPriceX96, slot0.tick, slot0.observationIndex, slot0.observationCardinality) = (
state.sqrtPriceX96,
state.tick,
observationIndex,
observationCardinality
);
} else {
// otherwise just update the price
slot0.sqrtPriceX96 = state.sqrtPriceX96;
}
// update liquidity if it changed
if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;
// update fee growth global and, if necessary, protocol fees
// overflow is acceptable, protocol has to withdraw before it hits type(uint128).max fees
if (zeroForOne) {
feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
if (state.protocolFee > 0) protocolFees.token0 += state.protocolFee;
} else {
feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
if (state.protocolFee > 0) protocolFees.token1 += state.protocolFee;
}
(amount0, amount1) = zeroForOne == exactInput
? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
: (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);
// do the transfers and collect payment
if (zeroForOne) {
if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));
uint256 balance0Before = balance0();
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');
} else {
if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0));
uint256 balance1Before = balance1();
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA');
}
emit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.liquidity, state.tick);
slot0.unlocked = true;
}
整体逻辑由一个 while 循环组成,将 swap 过程分解成多个小步骤,一点点的调整当前的 tick,直到满足用户所需的交易量或者价格触及限定价格(此时会部分成交)。
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
使用 `tickBitmap.nextInitializedTickWithinOneWord`` 来找到下一个已初始化的 tick
(step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
state.tick,
tickSpacing,
zeroForOne
);
使用 SwapMath.computeSwapStep
进行 tick 内的 swap。这个方法会计算出当前区间可以满足的输入数量 amountIn,如果它比 amountRemaining 要小,我们会说现在的区间不能满足整个交易,因此下一个 sqrtPriceX96 就是当前区间的上界/下界,也就是说,我们消耗完了整个区间的流动性。如果 amountIn 大于 amountRemaining,我们计算的 sqrtPriceX96 仍然在现在区间内。
// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
state.sqrtPriceX96,
(zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96)
? sqrtPriceLimitX96
: step.sqrtPriceNextX96,
state.liquidity,
state.amountSpecifiedRemaining,
fee
);
保存本次交易的 amountIn 和 amountOut:
- 如果是指定输入代币数量。amountSpecifiedRemaining 表示剩余可用输入代币数量,amountCalculated 表示已输出代币数量(以负数表示);
- 如果是指定输出代币数量。amountSpecifiedRemaining 表示剩余需要输出的代币数量(初始为负值,因此每次交换后需要加上 step.amountOut,直到为 0),amountCalculated 表示已使用的输入代币数量。
if (exactInput) {
state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());
} else {
state.amountSpecifiedRemaining += step.amountOut.toInt256();
state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());
}
如果本次 swap 后的价格达到目标价格,如果该 tick 已经初始化,则通过 ticks.cross
方法穿越该 tick,返回新增的净流动性 liquidityNet 更新可用流动性 state.liquidity,移动当前 tick 到下一个 tick。
如果本次 swap 后的价格达到目标价格,但是又不等于初始价格,即表示此时 swap 结束,使用 swap 后的价格计算最新的 tick 值。
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
// if the tick is initialized, run the tick transition
if (step.initialized) {
// check for the placeholder value, which we replace with the actual value the first time the swap
// crosses an initialized tick
if (!cache.computedLatestObservation) {
(cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observations.observeSingle(
cache.blockTimestamp,
0,
slot0Start.tick,
slot0Start.observationIndex,
cache.liquidityStart,
slot0Start.observationCardinality
);
cache.computedLatestObservation = true;
}
int128 liquidityNet =
ticks.cross(
step.tickNext,
(zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
(zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),
cache.secondsPerLiquidityCumulativeX128,
cache.tickCumulative,
cache.blockTimestamp
);
// if we're moving leftward, we interpret liquidityNet as the opposite sign
// safe because liquidityNet cannot be type(int128).min
if (zeroForOne) liquidityNet = -liquidityNet;
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
}
state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
// recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
重复上述步骤,直到 swap 完全结束。
完成 swap 后,更新 slot0 的状态和全局流动性。
// update tick and write an oracle entry if the tick change
if (state.tick != slot0Start.tick) {
(uint16 observationIndex, uint16 observationCardinality) =
observations.write(
slot0Start.observationIndex,
cache.blockTimestamp,
slot0Start.tick,
cache.liquidityStart,
slot0Start.observationCardinality,
slot0Start.observationCardinalityNext
);
(slot0.sqrtPriceX96, slot0.tick, slot0.observationIndex, slot0.observationCardinality) = (
state.sqrtPriceX96,
state.tick,
observationIndex,
observationCardinality
);
} else {
// otherwise just update the price
slot0.sqrtPriceX96 = state.sqrtPriceX96;
}
// update liquidity if it changed
if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;
最后,计算本次 swap 需要的具体 amount0 和 amount1,调用 IUniswapV3SwapCallback
接口。在回调之前已经把输出的 token 发送给了 recipient。
// do the transfers and collect payment
if (zeroForOne) {
if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));
uint256 balance0Before = balance0();
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');
} else {
if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0));
uint256 balance1Before = balance1();
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA');
}
IUniswapV3SwapCallback
的实现在 periphery 仓库的 SwapRouter.sol 中,负责支付输入的 token。
/// @inheritdoc IUniswapV3SwapCallback
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata _data
) external override {
require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
(bool isExactInput, uint256 amountToPay) =
amount0Delta > 0
? (tokenIn < tokenOut, uint256(amount0Delta))
: (tokenOut < tokenIn, uint256(amount1Delta));
if (isExactInput) {
pay(tokenIn, data.payer, msg.sender, amountToPay);
} else {
// either initiate the next swap or pay
if (data.path.hasMultiplePools()) {
data.path = data.path.skipToken();
exactOutputInternal(amountToPay, msg.sender, 0, data);
} else {
amountInCached = amountToPay;
tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
pay(tokenIn, data.payer, msg.sender, amountToPay);
}
}
}
至此,完成了整体 swap 流程。