Uniswapv3分析(三)Uniswap-v3-core中是实现V3的底层核心逻辑,而Uniswap-v3-periphery才是用户直接交互的地方。NonfungiblePositionManager.sol此合约继承了ERC721,可以铸造NFT表示头寸f(由onwer,tickLow
Uniswap-v3-core中是实现V3的底层核心逻辑,而Uniswap-v3-periphery才是用户直接交互的地方。
此合约继承了ERC721,可以铸造NFT表示头寸f(由onwer,tickLower,tickUpper唯一确定),用于管理用户的头寸。主要包括以下几个方法:
mint:创建头寸
increaseLiquidity:添加流动性
decreaseLiquidity:减少流动性
burn:销毁头寸
collect:取回代币
mint接收一个结构体参数,结构如下:
token0:代币0
token1:代币1
fee:手续费等级
recipient:头寸接收者
tickLower:价格区间低点
tickUpper:价格区间高点
deadline:截止时间(防止重放攻击)
amount0Desired:希望存入的代币0数量
amount1Desired:希望存入的代币1数量
amount0Min:最少存入的token0数量(防止被frontrun)
amount1Min:最少存入的token1数量(防止被frontrun)
函数里首先添加流动性,获得实际添加的流动性liquidity,消耗的amount0、amount1,以及交易对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
})
);
然后为receipient铸造NFT
_mint(params.recipient, (tokenId = _nextId++));
最后,保存头寸信息到_positions中。
// 计算positionKey
bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
// 通过positionKey获取每单位token0和token1的流动性的手续费数量
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);
// 生成PoolId
uint80 poolId =
cachePoolKey(
address(pool),
PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee})
);
// 将用户的流动性头寸,存入positions
_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
});
为头寸添加流动性(只能修改头寸的代币数量,无法修改价格区间。)。接收如下参数:
tokenId:NFT的tokenId
deadline:截止时间
amount0Desired:希望添加的token0数量
amount1Desired:希望添加的token1数量
amount0Min:最少添加的token0数量
amount1Min:最少添加的token1数量
首先获取头寸信息和key
与mint相同,然后通过addLiquidity添加流动性
最后更新头寸信息
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;
移除部分或者所有流动性,移除后的代币将以待取回代币形式记录,需要再次调用collect方法取回代币。接收参数如下:
tokenId:NFT的tokenId
deadline:截止时间
liquidity:希望移除的流动性数量
amount0Min:最少移除的token0数量
amount1Min:最少移除的token1数量
首先检查头寸流动性大于等于要除流动性。
uint128 positionLiquidity = position.liquidity;
require(positionLiquidity >= params.liquidity);
调用pool.burn方法销毁流动性,返回该流动性对应的token0和token1的代币数量amount0和amount1,然后确认其符合amount0Min和amount1Min的限制。
(amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity);
require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');
最后更新头寸状态。
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;
position.liquidity = positionLiquidity - params.liquidity;
销毁头寸,当该头寸的流动性与待取回代币数量都是0,才能调用。
function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) {
Position storage position = _positions[tokenId];
// 检查position的 liquidity tokensOwed0 tokensOwed1 必须为0
// 否则不能销毁position
require(position.liquidity == 0 && position.tokensOwed0 == 0 && position.tokensOwed1 == 0, 'Not cleared');
// 删除position数据
delete _positions[tokenId];
// 销毁NFT
_burn(tokenId);
}
取出待领取代币(移除的流动性,积累的收益) 接收以下参数:
tokenId:NFT的tokenId
recipient:代币接收者
amount0Max:最多领取的token0代币数量
amount1Max:最多领取的token1代币数量
首先获取取出代币数量。
require(params.amount0Max > 0 || params.amount1Max > 0);
// 当入参recipient为0,设为本Manager合约地址
address recipient = params.recipient == address(0) ? address(this) : params.recipient;
// 根据tokenId获取用户的position
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);
然后判断该头寸是否含有流动性,如果有,则使用burn(0)来触发一次头寸状态的更新。
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;
}
最后取回代币
// 计算实际要取数量
(uint128 amount0Collect, uint128 amount1Collect) =
(
params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max,
params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max
);
// 取出代币,得到实际取出的代币数量
(amount0, amount1) = pool.collect(
recipient,
position.tickLower,
position.tickUpper,
amount0Collect,
amount1Collect
);
the full amount expected
// 更新头寸状态
(position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);
SwapRouter是swap路由的管理。提供代币交易的接口,它是对 UniswapV3Pool 合约中交易相关接口的进一步封装,前端界面主要与这个合约来进行对接。主要包括以下函数:
exactInputSingle()
exactInput()
exactInputInternal()
exactOutputSingle()
exactOutput()
exactOutputInternal()
uniswapV3SwapCallback()
单步交换,指定输入代币数量,尽可能多地获得输出代币。接收如下参数:
tokenIn:输入代币地址
tokenOut:输出代币地址
fee:手续费等级
recipient:输出代币接收者
deadline:截止时间,超过该时间请求无效
amountIn:输入的代币数量
amountOutMinimum:最少收到的输出代币数量
sqrtPriceLimitX96:限制价格
函数逻辑十分简单,使用exactInputInternal进行交换,然后检查amountOut是否大于amountOutMinimum。
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');
}
多步交换,指定输入代币数量,尽可能多地获得输出代币。与单步交易tokenA -> tokenB不同的是,多步交换可以设置Basetoken,即 tokenA -> Basetoken -> tokenB。可以让用户选择最优的交易路径(注:要交多次手续费)。
接收参数如下:
path:交换路径
recipient:输出代币收款人
deadline:交易截止时间
amountIn:输入代币数量
amountOutMinimum:最少输出代币数量
逻辑如下:
function exactInput(ExactInputParams memory params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountOut)
{
address payer = msg.sender;
while (true) {
// 判断路径中交易的token地址是否大于等于2
bool hasMultiplePools = params.path.hasMultiplePools();
// 进行swap
params.amountIn = exactInputInternal(
params.amountIn,
hasMultiplePools ? address(this) : params.recipient, // 中间代币由此合约托管
0, // 传入价格 0 代表以市价交易
SwapCallbackData({
path: params.path.getFirstPool(),
payer: payer
})
);
// 判断是否需要继续交易
if (hasMultiplePools) {
payer = address(this);
// 将当前交换路径path的前20+3个字节删除,即pop最前面的token+fee
params.path = params.path.skipToken();
} else {
amountOut = params.amountIn;
break;
}
}
// 检查交易输出
require(amountOut >= params.amountOutMinimum, 'Too little received');
}
内部方法,指定输出代币数量,尽可能少地提供输入代币。
逻辑如下:
function exactInputInternal(
uint256 amountIn,
address recipient,
uint160 sqrtPriceLimitX96,
SwapCallbackData memory data
) private returns (uint256 amountIn) {
// 如果接受者地址为0,就转为本合约地址
if (recipient == address(0)) recipient = address(this);
// 从path中解析出Pool的关键信息
(address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();
// 判断交易顺序
// 之所以要判断,是因为Pool中price始终以 y/x 表示 (tokenx < tokeny)
bool zeroForOne = tokenIn < tokenOut;
// 调用Pool.swap进行交易
//返回完成本次交换所需的token0数量amount0Delta和实际输出的token1数量amount1Delta
(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));
}
swap的回调方法,实现IUniswapV3SwapCallback.uniswapV3SwapCallback接口。
接收参数如下:
amount0Delta:本次交换产生的amount0(对应代币为token0);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币
amount1Delta:本次交换产生的amount1(对应代币为token1);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币
_data:回调参数,这里为SwapCallbackData类型
逻辑如下:
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata _data
) external override {
require(amount0Delta > 0 || amount1Delta > 0);
SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
// 解析回调参数_data
(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));
//输入代币的场景
//则直接向pool合约转amountToPay
//指定输出代币的场景
//如果是多步交换,则移除前23的字符,将需要的输入作为下一步的输出,进入下一步交换
//如果是单步交换(或最后一步),则tokenIn与tokenOut交换,并向pool合约转账
if (isExactInput) {
pay(tokenIn, data.payer, msg.sender, amountToPay);
} else {
if (data.path.hasMultiplePools()) {
data.path = data.path.skipToken();
exactOutputInternal(amountToPay, msg.sender, 0, data);
} else {
amountInCached = amountToPay;
tokenIn = tokenOut;
pay(tokenIn, data.payer, msg.sender, amountToPay);
}
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!