Uniswapv3分析(二)本文将分析Uniswapv3的核心模块Uniswap-v3-core。UniswapV3Factory.sol工厂合约主要包含二个方法:createPool方法:用于创建交易池enableFeeAmount方法:用于设置手续费等级createP
本文将分析Uniswap v3的核心模块Uniswap-v3-core。
工厂合约主要包含二个方法:
createPool方法:用于创建交易池
enableFeeAmount方法:用于设置手续费等级
function createPool(
address tokenA,
address tokenB,
uint24 fee
) external override noDelegateCall returns (address pool) {
require(tokenA != tokenB);
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
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);
}
由于V3支持不同手续费等级,所以不同于V2只使用TokenA,TokenB来唯一确定一个交易池,V3还需要用到fee。
因此在创建池的过程中多了一个检查fee是否合规的操作。
Uniswap v3默认支持三种手续费等级:0.05%、0.30%和1.00%。若要新增,就要用到这个方法。
function enableFeeAmount(uint24 fee, int24 tickSpacing) public override {
require(msg.sender == owner);
require(fee < 1000000);
require(tickSpacing > 0 && tickSpacing < 16384);
require(feeAmountTickSpacing[fee] == 0);
feeAmountTickSpacing[fee] = tickSpacing;
emit FeeAmountEnabled(fee, tickSpacing);
}
逻辑很简单,接收两个参数fee(手续费),tickSpacing手续费等级。先判断调用者是否为合约的owner,再判断fee和tickSpacing是否合法, 再判断是否已经被添加,最后改变状态。
交易池合约主要包含以下方法: initialize:初始化交易 mint:添加流动性 burn:移除流动性 swap:交换代币 flash:闪电贷 collect:取回代币 setFeeProtocol:修改某个交易对的协议手续费比例 collectProtocol:收集某个交易对的协议手续费
创建完交易对后,需要调用initialize方法初始化slot0,才能正常使用。
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);
}
slot0结构如下:
sqrtPriceX96:交易对当前的开根号价格
tick:当前的tick
observationIndex:最近更新的观测点数组序号
observationCardinality:观测点数组容量(最大65536,最小1)
observationCardinalityNext:下一个观测点数组容量。
feeProtocol:协议手续费比例,可以分别为token0和token1设置交易手续费中分给协议的比例
unlocked:当前交易对合约是否非锁定状态
用于添加流动性。
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计算添加amount0的流动性所需要的token0的数量amount0Int和token1的数量amount1Int。
然后就是转账,改变状态没什么可说的。
值得注意的是转账通过回调msg.sender进行,因此个人EOA账户无法调用mint。
接下来分析_modifyPosition是如何计算的
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;
position = _updatePosition(
params.owner,
params.tickLower,
params.tickUpper,
params.liquidityDelta,
_slot0.tick
);
if (params.liquidityDelta != 0) {
if (_slot0.tick < params.tickLower) {
amount0 = SqrtPriceMath.getAmount0Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
} else if (_slot0.tick < params.tickUpper) {
uint128 liquidityBefore = liquidity;
(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 {
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
}
}
}
首先检查流动性区间是否合规。
然后通过_updatePosition函数更新头寸。(这个后面会说)
最后通过getAmount0Delta和getAmount1Delta计算需要提供的token0的数量amount0和token1的数量amount1。
具体计算逻辑如下:
当你提供流动性的区间的最小值大于当前tick_ic时,也就是区间在c的上方时,用户只需要提供x代币即可(amount0)。
反之,提供y代币(amount1)。
当tick_ic被包含在你提供流动性的区间时,也就是c在区间内部时,用户需要提供x代币和y代币。
值得注意的时当tick_ic被包含在你提供流动性的区间时,会记录一次观测点数据(也就是此时资产价格),且会更新当前交易对的全局活跃流动性liquidity(后面会用)。
接下来分析_updatePosition。
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;
uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128;
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);
if (liquidityDelta < 0) {
if (flippedLower) {
ticks.clear(tickLower);
}
if (flippedUpper) {
ticks.clear(tickUpper);
}
}
}
首先获取用户的流动性头寸,token0和token1的手续费(Pool的总手续费,所有头寸的总和)
然后通过observeSingle更新Oracle数据,接着使用ticks.update分别更新价格区间低点(tickLower)和价格区间高点(tickUpper)的状态。
然后计算该价格区间的累积每流动性手续费。
然后更新头寸信息,包括头寸的应收手续费tokensOwed0和tokensOwed1,以及头寸流动性liquidity。
如果是移除流动性,并且tick被翻转,则调用clear清空tick状态。
销毁流动性的逻辑与添加流动性几乎相同,唯一的区别是liquidityDelta是负的。就不再赘述了。
swap的逻辑十分复杂,让我们一步一步来分析。
先来看几个参数
address recipient,
bool zeroForOne,
int256 amountSpecified,
uint160 sqrtPriceLimitX96,
bytes calldata data
) external override noDelegateCall returns (int256 amount0, int256 amount1) {
recipient:交易后的代币接收者
zeroForOne:如果从token0交换token1则为true,从token1交换token0则为false
amountSpecified: 指定的代币数量,如果为正,表示希望输入的代币数量;如果为负,则表示希望输出的代币数量
sqrtPriceLimitX96:能够承受的价格上限(或下限),格式为Q64.96;如果从token0到token1,则表示swap过程中的价格下限;如果从token1到token0,则表示价格上限;如果价格超过该值,则swap失败
data:回调参数
然后通过MLOAD
(节省gas)定义一些数据。
当准备工作完成以后,才真正的开开始swap,实际的swap交易在一个循环中发生,让我们来仔细分析。
// 当剩余代币为0或价格到达用户的上限(下限)时结束循环
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
// 交易过程每一次循环的状态变量
StepComputations memory step;
// 交易的起始价格
step.sqrtPriceStartX96 = state.sqrtPriceX96;
// 通过位图找到下一个可以选的交易价格
(step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
state.tick,
tickSpacing,
zeroForOne
);
// 确保不会超出最小/最大刻度
if (step.tickNext < TickMath.MIN_TICK) {
step.tickNext = TickMath.MIN_TICK;
} else if (step.tickNext > TickMath.MAX_TICK) {
step.tickNext = TickMath.MAX_TICK;
}
//根据tickNext计算下一个tick的价格
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
// 计算交换后的价格, 消耗的输入代币数量, 得到的输出代币数量, 交易手续费
(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
);
// 更新tokenIn的余额,以及tokenOut数量
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 (cache.feeProtocol > 0) {
uint256 delta = step.feeAmount / cache.feeProtocol;
step.feeAmount -= delta;
state.protocolFee += uint128(delta);
}
// 更新全局手续费用
if (state.liquidity > 0)
state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);
// 如果此时的价格 = 一个tick的价格,就更新流动性 L 的值
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
// 检查如果该tick已经初始化,即是否为另一个流动性的边界
if (step.initialized) {
if (!cache.computedLatestObservation) {
(cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observations.observeSingle(
cache.blockTimestamp,
0,
slot0Start.tick,
slot0Start.observationIndex,
cache.liquidityStart,
slot0Start.observationCardinality
);
cache.computedLatestObservation = true;
}
// 计算当价格穿过该 tick 时,处于激活的流动性需要变化的数量
int128 liquidityNet =
ticks.cross(
step.tickNext,
(zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
(zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),
cache.secondsPerLiquidityCumulativeX128,
cache.tickCumulative,
cache.blockTimestamp
);
// 根据价格增加/减少,即向左或向右移动,增加/减少相应的流动性
if (zeroForOne) liquidityNet = -liquidityNet;
// 更新流动性
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
}
// 移动当前tick到下一个tick
state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
// aoumtIn被耗尽,使用交换后的价格计算最新的tick值
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
}
在完成交换后,然后更新全局状态
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 {
slot0.sqrtPriceX96 = state.sqrtPriceX96;
}
如果此时的tick与开始时的tick不同就记录一次观测点数据,更新slot0.sqrtPriceX96, slot0.tick等值。
如果交换前后tick值相同,则只需要修改价格
if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;
更新全局流动性
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;
}
更新累积手续费和协议手续费。(注意,如果是从token0交换token1,则只能收取token0作为手续费;反之,只能收取token1作为手续费。)
(amount0, amount1) = zeroForOne == exactInput
? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
: (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);
确定最终用户支付的 token 数和得到的 token 数。
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');
}
最后转账。
与V2一样,V3版本中也又两种闪电贷,但是是通过不同的函数接口来完成的。
第一种是普通的闪电贷,借入和还贷的token相同,通过UniswapV3Pool.flash()完成。 第二种是类似V2的flash swap,借入和还贷的token不同,这个是通过UniswapV3Pool.swap()来完成的。
就不赘述了。
function collect(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount0Requested,
uint128 amount1Requested
) external override lock returns (uint128 amount0, uint128 amount1) {
// we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1}
Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);
amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested;
amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested;
if (amount0 > 0) {
position.tokensOwed0 -= amount0;
TransferHelper.safeTransfer(token0, recipient, amount0);
}
if (amount1 > 0) {
position.tokensOwed1 -= amount1;
TransferHelper.safeTransfer(token1, recipient, amount1);
}
emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1);
}
逻辑比较简单,首先获取用户的头寸,再计算提取的代币数量amount0与amount1,最后更新状态,转移代币。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!