剖析DeFi交易产品之UniswapV3:Pool合约

本文首发于公众号:Keegan小钢


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 用于类型转换,TickTickBitmap 用于管理 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 priceuint160 sqrtPriceX96;// the current tickint24 tick;// the most-recently updated index of the observations arrayuint16 observationIndex;// the current maximum number of observations that are being storeduint16 observationCardinality;// the next maximum number of observations to store, triggered in observations.writeuint16 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 lockedbool 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 记录了当前价格对应的价格点。observationIndexobservationCardinalityobservationCardinalityNext 是跟 observations 数组有关的,也是计算预言机价格时需要的,这在之前的文章《价格预言机的使用总结(三):UniswapV3篇》讲解 UniswapV3 预言机时已经介绍过,这里不再赘述。feeProtocol 则用来存储协议费率,初始化时为 0,可通过 setFeeProtocol 函数来重置该值。unlocked 记录池子的锁定状态,初始化时为 true,主要作为一个防止重入锁来使用。

feeGrowthGlobal0X128feeGrowthGlobal1X128 记录两个 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 的具体数额 amount0amount1。之后,查询并临时记录下两种 token 在池子里的当前余额。然后,调用 msg.sender 的回调函数 uniswapV3MintCallback,在回调函数中需要完成两种 token 的支付。msg.sender 一般是 NonfungiblePositionManager 合约,所以 NonfungiblePositionManager 合约会实现该回调函数来完成支付。执行完回调函数之后,那池子里两种 token 的余额就会发生变化,判断其前后余额即可。

_modifyPosition 封装了主要的处理逻辑,其代码如下:

function _modifyPosition(ModifyPositionParams memory params)privatenoDelegateCallreturns (Position.Info storage position,int256 amount0,int256 amount1)
{// 检查Tick的上下限是否符合边界条件checkTicks(params.tickLower, params.tickUpper);// 从storage位置转存到内存中,后续访问可节省gasSlot0 memory _slot0 = slot0;// 第一步核心操作position = _updatePosition(params.owner,params.tickLower,params.tickUpper,params.liquidityDelta,_slot0.tick);if (params.liquidityDelta != 0) {if (_slot0.tick < params.tickLower) {// 当前报价低于传递的范围;流动性只能通过从左到右交叉而进入范围内,需要提供更多token0amount0 = 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);// 计算当前价格到价格区间上限之间需支付的amount0amount0 = SqrtPriceMath.getAmount0Delta(_slot0.sqrtPriceX96,TickMath.getSqrtRatioAtTick(params.tickUpper),params.liquidityDelta);// 计算从价格区间下限到当前价格之间需支付的amount1amount1 = SqrtPriceMath.getAmount1Delta(TickMath.getSqrtRatioAtTick(params.tickLower),_slot0.sqrtPriceX96,params.liquidityDelta);// 当前有效头寸的总流动性增加liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);} else {// 当前报价高于传递的范围;流动性只能通过从右到左交叉而进入范围内,需要提供更多token1amount1 = 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 optimizationuint256 _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);}}
}

我们看到有五个入参,其中,ownertickLowertickUpper 这三个组合起来的哈希值其实就是状态变量 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 的流动性头寸。其有三个入参,tickLowertickUpper 用来指定要移动哪个头寸,amount 指定要移除的流动性数额。

mint 的时候一样,第一步核心操作也是先 _modifyPosition。不过,因为是减少流动性,所以传入的 liquidityDelta 参数转为负数。而返回的 amount0Intamount1Int 也会是负数,所以转为 uint256 类型的 amount0amount1 时,又需要加上负号将负数再转为正数。之后,将 amount0amount1 分别累加到了头寸的 tokensOwed0tokensOwed1

这时候可能有人会产生疑问,既然是移除流动性,为什么没有转账逻辑?不是应该把 amount0amount1 转回给用户吗?其实,这也是和 UniswapV2 移除流动性时不同的地方了。UniswapV3 的处理方式并不是移除流动性时直接把两种 token 资产转给用户,而是先累加到 tokensOwed0tokensOwed1,代表这是欠用户的资产,其中也包括该头寸已赚取到的手续费。之后,用户其实是要通过 collect 函数来提取 tokensOwed0tokensOwed1 里的资产。

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 的地址,tickLowertickUpper 指定了头寸区间,amount0Requestedamount1Requested 是用户希望提取的数额。返回值 amount0amount1 就是实际提取的数额。

实现逻辑的第一行,通过 msg.sendertickLowertickUpper 来读取出用户的头寸。接着判断用户希望提取的数额 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)

其中,如果 zeroForOnetrue 的话,那交易后的价格不能小于 sqrtPriceLimitX96;如果 zeroForOnefalse,则交易后的价格不能大于 sqrtPriceLimitX96。返回值 amount0amount1 是交易后两个 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 互换的 tokenamountCalculated: 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 : step.sqrtPriceNextX96 > sqrtPriceLimitX96)? sqrtPriceLimitX96: step.sqrtPriceNextX96,state.liquidity,state.amountSpecifiedRemaining,fee);if (exactInput) {// 此时的剩余可交易金额为正数,需减去消耗的输入 amountIn 和手续费 feeAmountstate.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();// 此时该值表示 tokenOut 的累加值,结果为负数state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());} else {// 此时的剩余可交易金额为负数,需加上输出的 amountOutstate.amountSpecifiedRemaining += step.amountOut.toInt256();// 此时该值表示 tokenIn 的累加值,结果为正数state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());}...// 如果达到了下一个价格,则需要移动 tickif (state.sqrtPriceX96 == step.sqrtPriceNextX96) {// 如果 tick 已经初始化,则需要执行 tick 的转换if (step.initialized) {...// 转换到下一个 tickint128 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);}// 更新 tickstate.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {// 如果不需要移动 tick,则根据最新价格换算成最新的 tickstate.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 的地址,amount0amount1 是所要借贷的两个 token 数量,data 是给回调函数的参数。

还款则需在 uniswapV3FlashCallback 回调函数中完成。

最终,闪电贷赚取的手续费也是分配给 LP 和协议费。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/41170.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据治理新时代:筛斗数据如何推动企业数据价值的释放

【标题】数据治理新时代&#xff1a;筛斗数据如何引领企业数据价值的深度释放 在当今这个信息爆炸的时代&#xff0c;数据已成为企业最宝贵的资产之一&#xff0c;其潜在价值远超传统资源。然而&#xff0c;海量数据的涌现也带来了前所未有的挑战&#xff1a;如何高效、准确地…

【解码现代 C++】:实现自己的智能 【String 类】

目录 1. 经典的String类问题 1.1 构造函数 小李的理解 1.2 析构函数 小李的理解 1.3 测试函数 小李的理解 1.4 需要记住的知识点 2. 浅拷贝 2.1 什么是浅拷贝 小李的理解 2.2 需要记住的知识点 3. 深拷贝 3.1 传统版写法的String类 3.1.1 拷贝构造函数 小李的理…

共享门店模式:实体门店合伙制的解决方案

在当今这个快速迭代的商业时代&#xff0c;共享门店模式以其独到的商业智慧和灵活的运营策略&#xff0c;正逐步成为推动行业变革的重要力量。它巧妙地融合了共享经济的前沿理念与线下门店的实体优势&#xff0c;开辟了一条资源高效整合与价值深度挖掘的新路径。 共享门店模式…

量化交易对长期投资的影响

量化交易&#xff0c;通过其基于算法的决策过程&#xff0c;已成为现代金融市场的一个重要组成部分。对于长期投资&#xff0c;量化交易的影响是复杂且多维的&#xff0c;涉及市场效率、投资策略的优化以及风险管理等多个方面。 首先&#xff0c;量化交易通过提高交易速度和执…

大数据面试题之Presto[Trino](2)

描述Presto中的Connector是什么&#xff1f; 在Presto中&#xff0c;Connector是连接Presto查询引擎与外部数据存储系统的桥梁。它是一个插件化的组件&#xff0c;允许Presto与多种不同的数据源无缝集成&#xff0c;从而实现跨数据源的SQL查询。以下是Connector的一些关键特性…

MySQL学习(8):约束

1.什么是约束 约束是作用于表中字段上的规则&#xff0c;以限制表中数据&#xff0c;保证数据的正确性、有效性、完整性 约束分为以下几种&#xff1a; not null非空约束限制该字段的数据不能为nullunique唯一约束保证该字段的所有数据都是唯一、不重复的primary key主键约束…

Oracle中CREATE FORCE VIEW的说明和例子

Oracle数据库中的CREATE FORCE VIEW语句用于创建视图&#xff0c;即使在视图所依赖的基表或对象不存在&#xff0c;或者创建视图的用户对这些对象没有足够的权限时&#xff0c;也能强制创建视图。不过&#xff0c;需要明确的是&#xff0c;尽管视图能被强制创建&#xff0c;但在…

微信小程序毕业设计-走失人员的报备平台系统项目开发实战(附源码+论文)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;微信小程序毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计…

顺序表(1) 基础(详解+学习历程)

一、前言 顺序表是数据结构的一种。数据结构是计算机存储、组织数据的一种方式。 数据结构中一种称为线性结构的逻辑结构主要是对线性表的学习&#xff0c;线性表包含顺序表。线性表是对一类事物的集合的统称&#xff1b; 线性表不一定有物理结构&#xff0c;但是一定有逻辑…

Docker 安装迅雷NAS

一、前言 在本文之前&#xff0c;博主在家用服务器 CentOS 上使用的下载方案是 Aria2 和其前端面板 Ariang. 所下载的资源大多数是 BT 资源&#xff0c;奈何 Aria2 对 BT 资源的下载速度实在堪忧&#xff0c;配置 BT 服务器效果不佳且费时。每次都将 BT 资源云添加至迅雷云盘&…

NLP - Softmax与层次Softmax对比

Softmax Softmax是神经网络中常用的一种激活函数&#xff0c;用于多分类任务。Softmax函数将未归一化的logits转换为概率分布。公式如下&#xff1a; P ( y i ) e z i ∑ j 1 N e z j P(y_i) \frac{e^{z_i}}{\sum_{j1}^{N} e^{z_j}} P(yi​)∑j1N​ezj​ezi​​ 其中&#…

Github与本地仓库建立链接、Git命令(或使用Github桌面应用)

一、Git命令&#xff08;不嫌麻烦可以使用Github桌面应用&#xff09; git clone [] cd [] git branch -vv #查看本地对应远程的分支对应关系 git branch -a #查看本地和远程所有分支 git checkout -b [hongyuan] #以当前的本地分支作为基础新建一个【】分支,命名为h…

windows内置的hyper-v虚拟机的屏幕分辨率很低,怎么办?

# windows内置的hyper-v虚拟机的屏幕分辨率很低&#xff0c;怎么办&#xff1f; 只能这么大了&#xff0c;全屏也只是把字体拉伸而已。 不得不说&#xff0c;这个hyper-v做的很烂。 直接复制粘贴也做不到。 但有一个办法可以破解。 远程桌面。 我们可以在外面的windows系统&…

python解析Linux top 系统信息并生成动态图表(pandas和matplotlib)

文章目录 0. 引言1. 功能2.使用步骤3. 程序架构流程图结构图 4. 数据解析模块5. 图表绘制模块6. 主程序入口7. 总结8. 附录完整代码 0. 引言 在性能调优和系统监控中&#xff0c;top 命令是一种重要工具&#xff0c;提供了实时的系统状态信息&#xff0c;如 CPU 使用率、内存使…

0/1背包问题总结

文章目录 &#x1f347;什么是0/1背包问题&#xff1f;&#x1f348;例题&#x1f349;1.分割等和子集&#x1f349;2.目标和&#x1f349;3.最后一块石头的重量Ⅱ &#x1f34a;总结 博客主页&#xff1a;lyyyyrics &#x1f347;什么是0/1背包问题&#xff1f; 0/1背包问题是…

CFS三层内网渗透——第二层内网打点并拿下第三层内网(三)

目录 八哥cms的后台历史漏洞 配置socks代理 ​以我的kali为例,手动添加 socks配置好了&#xff0c;直接sqlmap跑 ​登录进后台 蚁剑配置socks代理 ​ 测试连接 ​编辑 成功上线 上传正向后门 生成正向后门 上传后门 ​内网信息收集 ​进入目标二内网机器&#xf…

小程序分包加载、独立分包、分包预加载等

一、小程序分包加载 小程序的代码通常是由许多页面、组件以及资源等组成&#xff0c;随着小程序功能的增加&#xff0c;代码量也会逐渐增加&#xff0c; 体积过大就会导致用户打开速度变慢&#xff0c;影响用户的使用体验。分包加载是一种小程序优化技术。将小程序不同功能的代…

如果在已经打开的网页里面,我点击了一个链接,这个链接在新的标签页打开,如何能得到新的标签页其返回的数据收发数据。

要在已打开的网页中点击一个链接并在新标签页中打开&#xff0c;然后捕获新标签页中的所有HTTP请求和响应&#xff0c;你可以使用Selenium库结合selenium-wire插件。下面是一个示例代码&#xff0c;展示了如何实现这一功能&#xff1a; python import json from selenium imp…

【微信小程序开发实战项目】——花店微信小程序实战项目(4)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

Nginx的安装与配置 —— Linux系统

一、Nginx 简介 1.1 什么是 Nginx Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器&#xff0c;在BSD-like 协议下发行。其特点是占有内存少&#xff0c;并发能力强&#xff0c;事实上nginx的并发能力在同类型的网页服务…