值得信赖的区块链资讯!
剖析DeFi交易产品之UniswapV3:Pool合约
UniswapV3Pool 合约则复杂很多了,其引用的库合约就达到了 13 个,通过 using 方式使用的也达到了 9 个,如下所示:
using LowGasSafeMath for uint256;
using LowGasSafeMath for int256;
using SafeCast for uint256;
using SafeCast for int256;
using Tick for mapping(int24 => Tick.Info);
using TickBitmap for mapping(int16 => uint256);
using Position for mapping(bytes32 => Position.Info);
using Position for Position.Info;
using Oracle for Oracle.Observation[65535];
LowGasSafeMath 是用于加减乘除算法计算的,SafeCast 用于类型转换,Tick 和 TickBitmap 用于管理 tick 处理相关的操作和计算,Position 则主要用于更新流动性的头寸,Oracle 则是用于预言机计算的。
接着,来看看定义了哪些状态变量:
address public immutable override factory;
address public immutable override token0;
address public immutable override token1;
uint24 public immutable override fee;
int24 public immutable override tickSpacing;
uint128 public immutable override maxLiquidityPerTick;
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;
}
Slot0 public override slot0;
uint256 public override feeGrowthGlobal0X128;
uint256 public override feeGrowthGlobal1X128;
// accumulated protocol fees in token0/token1 units
struct ProtocolFees {
uint128 token0;
uint128 token1;
}
ProtocolFees public override protocolFees;
uint128 public override liquidity;
mapping(int24 => Tick.Info) public override ticks;
mapping(int16 => uint256) public override tickBitmap;
mapping(bytes32 => Position.Info) public override positions;
Oracle.Observation[65535] public override observations;
前 5 个变量我们都已经了解过了,第 6 个变量 maxLiquidityPerTick 表示每个 tick 能接受的最大流动性,是在构造函数中根据 tickSpacing 计算出来的。
slot0 记录了当前的一些状态值,都封装在了结构体 Slot0 中,其共有 7 个字段。sqrtPriceX96 是当前价格,记录的是根号价格,且做了扩展,准确来说:sqrtPriceX96 = (token1数量 / token0数量) ^ 0.5 * 2^96。换句话说,这个值代表的是 token0 和 token1 数量比例的平方根,经过放大以获得更高的精度。这样设计的目的是为了方便和优化合约中的一些计算。如果想从 sqrtPriceX96 得出具体的价格,还需要做一些额外的计算。tick 记录了当前价格对应的价格点。observationIndex、observationCardinality 和 observationCardinalityNext 是跟 observations 数组有关的,也是计算预言机价格时需要的,这在之前的文章《价格预言机的使用总结(三):UniswapV3篇》讲解 UniswapV3 预言机时已经介绍过,这里不再赘述。feeProtocol 则用来存储协议费率,初始化时为 0,可通过 setFeeProtocol 函数来重置该值。unlocked 记录池子的锁定状态,初始化时为 true,主要作为一个防止重入锁来使用。
feeGrowthGlobal0X128 和 feeGrowthGlobal1X128 记录两个 token 的每单位流动性所获取的手续费。
protocolFees 则记录了两个 token 的累计未被领取的协议手续费。
liquidity 记录了池子当前可用的流动性。注意,这里不是指注入池子里的所有流动性总量,而是包含了当前价格的那些有效头寸的流动性总量。
ticks 记录池子里每个 tick 的详细信息,key 为 tick 的序号,value 就是详细信息。tickBitmap 记录已初始化的 tick 的位图。如果一个 tick 没有被用作流动性区间的边界点,即该 tick 没有被初始化,那在交易过程中可以跳过这个 tick。而为了更高效地寻找下一个已初始化的 tick,就使用了 tickBitmap 来记录已初始化的 tick。如果 tick 已被初始化,位图中对应于该 tick 序号的位置设置为 1,否则为 0。
positions 记录每个流动性头寸的详细信息,具体信息如下:
library Position {
// 用于存储每个用户的头寸信息
struct Info {
// 当前头寸的总流动性
uint128 liquidity;
// 截止最后一次更新流动性或所欠费用时,每单位流动性的费用增长
uint256 feeGrowthInside0LastX128;
uint256 feeGrowthInside1LastX128;
// 欠头寸所有者的费用
uint128 tokensOwed0;
uint128 tokensOwed1;
}
...
}
observations 则是存储了计算预言机价格相关的累加值,包括 tick 累加值和流动性累加值。具体用法在《价格预言机的使用总结(三):UniswapV3篇》一文中已经介绍过,这里也不再赘述。
接下来就到合约函数了,UniswapV3Pool 核心的函数在 IUniswapV3PoolActions 接口里有定义,该接口共定义了 7 个函数:
-
initialize:初始化 slot0 状态 -
mint:添加流动性 -
collect:提取收益 -
burn:移除流动性 -
swap:兑换 -
flash:闪电贷 -
increaseObservationCardinalityNext:扩展observations数组可存储的容量
initialize 通常会在第一次添加流动性时被调用,主要会初始化 slot0 状态变量,其中 sqrtPriceX96 是直接作为入参传入的,因为第一次添加流动性时,价格其实是由 LP 自己定的。初始的 tick 则是根据 sqrtPriceX96 计算出来的。而最后一个函数increaseObservationCardinalityNext 是用于预言机的,因为默认的 observations 数组实际存储的容量只是 1,需要扩展这个容量才可计算预言机价格。
mint 函数
mint 是添加流动性的底层函数,以下是其代码实现:
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);
}
其有 5 个入参:
-
recipient:流动性的接收者地址 -
tickLower:区间价格下限的 tick 序号 -
tickUpper:区间价格上限的 tick 序号 -
amount:待添加的流动性数量 -
data:传给回调函数的数据
其中,tick 的上下限和 amount 其实都是通过前端 SDK 根据用户的输入计算好对应的值,通常是通过流动性管理的入口合约 NonfungiblePositionManager 合约下传进来的。关于 NonfungiblePositionManager 合约的实现后面文章再详解。
添加流动性的主要操作其实是在 _modifyPosition 私有函数里,执行完该函数后,返回值包括了需要添加到池子里的两种 token 的具体数额 amount0 和 amount1。之后,查询并临时记录下两种 token 在池子里的当前余额。然后,调用 msg.sender 的回调函数 uniswapV3MintCallback,在回调函数中需要完成两种 token 的支付。msg.sender 一般是 NonfungiblePositionManager 合约,所以 NonfungiblePositionManager 合约会实现该回调函数来完成支付。执行完回调函数之后,那池子里两种 token 的余额就会发生变化,判断其前后余额即可。
_modifyPosition 封装了主要的处理逻辑,其代码如下:
function _modifyPosition(ModifyPositionParams memory params)
private
noDelegateCall
returns (
Position.Info storage position,
int256 amount0,
int256 amount1
)
{
// 检查Tick的上下限是否符合边界条件
checkTicks(params.tickLower, params.tickUpper);
// 从storage位置转存到内存中,后续访问可节省gas
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) {
// 当前报价低于传递的范围;流动性只能通过从左到右交叉而进入范围内,需要提供更多token0
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
amount0 = SqrtPriceMath.getAmount0Delta(
_slot0.sqrtPriceX96,
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
// 计算从价格区间下限到当前价格之间需支付的amount1
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
_slot0.sqrtPriceX96,
params.liquidityDelta
);
// 当前有效头寸的总流动性增加
liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);
} else {
// 当前报价高于传递的范围;流动性只能通过从右到左交叉而进入范围内,需要提供更多token1
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
}
}
}
其中,第一步的核心操作是调用 _updatePosition 函数,先更新头寸。之后的核心操作是计算此次调整头寸流动性时对应的 amount0 和 amount1,这需要根据三种不同情况分别计算:
-
当前 tick 小于头寸的 tick 区间下限时,则只需要更多 token0,所以也只需要计算 amount0 -
当前 tick 大于头寸的 tick 区间上限时,则只需要更多 token1,所以也只需要计算 amount1 -
当前 tick 处于头寸的 tick 区间内时,分别计算 amount0 和 amount1,且池子里处于激活状态的总流动性也跟着调整
前两种状态,添加的流动性都是没有激活的,所以不需要把添加的流动性追加到当前的 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; // SLOAD for gas optimization
uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128; // SLOAD for gas optimization
// 是否需要将tick从初始化翻转为未初始化,或者反之亦然
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
);
// 更新tickLower的数据
flippedLower = ticks.update(
tickLower,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
false,
maxLiquidityPerTick
);
// 更新tickUpper的数据
flippedUpper = ticks.update(
tickUpper,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
true,
maxLiquidityPerTick
);
if (flippedLower) {
// 在tick位图中翻转lower tick的状态
tickBitmap.flipTick(tickLower, tickSpacing);
}
if (flippedUpper) {
// 在tick位图中翻转upper tick的状态
tickBitmap.flipTick(tickUpper, tickSpacing);
}
}
// 计算增长的手续费
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128);
// 更新头寸元数据
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
// 清理不再需要用到的tick数据
if (liquidityDelta < 0) {
if (flippedLower) {
ticks.clear(tickLower);
}
if (flippedUpper) {
ticks.clear(tickUpper);
}
}
}
我们看到有五个入参,其中,owner、tickLower、tickUpper 这三个组合起来的哈希值其实就是状态变量 positions 的 key。实际上,key 的计算是通过 keccak256 算法所得的:
keccak256(abi.encodePacked(owner, tickLower, tickUpper))
实现代码的第一行,就是通过这三个参数得到 Position.Info 类型的 position 变量,从而得到待更新的头寸数据。另外,owner 其实是 NonfungiblePositionManager 合约。其实,对于底层 Pool 合约来说,所有的头寸 owner 都是 NonfungiblePositionManager 合约,而每个用户的头寸则是在 NonfungiblePositionManager 合约里进行区分管理的。
入参中的 liquidityDelta 是需要增加或减少的流动性,该值为正数则表示要增加流动性,负数则是要减少流动性。
入参的 tick 是当前激活的 tick,即 slot0 中保存的 tick。
该内部函数的核心操作逻辑是:先分别更新 tick 的下限和上限的元数据;如果 tick 的流动性从 0 增长为非 0 状态,或从非 0 状态减少成了为 0 的状态,则需要在 tick 位图中执行翻转操作;接着更新头寸元数据,包括流动性的加减和手续费的计算;最后将已经不再需要用到的 tick 数据给清理掉。
至此,池子底层添加流动性的 mint 函数全流程就讲解完了。
burn 函数
接下来看看做移除流动性操作的 burn 函数,其实现逻辑相对简单很多,以下是其代码实现:
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);
}
该函数移除的是 msg.sender 的流动性头寸。其有三个入参,tickLower 和 tickUpper 用来指定要移动哪个头寸,amount 指定要移除的流动性数额。
和 mint 的时候一样,第一步核心操作也是先 _modifyPosition。不过,因为是减少流动性,所以传入的 liquidityDelta 参数转为负数。而返回的 amount0Int 和 amount1Int 也会是负数,所以转为 uint256 类型的 amount0 和 amount1 时,又需要加上负号将负数再转为正数。之后,将 amount0 和 amount1 分别累加到了头寸的 tokensOwed0 和 tokensOwed1。
这时候可能有人会产生疑问,既然是移除流动性,为什么没有转账逻辑?不是应该把 amount0 和 amount1 转回给用户吗?其实,这也是和 UniswapV2 移除流动性时不同的地方了。UniswapV3 的处理方式并不是移除流动性时直接把两种 token 资产转给用户,而是先累加到 tokensOwed0 和 tokensOwed1,代表这是欠用户的资产,其中也包括该头寸已赚取到的手续费。之后,用户其实是要通过 collect 函数来提取 tokensOwed0 和 tokensOwed1 里的资产。
collect 函数
collect 函数其实很简单,以下是其代码实现:
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);
}
5 个入参很好理解,recipient 就是接收 token 的地址,tickLower 和 tickUpper 指定了头寸区间,amount0Requested 和 amount1Requested 是用户希望提取的数额。返回值 amount0 和 amount1 就是实际提取的数额。
实现逻辑的第一行,通过 msg.sender、tickLower、tickUpper 来读取出用户的头寸。接着判断用户希望提取的数额 amount0Requested 和头寸里的 tokensOwed0 哪个值小就实际提取哪个,amount1 的也同样。之后就是从头寸的 tokensOwed 里减掉提取的数额并转账给接收地址。最后发送 Collect 事件。
swap 函数
swap 函数是实现交易的底层函数,其代码逻辑复杂很多,我们对其进行逐步拆解来看。
首先,其入参有 5 个:
function swap(
// 收款地址
address recipient,
// 交易方向,true表示用token0交换token1,false则相反
bool zeroForOne,
// 指定的交易数额,如果是正数则为指定的输入,负数则为指定的输出
int256 amountSpecified,
// 限定的价格
uint160 sqrtPriceLimitX96,
// 传给回调函数的参数
bytes calldata data
) external override noDelegateCall returns (int256 amount0, int256 amount1)
其中,如果 zeroForOne 为 true 的话,那交易后的价格不能小于 sqrtPriceLimitX96;如果 zeroForOne 为 false,则交易后的价格不能大于 sqrtPriceLimitX96。返回值 amount0 和 amount1 是交易后两个 token 的实际成交数额。
下面我们只摘取一些重要代码添加注解进行说明,以下是执行实际交易前的一些准备工作:
// 将状态变量保存在内存中,后续访问通过 MLOAD 完成,可以节省 gas
Slot0 memory slot0Start = slot0;
// 防止重入
slot0.unlocked = false;
// 缓存交易前的数据,以节省 gas
SwapCache memory cache =
SwapCache({
liquidityStart: liquidity,
blockTimestamp: _blockTimestamp(),
feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4),
secondsPerLiquidityCumulativeX128: 0,
tickCumulative: 0,
computedLatestObservation: false
});
// 如果 amountSpecified 为正数,则指定的是确定的输入数额
bool exactInput = amountSpecified > 0;
// 缓存交易过程中需要用到的临时变量
SwapState memory state =
SwapState({
// 剩余可交易金额
amountSpecifiedRemaining: amountSpecified,
// 已交易互换的金额,指与 amountSpecifiedRemaining 互换的 token
amountCalculated: 0,
sqrtPriceX96: slot0Start.sqrtPriceX96,
tick: slot0Start.tick,
feeGrowthGlobalX128: zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128,
protocolFee: 0,
liquidity: cache.liquidityStart
});
之后在一个 while 循环中处理实际的交易逻辑:
// 当剩余可交易金额为零,或交易后价格达到了限定的价格之后才退出循环
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
// 缓存每一次循环的状态变量
StepComputations memory step;
// 交易的起始价格
step.sqrtPriceStartX96 = state.sqrtPriceX96;
// 通过 tick 位图找到下一个已初始化的 tick,即下一个流动性边界点
(step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
state.tick,
tickSpacing,
zeroForOne
);
...
// 将上一步找到的下一个 tick 转为根号价格
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
// 在当前价格和下一口价格之间计算交易结果,返回最新价格、消耗的 amountIn、输出的 amountOut 和手续费 feeAmount
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
state.sqrtPriceX96,
(zeroForOne ? step.sqrtPriceNextX96 sqrtPriceLimitX96)
? sqrtPriceLimitX96
: step.sqrtPriceNextX96,
state.liquidity,
state.amountSpecifiedRemaining,
fee
);
if (exactInput) {
// 此时的剩余可交易金额为正数,需减去消耗的输入 amountIn 和手续费 feeAmount
state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
// 此时该值表示 tokenOut 的累加值,结果为负数
state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());
} else {
// 此时的剩余可交易金额为负数,需加上输出的 amountOut
state.amountSpecifiedRemaining += step.amountOut.toInt256();
// 此时该值表示 tokenIn 的累加值,结果为正数
state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());
}
...
// 如果达到了下一个价格,则需要移动 tick
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
// 如果 tick 已经初始化,则需要执行 tick 的转换
if (step.initialized) {
...
// 转换到下一个 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
state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
// 如果不需要移动 tick,则根据最新价格换算成最新的 tick
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
}
一笔交易有时候会跨越多个流动性区间,所以需要使用循环处理在每一个区间内的交易。当剩余可交易金额已经消耗完,或价格已经达到了指定的限定价格后,循环也就结束了,即交易主流程结束了。
之后就是一些交易收尾的工作了,包括更新 tick、价格、流动性、手续费增长系数等。最后很关键的一步就是做转账和支付,以下是最后的代码:
// 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');
}
// 发送 Swap 事件
emit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.tick);
// 解除防止重入的锁
slot0.unlocked = true;
先将 tokenOut 转给了用户,然后执行了回调函数 uniswapV3SwapCallback,在回调函数里会完成 tokenIn 的支付,执行完回调函数后的余额校验是为了确保回调函数确实完成了 tokenIn 的支付。因为先将 tokenOut 转给了用户,之后才完成支付,因此在回调函数中其实还可以做和 UniswapV2 一样的 flash swap。
flash 函数
flash 函数实现了闪电贷功能,与 flash swap 不同,闪电贷借什么就需要还什么。另外,UniswapV3 的闪电贷可以两种 token 都借。
flash 函数的代码实现相对比较简单,以下是其代码实现:
function flash(
address recipient,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external override lock noDelegateCall {
uint128 _liquidity = liquidity;
require(_liquidity > 0, 'L');
// 计算借贷的手续费
uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);
uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);
// 记录还款前的余额
uint256 balance0Before = balance0();
uint256 balance1Before = balance1();
// 将所借 token 转给用户
if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);
if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);
// 调用回调函数,在该函数里需要完成还款,包括还所借 token 和支付手续费
IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);
// 读取还款后的余额
uint256 balance0After = balance0();
uint256 balance1After = balance1();
// 还款后的余额不能小于还款前的余额加上手续费
require(balance0Before.add(fee0) <= balance0After, 'F0');
require(balance1Before.add(fee1) <= balance1After, 'F1');
// 计算出实际收到的手续费
uint256 paid0 = balance0After - balance0Before;
uint256 paid1 = balance1After - balance1Before;
// 手续费分配
if (paid0 > 0) {
uint8 feeProtocol0 = slot0.feeProtocol % 16;
uint256 fees0 = feeProtocol0 == 0 ? 0 : paid0 / feeProtocol0;
if (uint128(fees0) > 0) protocolFees.token0 += uint128(fees0);
feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - fees0, FixedPoint128.Q128, _liquidity);
}
if (paid1 > 0) {
uint8 feeProtocol1 = slot0.feeProtocol >> 4;
uint256 fees1 = feeProtocol1 == 0 ? 0 : paid1 / feeProtocol1;
if (uint128(fees1) > 0) protocolFees.token1 += uint128(fees1);
feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity);
}
emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1);
}
入参有 4 个,recipient 是接收所贷 token 的地址,amount0 和 amount1 是所要借贷的两个 token 数量,data 是给回调函数的参数。
还款则需在 uniswapV3FlashCallback 回调函数中完成。
最终,闪电贷赚取的手续费也是分配给 LP 和协议费。
比推快讯
更多 >>- Coinbase CEO:法国央行行长对比特币存在误解,比特币更加独立
- 荷兰加密货币平台 Finst 完成 800 万欧元 A 轮融资,Endeit Capital 领投
- 特朗普不会在达沃斯论坛期间宣布美联储主席人选
- 路透调查:100 位经济学家中 58 位预计美联储 Q1 将把联邦基金利率维持在 3.50%至 3.75%区间
- 摩根士丹利:黄金对美元霸权的挑战“看不到尽头”
- 价值 370 万美元 AIA 从团队钱包转出,或将出售
- 美国快餐品牌 Steak'n Shake 将向员工支付 BTC 奖励,锁定期两年
- 币安疑似将发布一款 AI 产品 Topic Rush,专为 meme 交易服务
- 分析:比特币跌破 9 万美元后震荡盘整,加密市场避险情绪升温
- 贝莱德向 Coinbase 存入 635.16 枚 BTC 和 30,827.68 枚 ETH
- Bakkt 建立 ATM 计划拟出售普通股募资最高 3 亿美元
- Mask 创始人回应接管 Lens:暂未有明确发币预期,将支持预测市场及多链
- 数据:过去 24h Binance 净流出 4.38 亿 USDT
- Lightning AI 与 Voltage Park 合并,估值超 25 亿美元
- “1011 内幕巨鲸”代理人:Wintermute 观点被误读,机构参与正推动市场从投机转向配置
- Bitget TradFi 日交易量突破 40 亿美元续创新高
- 世界经济论坛:特朗普特别致辞预计于北京时间 21:30 正式开始
- 分析:比特币 CEX 净流入持续性存疑,或意味缺乏持续抛售压力
- RedStone 收购数据平台 Security Token Market 以加速代币化资产的采用
- DeFiLlama 创始人:多数加密平台钱包登录模式实际落地摩擦远高预期
- GMGN 创始人内盘买入近 2%memes,高点未及时获利错失近 45 万美元利润
- 格里芬:特朗普鼓励美联储推行宽松政策加大通胀风险
- 主流 Perp DEX 一览:各平台交易量均有所回升,Hyperliquid 以 89 亿美元交易量居首
- 数据:若 BTC 突破 93,742 美元,主流 CEX 累计空单清算强度将达 20.6 亿美元
- 青年开发者借助 AI 技术打造空气可视化平台 airo2.xyz
- Vitalik 提议在以太坊协议层引入原生 DVT 质押机制,强化安全性与去中心化
- 数据:Polymarket 上比特币 1 月份价格达到 10 万美元的概率降至 7%
- 俄罗斯议员:非法加密挖矿每年造成约 2.5 亿美元经济损失
- Bitpanda 拟推出整合股票、ETF 和加密货币的统一投资平台
- Bitget 发布 UEX 白皮书,阐述多资产体系演进路径
- Pacifica 交易量突破 1000 亿美元,单积分获取成本最低或约 0.3 美元
- 某巨鲸将 26.85 枚 WBTC 兑换为 490.4 枚 XAUT,均价 4,877 美元
- SKR 上线成交额突破 3,800 万美元,Byreal 为主要交易平台
- 数据:某巨鲸从币安提取 8000 枚 ETH 并将其存入 Aave V3,价值 2372 万美元
- 比特币原生 L1 协议 Bitway 完成 444.4 万美元种子轮融资,TRON DAO 领投
- 数据:3138.65 万枚 EDU 从匿名地址转入 Animoca Brands,价值约 471 万美元
- ETH OG 向 Coinbase 充值 1.41 万枚 ETH,价值 4193 万美元
- 2000 万波段猎手割肉链上黄金空单,持仓规模降至 149 万美元
- 0xa503 开头巨鲸新建 30 枚 BTC 多单,约合 268 万美元
- 7 Siblings 持有 59.68 万枚 ETH,价值 17.65 亿美金且浮亏 2770 万美元
- 帕内塔:美国推动数字金融反映支持美元需求努力
- QCP Asia:日债波动叠加关税风险回升,市场转向避险,比特币承压
- 美股盘前加密货币概念股涨跌不一,CRCL 涨 1.11%
- 以太坊提币情绪延续,过去 24 小时 CEX 净流出 3.86 万枚 ETH
- 火币 HTX“新年第一课”即将开讲:于佳宁详解市场周期识别与资产配置逻辑
- Fight.ID 开放 FIGHT 空投资格查询
- Nansen 已启动第三季积分计划
- 某波段 PEPE 交易员再度做多 110 万美元 PEPE
- Vitalik Buterin:2026 年将全面回归去中心化社交,竞争与去中心化是改善公共讨论的起点
- 数据:ETH 当前全网 8 小时平均资金费率为 0.0007%
比推专栏
更多 >>观点
比推热门文章
- 分析:比特币跌破 9 万美元后震荡盘整,加密市场避险情绪升温
- 贝莱德向 Coinbase 存入 635.16 枚 BTC 和 30,827.68 枚 ETH
- 创始人如何驾驭“危险”的媒体关系?
- Bakkt 建立 ATM 计划拟出售普通股募资最高 3 亿美元
- Mask 创始人回应接管 Lens:暂未有明确发币预期,将支持预测市场及多链
- 数据:过去 24h Binance 净流出 4.38 亿 USDT
- Lightning AI 与 Voltage Park 合并,估值超 25 亿美元
- “1011 内幕巨鲸”代理人:Wintermute 观点被误读,机构参与正推动市场从投机转向配置
- Bitget TradFi 日交易量突破 40 亿美元续创新高
- 世界经济论坛:特朗普特别致辞预计于北京时间 21:30 正式开始
比推 APP



