Uniswap-v3-core合约定义的是基础方法,而Uniswap-v3-periphery合约才是我们平常直接交互的合约。
比如,众所周知Uniswap v3头寸是一个NFT,这个NFT就是在periphery合约中创建和管理的,在core合约中并没有任何NFT的概念。
头寸管理合约,全局仅有一个,负责管理所有交易对的头寸,主要包括以下几个方法:
- createAndInitializePoolIfNecessary:创建并初始化合约
- mint:创建头寸
- increaseLiquidity:添加流动性
- decreaseLiquidity:减少流动性
- burn:销毁头寸
- collect:取回代币
需要特别注意,该合约继承了ERC721
,可以mint NFT。因为每个Uniswap v3的头寸(由owner
、tickLower
和tickUpper
确定)是唯一的,因此非常适合用NFT表示。
我们在Uniswap-v3-core中提到,一个交易对合约被创建后,需要初始化才能使用。
本方法就把这一系列操作合并成一个方法:创建并初始化交易对。
/// @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);
}
}
}
首先根据交易对代币(token
和token1
)和手续费fee
获取pool
对象:
- 如果不存在,则调用Uniswap-v3-core工厂合约
createPool
创建该交易对并初始化 - 如果已存在,则根据额
slot0
判断是否已经初始化(价格),如果没有则调用Uniswap-v3-core的initialize
方法进行初始化。
创建新头寸,方法接受的参数如下:
token0
:代币0token1
:代币1fee
:手续费等级(需符合工厂合约中定义的手续费等级)tickLower
:价格区间低点tickUpper
:价格区间高点amount0Desired
:希望存入的代币0数量amount1Desired
:希望存入的代币1数量amount0Min
:最少存入的token0
数量(防止被frontrun)amount1Min
:最少存入的token1
数量(防止被frontrun)recipient
:头寸接收者deadline
:截止时间(超过该时间后请求无效)(防止重放攻击)
返回:
tokenId
:每个头寸会分配一个唯一的tokenId
,代表NFTliquidity
:头寸的流动性amount0
:token0
的数量amount1
:token1
的数量
/// @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
})
);
首先通过addLiquidity方法完成流动性添加,获得实际得到的流动性liquidity
,消耗的amount0
、amount1
,以及交易对pool
。
_mint(params.recipient, (tokenId = _nextId++));
通过ERC721
合约的_mint
方法,向接收者recipient
铸造NFT,tokenId
从1开始递增。
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);
}
最后,保存头寸信息到_positions
中。
为一个头寸添加流动性。需注意,可以修改头寸的代币数量,但是不能修改价格区间。
参数如下:
tokenId
:创建头寸时返回的tokenId
,即NFT的tokenId
amount0Desired
:希望添加的token0
数量amount1Desired
:希望添加的token1
数量amount0Min
:最少添加的token0
数量(防止被frontrun)amount1Min
:最少添加的token1
数量(防止被frontrun)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)
})
);
首先根据tokenId
获取头寸信息;与mint方法一样,这里调用addLiquidity添加流动性,返回添加成功的流动性liquidity
,所消耗的amount0
和amount1
,以及交易对合约pool
。
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);
}
根据pool
对象里的最新头寸信息,更新本合约的头寸状态,比如token0
和token1
的可取回代币数tokensOwed0
和tokensOwed1
,以及头寸当前流动性等。
移除流动性,可以移除部分或者所有流动性,移除后的代币将以待取回代币形式记录,需要再次调用collect方法取回代币。
参数如下:
tokenId
:创建头寸时返回的tokenId
,即NFT的tokenId
liquidity
:希望移除的流动性数量amount0Min
:最少移除的token0
数量(防止被frontrun)amount1Min
:最少移除的token1
数量(防止被frontrun)deadline
:截止时间(超过该时间请求无效)(防止重放攻击)
/// @inheritdoc INonfungiblePositionManager
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
external
payable
override
isAuthorizedForToken(params.tokenId)
checkDeadline(params.deadline)
returns (uint256 amount0, uint256 amount1)
{
注意,这里使用isAuthorizedForToken
modifer:
modifier isAuthorizedForToken(uint256 tokenId) {
require(_isApprovedOrOwner(msg.sender, tokenId), 'Not approved');
_;
}
确认当前用户具备操作该tokenId
的权限,否则禁止移除。
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');
调用Uniswap-v3-core的burn方法销毁流动性,返回该流动性对应的token0
和token1
的代币数量amount0
和amount1
,确认其符合amount0Min
和amount1Min
的限制。
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相同,此处计算头寸的待取回代币等信息。
销毁头寸NFT。仅当该头寸的流动性为0,并且待取回代币数量都是0时,才能销毁NFT。
同样,调用该方法需要验证当前用户拥有tokenId
的权限。
/// @inheritdoc INonfungiblePositionManager
function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) {
Position storage position = _positions[tokenId];
require(position.liquidity == 0 && position.tokensOwed0 == 0 && position.tokensOwed1 == 0, 'Not cleared');
delete _positions[tokenId];
_burn(tokenId);
}
取回待领取代币。
参数如下:
tokenId
:创建头寸时返回的tokenId
,即NFT的tokenId
recipient
:代币接收者amount0Max
:最多领取的token0
代币数量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;
}
如果该头寸含有流动性,则触发一次头寸状态的更新,这里使用burn
0流动性来触发。这是因为Uniswap-v3-core只在mint
和burn
时才更新头寸状态,而collect
方法可能在swap
之后被调用,可能会导致头寸状态不是最新的。
// 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);
}
调用Uniswap-v3-core的collect
方法取回代币,并更新头寸的待取回代币数量。
交换代币,包括以下几个方法:
- exactInputSingle:单步交换,指定输入代币数量,尽可能多地获得输出代币
- exactInput:多步交换,指定输入代币数量,尽可能多地获得输出代币
- exactOutputSingle:单步交换,指定输出代币数量,尽可能少地提供输入代币
- exactOutput:多步交换,指定输出代币数量,尽可能少地提供输入代币
另外,该合约也实现了:
- uniswapV3SwapCallback:交换回调方法
- exactInputInternal:单步交换,内部方法,指定输入代币数量,尽可能多地获得输出代币
- exactOutputInternal:单步交换,内部方法,指定输出代币数量,尽可能少地提供输入代币
单步交换,指定输入代币数量,尽可能多地获得输出代币。
参数如下:
tokenIn
:输入代币地址tokenOut
:输出代币地址fee
:手续费等级recipient
:输出代币接收者deadline
:截止时间,超过该时间请求无效amountIn
:输入的代币数量amountOutMinimum
:最少收到的输出代币数量sqrtPriceLimitX96
:(最高或最低)限制价格
返回:
amountOut
:输出代币数量
/// @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,最后确认输出代币数量amountOut
符合最小输出代币要求amountOutMinimum
。
注意,SwapCallbackData
中的path
按照Path.sol中定义的格式编码。
多步交换,指定输入代币数量,尽可能多地获得输出代币。
参数如下:
path
:交换路径,格式请参考:Path.solrecipient
:输出代币收款人deadline
:交易截止时间amountIn
:输入代币数量amountOutMinimum
:最少输出代币数量
返回:
amountOut
:输出代币
/// @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');
}
在多步交换中,需要按照交换路径,拆成多个单步交换,循环进行,直到路径结束。
如果是第一步交换,则payer
为合约调用方,否则,payer
为当前SwapRouter
合约。
在循环中首先根据hasMultiplePools判断路径path
中是否剩余2个及以上的池子。如果有,则中间交换步骤的收款地址设置为当前SwapRouter
合约,否则设置为入口参数recipient
。
每一步交换后,将当前交换路径path
的前20+3个字节删除,即弹出(pop)最前面的token+fee信息,进入下一次交换,并将每一步交换的输出作为下一次交换的输入。
每一步交换调用exactInputInternal进行。
多步交换后,确认最后的amountOut
满足最小输出代币要求amountOutMinimum
。
单步交换,指定输出代币数量,尽可能少地提供输入代币。
参数如下:
tokenIn
:输入代币地址tokenOut
:输出代币地址fee
:手续费等级recipient
:输出代币收款人deadline
:请求截止时间amountOut
:输出代币数量amountInMaximum
:最大输入代币数量sqrtPriceLimitX96
:最大或最小代币价格
返回:
amountIn
:实际输入代币数量
/// @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完成单步交换,并确认实际输入代币数量amountIn
小于等于最大输入代币数量amountInMaximum
。
多步交换,指定输出代币数量,尽可能少地提供输入代币。
参数如下:
path
:交换路径,格式请参考:Path.solrecipient
:输出代币收款人deadline
:请求截止时间amountOut
:指定输出代币数量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;
}
调用exactOutputInternal完成交换,注意,该方法会在回调方法中继续完成下一步交换,因此不需要像exactInput使用循环交易。
最后确认实际输入代币数量amountIn
小于等于最大输入代币数量amountInMaximum
。
单步交换,内部方法,指定输入代币数量,尽可能多地获得输出代币。
/// @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);
如果没有指定recipient
,则默认为当前SwapRouter
合约地址。这是因为在多步交换时,需要将中间代币保存在当前SwapRouter
合约。
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
根据decodeFirstPool解析path
中第一个池子的信息。
bool zeroForOne = tokenIn < tokenOut;
因为Uniswap v3池子token0
地址小于token1
,根据两个代币地址判断当前是否由token0
交换到token1
。注意,tokenIn
可以是token0
或token1
。
(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)
);
调用swap方法,获得完成本次交换所需的amount0
和amount1
。如果是从token0
交换token1
,则amount1
是负数;反之,amount0
是负数。
如果没有指定sqrtPriceLimitX96
,则默认为最低或最高价格,因为在多步交换中,无法指定每一步的价格。
return uint256(-(zeroForOne ? amount1 : amount0));
返回amountOut
。
单步交换,内部方法,指定输出代币数量,尽可能少地提供输入代币。
/// @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;
这部分代码与exactInputInternal类似。
(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)
);
调用Uniswap-v3-core的swap
方法完成单步交换,注意,因为是指定输出代币数量,此处需要使用-amountOut.toInt256()
。
返回的amount0Delta
和amount1Delta
为完成本次交换所需的token0
数量和实际输出的token1
数量。
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);
}
swap的回调方法,实现IUniswapV3SwapCallback.uniswapV3SwapCallback
接口。
参数如下:
amount0Delta
:本次交换产生的amount0
(对应代币为token0
);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币amount1Delta
:本次交换产生的amount1
(对应代币为token1
);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币_data
:回调参数,这里为SwapCallbackData
类型
/// @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);
解析回调参数_data
,根据decodeFirstPool获得交易路径上的第一个交易对信息。
(bool isExactInput, uint256 amountToPay) =
amount0Delta > 0
? (tokenIn < tokenOut, uint256(amount0Delta))
: (tokenOut < tokenIn, uint256(amount1Delta));
根据不同输入,有以下几种交易组合:
场景 | 说明 | amount0Delta > 0 | amount1Delta > 0 | tokenIn < tokenOut | isExactInput | amountToPay |
---|---|---|---|---|---|---|
1 | 输入指定数量token0 ,输出尽可能多token1 |
true | false | true | true | amount0Delta |
2 | 输入尽可能少token0 ,输出指定数量token1 |
true | false | true | false | amount0Delta |
3 | 输入指定数量token1 ,输出尽可能多token0 |
false | true | false | true | amount1Delta |
4 | 输入尽可能少token1 ,输出指定数量token0 |
false | true | false | false | 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);
}
}
- 如果
isExactInput
,即指定输入代币的场景,上表中的场景1和场景3,则直接向SwapRouter
合约转账amount0Delta
(场景1)或amount1Delta
(场景3)(都是正数)。 - 如果是指定输出代币的场景
- 如果是多步交换,则移除前23的字符(pop最前面的token+fee),将需要的输入作为下一步的输出,进入下一步交换
- 如果是单步交换(或最后一步),则
tokenIn
与tokenOut
交换,并向SwapRouter
合约转账
添加流动性的回调方法。
参数如下:
amount0Owed
:应转账的token0
数量amount1Owed
:应转账的token1
数量data
:在mint
方法中传入的回调参数
/// @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);
}
首先反向解析回调参数MintCallbackData
,并确认该方法是被指定的交易对合约调用,因为该方法是一个external
方法,可以被外部调用,因此需要确认调用方。
最后,向调用方转入指定的代币数量。
给已初始化的交易对(池子)添加流动性。
/// @notice Add liquidity to an initialized pool
function addLiquidity(AddLiquidityParams memory params)
internal
returns (
uint128 liquidity,
uint256 amount0,
uint256 amount1,
IUniswapV3Pool pool
)
{
PoolAddress.PoolKey memory poolKey =
PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee});
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
根据factory
、token0
、token1
和fee
获取交易对pool
。
// compute the liquidity amount
{
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper);
liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
sqrtRatioAX96,
sqrtRatioBX96,
params.amount0Desired,
params.amount1Desired
);
}
从slot0
获取当前价格sqrtPriceX96
,根据tickLower
和tickUpper
计算区间的最低价格sqrtRatioAX96
和最高价格sqrtRatioBX96
。
根据getLiquidityForAmounts计算能够获得的最大流动性。
(amount0, amount1) = pool.mint(
params.recipient,
params.tickLower,
params.tickUpper,
liquidity,
abi.encode(MintCallbackData({poolKey: poolKey, payer: msg.sender}))
);
使用Uniswap-v3-core的mint
方法添加流动性,并返回实际消耗的amount0
和amount1
。
我们在Uniswap-v3-core的mint
方法中提到,调用方需实现uniswapV3MintCallback接口。这里传入MintCallbackData
作为回调参数,在uniswapV3MintCallback
方法中可以反向解析出来,以便获取交易对和用户信息。
require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');
最后,确认实际消耗的amount0
和amount1
满足amount0Min
和amount1Min
的最低要求。
根据amount0
和价格区间计算流动性。
根据Uniswap-v3-core的getAmount0Delta
中的公式:
可得:
/// @notice Computes the amount of liquidity received for a given amount of token0 and price range
/// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower))
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount0 The amount0 being sent in
/// @return liquidity The amount of returned liquidity
function getLiquidityForAmount0(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint256 amount0
) internal pure returns (uint128 liquidity) {
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
uint256 intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96);
return toUint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96));
}
根据amount1
和价格区间计算流动性。
根据Uniswap-v3-core的getAmount1Delta
公式:
可得:
/// @notice Computes the amount of liquidity received for a given amount of token1 and price range
/// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)).
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount1 The amount1 being sent in
/// @return liquidity The amount of returned liquidity
function getLiquidityForAmount1(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint256 amount1
) internal pure returns (uint128 liquidity) {
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
return toUint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 - sqrtRatioAX96));
}
根据当前价格,计算能够返回的最大流动性。
- 因为当
$\sqrt{P}$ 增大时,需要消耗$x$ ,因此如果当前价格低于价格区间低点时,需要完全根据$x$ 即amount0
计算流动性 - 反之,如果当前价格高于价格区间高点,需要根据
$y$ 即amount1
计算流动性
如下图所示:
其中,
/// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current
/// pool prices and the prices at the tick boundaries
/// @param sqrtRatioX96 A sqrt price representing the current pool prices
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount0 The amount of token0 being sent in
/// @param amount1 The amount of token1 being sent in
/// @return liquidity The maximum amount of liquidity received
function getLiquidityForAmounts(
uint160 sqrtRatioX96,
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint256 amount0,
uint256 amount1
) internal pure returns (uint128 liquidity) {
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
if (sqrtRatioX96 <= sqrtRatioAX96) {
liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0);
} else if (sqrtRatioX96 < sqrtRatioBX96) {
uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0);
uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1);
liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1;
} else {
liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1);
}
}
在Uniswap v3 SwapRouter中,交易路径被编码为一个bytes
类型字符串,其格式为:
其中, token0
交换到token1
,使用手续费等级为fee0
的池子(token0
、token1
、fee0
),继续交换到token2
,使用手续费等级为fee1
的池子(token1
、token2
、fee1
)。
交易路径path
示例如下:
判断交易路径是否经过多个池子(2个及以上)。
/// @notice Returns true iff the path contains two or more pools
/// @param path The encoded swap path
/// @return True if path contains two or more pools, otherwise false
function hasMultiplePools(bytes memory path) internal pure returns (bool) {
return path.length >= MULTIPLE_POOLS_MIN_LENGTH;
}
我们从上述路径编码可知,如果经过2个池子,至少包含3个代币,则路径长度至少需要 MULTIPLE_POOLS_MIN_LENGTH
即等于66。
计算路径中的池子数量。
算法为:
/// @notice Returns the number of pools in the path
/// @param path The encoded swap path
/// @return The number of pools in the path
function numPools(bytes memory path) internal pure returns (uint256) {
// Ignore the first token address. From then on every fee and token offset indicates a pool.
return ((path.length - ADDR_SIZE) / NEXT_OFFSET);
}
解析第一个path的信息,包括token0
,token1
和fee
。
分别返回字符串中0-19子串(token0
,转address
类型),20-22子串(fee
,转uint24
类型),和23-42子串(token1
,转address
类型)。请参考BytesLib.sol
的toAddress和toUint24方法。
/// @notice Decodes the first pool in path
/// @param path The bytes encoded swap path
/// @return tokenA The first token of the given pool
/// @return tokenB The second token of the given pool
/// @return fee The fee level of the pool
function decodeFirstPool(bytes memory path)
internal
pure
returns (
address tokenA,
address tokenB,
uint24 fee
)
{
tokenA = path.toAddress(0);
fee = path.toUint24(ADDR_SIZE);
tokenB = path.toAddress(NEXT_OFFSET);
}
返回第一个池子的路径,即返回前43(即20+3+20)个字符组成的子字符串。
/// @notice Gets the segment corresponding to the first pool in the path
/// @param path The bytes encoded swap path
/// @return The segment containing all data necessary to target the first pool in the path
function getFirstPool(bytes memory path) internal pure returns (bytes memory) {
return path.slice(0, POP_OFFSET);
}
跳过当前路径上的第一个token+fee
,即跳过前20+3个字符。
/// @notice Skips a token + fee element from the buffer and returns the remainder
/// @param path The swap path
/// @return The remaining token + fee elements in the path
function skipToken(bytes memory path) internal pure returns (bytes memory) {
return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET);
}
从字符串的指定序号起,读取一个地址(20个字符):
function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
require(_start + 20 >= _start, 'toAddress_overflow');
require(_bytes.length >= _start + 20, 'toAddress_outOfBounds');
address tempAddress;
assembly {
tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
}
return tempAddress;
}
因为变量_bytes
类型为bytes
,根据ABI定义,bytes
的第一个32字节存储字符串的长度(length),因此需要先跳过前面32字节,即add(_bytes, 0x20)
;add(add(_bytes, 0x20), _start)
表示定位到字符串指定序号_start
;mload
读取从该序号起的32个字节,因为address
类型只有20字节,因此需要div 0x1000000000000000000000000
,即右移12字节。
假设_strat = 0
,_bytes
的分布如下图所示:
从字符串的指定序号起,读取一个uint24
(24位,即3个字符):
function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
require(_start + 3 >= _start, 'toUint24_overflow');
require(_bytes.length >= _start + 3, 'toUint24_outOfBounds');
uint24 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x3), _start))
}
return tempUint;
}
因为_bytes
前32个字符表示字符串长度;mload
读取32字节,可以确保从_start
开始的3个字节在读取出来的32字节的最低位,赋值给类型为uint24
的变量将只保留最低位的3个字节。
假设_strat = 0
,_bytes
的分布如下图所示:
根据白皮书公式5.3-5.5,计算
本合约提供价格预言机相关方法,包括如下方法:
- consult:查询从一段时间前到现在的几何平均价格(以
tick
形式) - getQuoteAtTick:根据
tick
计算代币价格
查询从一段时间前到现在的几何平均价格(以tick
形式)。
参数如下:
pool
: 交易对池子地址period
:以秒计数的区间
返回:
timeWeightedAverageTick
:时间加权平均价格
/// @notice Fetches time-weighted average tick using Uniswap V3 oracle
/// @param pool Address of Uniswap V3 pool that we want to observe
/// @param period Number of seconds in the past to start calculating time-weighted average
/// @return timeWeightedAverageTick The time-weighted average tick from (block.timestamp - period) to block.timestamp
function consult(address pool, uint32 period) internal view returns (int24 timeWeightedAverageTick) {
require(period != 0, 'BP');
uint32[] memory secondAgos = new uint32[](2);
secondAgos[0] = period;
secondAgos[1] = 0;
构造两个监测点,第一个为period
时间之前,第二个为现在。
(int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
根据IUniswapV3Pool.observe
方法获取累积tick
,即公式5.4中的
timeWeightedAverageTick = int24(tickCumulativesDelta / period);
// Always round to negative infinity
if (tickCumulativesDelta < 0 && (tickCumulativesDelta % period != 0)) timeWeightedAverageTick--;
如果tickCumulativesDelta
为负数,并且无法被period
整除,则将平均价格-1。
/// @notice Given a tick and a token amount, calculates the amount of token received in exchange
/// @param tick Tick value used to calculate the quote
/// @param baseAmount Amount of token to be converted
/// @param baseToken Address of an ERC20 token contract used as the baseAmount denomination
/// @param quoteToken Address of an ERC20 token contract used as the quoteAmount denomination
/// @return quoteAmount Amount of quoteToken received for baseAmount of baseToken
function getQuoteAtTick(
int24 tick,
uint128 baseAmount,
address baseToken,
address quoteToken
) internal pure returns (uint256 quoteAmount) {
uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick);
// Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself
if (sqrtRatioX96 <= type(uint128).max) {
uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96;
quoteAmount = baseToken < quoteToken
? FullMath.mulDiv(ratioX192, baseAmount, 1 << 192)
: FullMath.mulDiv(1 << 192, baseAmount, ratioX192);
} else {
uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64);
quoteAmount = baseToken < quoteToken
? FullMath.mulDiv(ratioX128, baseAmount, 1 << 128)
: FullMath.mulDiv(1 << 128, baseAmount, ratioX128);
}
}
根据Uniswap-v3-core的getSqrtRatioAtTick
方法计算tick
对应的
如果baseToken < quoteToken
,则baseToken
为token0
,quoteToken
为token1
:
反之,baseToken
为token1
,quoteToken
为token0
: