本文copy自 https://liaoph.com/uniswap-v3-2/
前文已經(jīng)說(shuō)過(guò) Uniswap v3 的代碼架構(gòu)。一般來(lái)說(shuō),用戶的操作都是從 uniswap-v3-periphery 中的合約開始。
創(chuàng)建交易對(duì)

function createAndInitializePoolIfNecessary(
address tokenA,
address tokenB,
uint24 fee,
uint160 sqrtPriceX96
) external payable returns (address pool) {
pool = IUniswapV3Factory(factory).getPool(tokenA, tokenB, fee);
if (pool == address(0)) {
pool = IUniswapV3Factory(factory).createPool(tokenA, tokenB, fee);
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
} else {
(uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0();
if (sqrtPriceX96Existing == 0) {
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
}
}
}
首先調(diào)用 UniswapV3Factory.getPool 方法查看交易對(duì)是否已經(jīng)創(chuàng)建,getPool 函數(shù)是 solidity 自動(dòng)為 UniswapV3Factory 合約中的狀態(tài)變量 getPool 生成的外部函數(shù),getPool 的數(shù)據(jù)類型為:
contract UniswapV3Factory is IUniswapV3Factory, UniswapV3PoolDeployer, NoDelegateCall {
...
mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;
...
}
使用 3個(gè) map 說(shuō)明了 v3 版本使用 (tokenA, tokenB, fee) 來(lái)作為一個(gè)交易對(duì)的鍵,即相同代幣,不同費(fèi)率之間的流動(dòng)池不一樣。另外對(duì)于給定的 tokenA 和 tokenB,會(huì)先將其地址排序,將地址值更小的放在前,這樣方便后續(xù)交易池的查詢和計(jì)算。
再來(lái)看 UniswapV3Factory 創(chuàng)建交易對(duì)的過(guò)程,實(shí)際上它是調(diào)用 deploy 函數(shù)完成交易對(duì)的創(chuàng)建:
function deploy(
address factory,
address token0,
address token1,
uint24 fee,
int24 tickSpacing
) internal returns (address pool) {
parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});
pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
delete parameters;
}
這里的 fee 和 tickSpacing 是和費(fèi)率及價(jià)格最小間隔相關(guān)的設(shè)置,這里只關(guān)注創(chuàng)建過(guò)程,費(fèi)率和 tick 的實(shí)現(xiàn)后面再來(lái)做介紹。
CREATE2
創(chuàng)建交易對(duì),就是創(chuàng)建一個(gè)新的合約,作為流動(dòng)池來(lái)提供交易功能。創(chuàng)建合約的步驟是:
pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
這里先通過(guò) keccak256(abi.encode(token0, token1, fee) 將 token0, token1, fee 作為輸入,得到一個(gè)哈希值,并將其作為 salt 來(lái)創(chuàng)建合約。因?yàn)橹付?salt, solidity 會(huì)使用 EVM 的 CREATE2 指令來(lái)創(chuàng)建合約。使用 CREATE2 指令的好處是,只要合約的 bytecode 及 salt 不變,那么創(chuàng)建出來(lái)的地址也將不變。
關(guān)于使用 salt 創(chuàng)建合約的解釋:Salted contract creations / create2
CREATE2指令的具體解釋可以參考:EIP-1014。solidity 在 0.6.2 版本后在語(yǔ)法層面支持了CREATE2. 如果使用更低的版本,可以參考 Uniswap v2 的代碼實(shí)現(xiàn)同樣的功能。
使用 CREATE2 的好處是:
- 可以在鏈下計(jì)算出已經(jīng)創(chuàng)建的交易池的地址
- 其他合約不必通過(guò)
UniswapV3Factory中的接口來(lái)查詢交易池的地址,可以節(jié)省 gas - 合約地址不會(huì)因?yàn)?reorg 而改變
不需要通過(guò) UniswapV3Factory 的接口來(lái)計(jì)算交易池合約地址的方法,可以看這段代碼。
新交易對(duì)合約的構(gòu)造函數(shù)中會(huì)反向查詢 UniswapV3Factory 中的 parameters 值來(lái)進(jìn)行初始變量的賦值:
constructor() {
int24 _tickSpacing;
(factory, token0, token1, fee, _tickSpacing) = IUniswapV3PoolDeployer(msg.sender).parameters();
tickSpacing = _tickSpacing;
maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(_tickSpacing);
}
為什么不直接使用參數(shù)傳遞來(lái)對(duì)新合約的狀態(tài)變量賦值呢。這是因?yàn)?CREATE2 會(huì)將合約的 initcode 和 salt 一起用來(lái)計(jì)算創(chuàng)建出的合約地址。而 initcode 是包含 contructor code 和其參數(shù)的,如果合約的 constructor 函數(shù)包含了參數(shù),那么其 initcode 將因?yàn)槠鋫魅雲(yún)?shù)不同而不同。在 off-chain 計(jì)算合約地址時(shí),也需要通過(guò)這些參數(shù)來(lái)查詢對(duì)應(yīng)的 initcode。為了讓合約地址的計(jì)算更簡(jiǎn)單,這里的 constructor 不包含參數(shù)(這樣合約的 initcode 將時(shí)唯一的),而是使用動(dòng)態(tài) call 的方式來(lái)獲取其創(chuàng)建參數(shù)。
最后,對(duì)創(chuàng)建的交易對(duì)合約進(jìn)行初始化:
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);
}
初始化主要是設(shè)置了交易池的初始價(jià)格(注意,此時(shí)池子中還沒有流動(dòng)性),以及費(fèi)率,tick 等相關(guān)變量的初始化。完成之后一個(gè)交易池就創(chuàng)建好了。
提供流動(dòng)性
在合約內(nèi),v3 會(huì)保存所有用戶的流動(dòng)性,代碼內(nèi)稱作 Position,提供流動(dòng)性的調(diào)用流程如下:

用戶還是首先和 NonfungiblePositionManager 合約交互。v3 這次將 LP token 改成了 ERC721 token,并且將 token 功能放到 NonfungiblePositionManager 合約中。這個(gè)合約替代用戶完成提供流動(dòng)性操作,然后根據(jù)將流動(dòng)性的數(shù)據(jù)元記錄下來(lái),并給用戶鑄造一個(gè) NFT Token.
省略部分非關(guān)鍵步驟,我們先來(lái)看添加流動(dòng)性的函數(shù):
struct AddLiquidityParams {
address token0; // token0 的地址
address token1; // token1 的地址
uint24 fee; // 交易費(fèi)率
address recipient; // 流動(dòng)性的所屬人地址
int24 tickLower; // 流動(dòng)性的價(jià)格下限(以 token0 計(jì)價(jià)),這里傳入的是 tick index
int24 tickUpper; // 流動(dòng)性的價(jià)格上線(以 token0 計(jì)價(jià)),這里傳入的是 tick index
uint128 amount; // 流動(dòng)性 L 的值
uint256 amount0Max; // 提供的 token0 上限數(shù)
uint256 amount1Max; // 提供的 token1 上限數(shù)
}
function addLiquidity(AddLiquidityParams memory params)
internal
returns (
uint256 amount0,
uint256 amount1,
IUniswapV3Pool pool
)
{
PoolAddress.PoolKey memory poolKey =
PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee});
// 這里不需要訪問 factory 合約,可以通過(guò) token0, token1, fee 三個(gè)參數(shù)計(jì)算出 pool 的合約地址
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
(amount0, amount1) = pool.mint(
params.recipient,
params.tickLower,
params.tickUpper,
params.amount,
// 這里是 pool 合約回調(diào)所使用的參數(shù)
abi.encode(MintCallbackData({poolKey: poolKey, payer: msg.sender}))
);
require(amount0 <= params.amount0Max);
require(amount1 <= params.amount1Max);
}
這里有幾點(diǎn)值得注意:
傳入的 lower/upper 價(jià)格是以 tick index 來(lái)表示的,因此需要在鏈下先計(jì)算好價(jià)格所對(duì)應(yīng)的 tick index
傳入的是流動(dòng)性 <nobr aria-hidden="true" style="box-sizing: border-box; transition: none 0s ease 0s; border: 0px; padding: 0px; margin: 0px; max-width: none; max-height: none; min-width: 0px; min-height: 0px; vertical-align: 0px; line-height: normal; text-decoration: none; white-space: nowrap !important;">L</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><mi>L</mi></math> 的大小,這個(gè)也需要在鏈下先計(jì)算好,計(jì)算過(guò)程見下面
我們不需要訪問 factory 就可以計(jì)算出 pool 的地址,實(shí)現(xiàn)原理見 CREATE2
這里有一個(gè)回調(diào)函數(shù)的參數(shù)。v3 使用回調(diào)函數(shù)來(lái)完成進(jìn)行流動(dòng)性 token 的支付操作,原因見下面
從 token 數(shù)計(jì)算流動(dòng)性 L



回調(diào)函數(shù)
使用回調(diào)函數(shù)原因是,將 Position 的 owner 和實(shí)際流動(dòng)性 token 支付者解耦。這樣可以讓中間合約來(lái)管理用戶的流動(dòng)性,并將流動(dòng)性 token 化。關(guān)于 token 化,Uniswap v3 默認(rèn)實(shí)現(xiàn)了 ERC721 token(因?yàn)榧词故峭粋€(gè)池子,流動(dòng)性之間差異也也很大)。
例如,當(dāng)用戶通過(guò) NonfungiblePositionManager 來(lái)提供流動(dòng)性時(shí),對(duì)于 UniswapV3Pool 合約來(lái)說(shuō),這個(gè) Position 的 owner 是 NonfungiblePositionManager,而 NonfungiblePositionManager 再通過(guò) NFT Token 將 Position 與用戶關(guān)聯(lián)起來(lái)。這樣用戶就可以將 LP token 進(jìn)行轉(zhuǎn)賬或者抵押類操作。
在 NonfungiblePositionManager 中回調(diào)函數(shù)的實(shí)現(xiàn)如下:
struct MintCallbackData {
PoolAddress.PoolKey poolKey;
address payer; // 支付 token 的地址
}
/// @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);
// 根據(jù)傳入的參數(shù),使用 transferFrom 代用戶向 Pool 中支付 token
if (amount0Owed > 0) pay(decoded.poolKey.token0, decoded.payer, msg.sender, amount0Owed);
if (amount1Owed > 0) pay(decoded.poolKey.token1, decoded.payer, msg.sender, amount1Owed);
}
postion 更新
接著我們看 UniswapV3Pool 是如何添加流動(dòng)性的。流動(dòng)性的添加主要在 UniswapV3Pool._modifyPosition 中,這個(gè)函會(huì)先調(diào)用 _updatePosition 來(lái)創(chuàng)建或修改一個(gè)用戶的 Position,省略其中的非關(guān)鍵步驟:
function _updatePosition(
address owner,
int24 tickLower,
int24 tickUpper,
int128 liquidityDelta,
int24 tick
) private returns (Position.Info storage position) {
// 獲取用戶的 Postion
position = positions.get(owner, tickLower, tickUpper);
...
// 根據(jù)傳入的參數(shù)修改 Position 對(duì)應(yīng)的 lower/upper tick 中
// 的數(shù)據(jù),這里可以是增加流動(dòng)性,也可以是移出流動(dòng)性
bool flippedLower;
bool flippedUpper;
if (liquidityDelta != 0) {
uint32 blockTimestamp = _blockTimestamp();
// 更新 lower tikc 和 upper tick
// fippedX 變量表示是此 tick 的引用狀態(tài)是否發(fā)生變化,即
// 被引用 -> 未被引用 或
// 未被引用 -> 被引用
// 后續(xù)需要根據(jù)這個(gè)變量的值來(lái)更新 tick 位圖
flippedLower = ticks.update(
tickLower,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
false,
maxLiquidityPerTick
);
flippedUpper = ticks.update(
tickUpper,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
true,
maxLiquidityPerTick
);
// 如果一個(gè) tick 第一次被引用,或者移除了所有引用
// 那么更新 tick 位圖
if (flippedLower) {
tickBitmap.flipTick(tickLower, tickSpacing);
secondsOutside.initialize(tickLower, tick, tickSpacing, blockTimestamp);
}
if (flippedUpper) {
tickBitmap.flipTick(tickUpper, tickSpacing);
secondsOutside.initialize(tickUpper, tick, tickSpacing, blockTimestamp);
}
}
...
// 更新 position 中的數(shù)據(jù)
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
// 如果移除了對(duì) tick 的引用,那么清除之前記錄的元數(shù)據(jù)
// 這只會(huì)發(fā)生在移除流動(dòng)性的操作中
if (liquidityDelta < 0) {
if (flippedLower) {
ticks.clear(tickLower);
secondsOutside.clear(tickLower, tickSpacing);
}
if (flippedUpper) {
ticks.clear(tickUpper);
secondsOutside.clear(tickUpper, tickSpacing);
}
}
}
先忽略費(fèi)率相關(guān)的操作,這個(gè)函數(shù)所做的操作是:
添加/移除流動(dòng)性時(shí),先更新這個(gè) Positon 對(duì)應(yīng)的 lower/upper tick 中記錄的元數(shù)據(jù)
更新 position
根據(jù)需要更新 tick 位圖
Postion 是以 owner, lower tick, uppper tick 作為鍵來(lái)存儲(chǔ)的,注意這里的 owner 實(shí)際上是 NonfungiblePositionManager 合約的地址。這樣當(dāng)多個(gè)用戶在同一個(gè)價(jià)格區(qū)間提供流動(dòng)性時(shí),在底層的 UniswapV3Pool 合約中會(huì)將他們合并存儲(chǔ)。而在 NonfungiblePositionManager 合約中會(huì)按用戶來(lái)區(qū)別每個(gè)用戶擁有的 Position.
Postion 中包含的字段中,除去費(fèi)率相關(guān)的字段,只有一個(gè)即流動(dòng)性 L:
library Position {
// info stored for each user's position
struct Info {
// 此 position 中包含的流動(dòng)性大小,即 L 值
uint128 liquidity;
...
}
更新 position 只需要一行調(diào)用:
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
其中包含了 position 中流動(dòng)性 L 的更新,以及手續(xù)費(fèi)相關(guān)的計(jì)算。
tick 管理
我們?cè)賮?lái)看 tick 相關(guān)的管理,在 UniswapV3Pool 合約中有兩個(gè)狀態(tài)變量記錄了 tick 相關(guān)的信息:
// tick 元數(shù)據(jù)管理的庫(kù)
using Tick for mapping(int24 => Tick.Info);
// tick 位圖槽位的庫(kù)
using TickBitmap for mapping(int16 => uint256);
// 記錄了一個(gè) tick 包含的元數(shù)據(jù),這里只會(huì)包含所有 Position 的 lower/upper ticks
mapping(int24 => Tick.Info) public override ticks;
// tick 位圖,因?yàn)檫@個(gè)位圖比較長(zhǎng)(一共有 887272x2 個(gè)位),大部分的位不需要初始化
// 因此分成兩級(jí)來(lái)管理,每 256 位為一個(gè)單位,一個(gè)單位稱為一個(gè) word
// map 中的鍵是 word 的索引
mapping(int16 => uint256) public override tickBitmap;
library Tick {
...
// tick 中記錄的數(shù)據(jù)
struct Info {
// 記錄了所有引用這個(gè) tick 的 position 流動(dòng)性的和
uint128 liquidityGross;
// 當(dāng)此 tick 被越過(guò)時(shí)(從左至右),池子中整體流動(dòng)性需要變化的值
int128 liquidityNet;
...
}
tick 中和流動(dòng)性相關(guān)的字段有兩個(gè) liquidityGross,liquidityNet。
liquidityNet 表示當(dāng)價(jià)格從左至右經(jīng)過(guò)此 tick 時(shí)整體流動(dòng)性需要變化的凈值。在單個(gè)流動(dòng)性中,對(duì)于 lower tick 來(lái)說(shuō),它的值為正,對(duì)于 upper tick 來(lái)說(shuō)它的值為 負(fù)。
如果有兩個(gè) position 中的流動(dòng)性相等,例如 L = 500,并且這兩個(gè) position 同時(shí)引用了一個(gè) tick,其中一個(gè)為 lower tick ,另一個(gè)為 upper tick,那么對(duì)于這個(gè) tick,它的 liquidityNet = 0。此時(shí)我們就需要有一種機(jī)制來(lái)判斷一個(gè) tick 是否仍然在被引用中。這里使用 liquidityGross 記錄流動(dòng)性的增值(不考慮 lower/upper),我們可以就通過(guò)流動(dòng)性變化前后 liquidityGross 是否等于 0 來(lái)判斷這個(gè) tick 是否仍被引用。
當(dāng)價(jià)格變動(dòng)導(dǎo)致 tickcurrent 越過(guò)一個(gè) position 的 lower/upper tick 時(shí),我們需要根據(jù) tick 中記錄的值來(lái)更新當(dāng)前價(jià)格所對(duì)應(yīng)的總體流動(dòng)性。假設(shè) position 的流動(dòng)性值為 ΔL,會(huì)有以下四種情況:

liquidityNet 中記錄的就是當(dāng)從左至右穿過(guò)這個(gè) tick 時(shí),需要增減的流動(dòng)性,當(dāng)其為 lower tick 時(shí),其值為正,當(dāng)其為 upper tick 時(shí),其值為負(fù)。對(duì)于從右至左穿過(guò)的情況,只需將 liquidityNet 的值去翻即可完成計(jì)算。
我再來(lái)看如何更新 tick 元數(shù)據(jù),以下是 tick.update 函數(shù):
function update(
mapping(int24 => Tick.Info) storage self,
int24 tick,
int24 tickCurrent,
int128 liquidityDelta,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
bool upper,
uint128 maxLiquidity
) internal returns (bool flipped) {
Tick.Info storage info = self[tick];
uint128 liquidityGrossBefore = info.liquidityGross;
uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);
require(liquidityGrossAfter <= maxLiquidity, 'LO');
// 通過(guò) liquidityGross 在進(jìn)行 position 變化前后的值
// 來(lái)判斷 tick 是否仍被引用
flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0);
...
info.liquidityGross = liquidityGrossAfter;
// 更新 liquidityNet 的值,對(duì)于 upper tick,
info.liquidityNet = upper
? int256(info.liquidityNet).sub(liquidityDelta).toInt128()
: int256(info.liquidityNet).add(liquidityDelta).toInt128();
}
此函數(shù)返回的 flipped 表示此 tick 的引用狀態(tài)是否發(fā)生變化,之前的 _updatePosition 中的代碼會(huì)根據(jù)這個(gè)返回值去更新 tick 位圖。
tick 位圖
tick 位圖用于記錄所有被引用的 lower/upper tick index,我們可以用過(guò) tick 位圖,從當(dāng)前價(jià)格找到下一個(gè)(從左至右或者從右至左)被引用的 tick index。關(guān)于 tick 位圖的管理,在 _updatePosition 中的:
if (flippedLower) {
tickBitmap.flipTick(tickLower, tickSpacing);
secondsOutside.initialize(tickLower, tick, tickSpacing, blockTimestamp);
}
if (flippedUpper) {
tickBitmap.flipTick(tickUpper, tickSpacing);
secondsOutside.initialize(tickUpper, tick, tickSpacing, blockTimestamp);
}
這里不做進(jìn)一步的說(shuō)明,具體代碼實(shí)現(xiàn)在TickBitmap庫(kù)中。tick 位圖有以下幾個(gè)特性:
- 對(duì)于不存在的 tick,不需要初始值,因?yàn)樵L問 map 中不存在的 key 默認(rèn)值就是 0
- 通過(guò)對(duì)位圖的每個(gè) word(uint256) 建立索引來(lái)管理位圖,即訪問路徑為 word index -> word -> tick bit
token 數(shù)確認(rèn)
_modifyPosition 函數(shù)在調(diào)用 _updatePosition 更新完 Position 后,會(huì)計(jì)算出此次提供流動(dòng)性具體所需的 x token 和 y token 數(shù)量。
function _modifyPosition(ModifyPositionParams memory params)
private
noDelegateCall
returns (
Position.Info storage position,
int256 amount0,
int256 amount1
)
{
...
Slot0 memory _slot0 = slot0; // SLOAD for gas optimization
position = _updatePosition(
...
);
...
}
這里插入一個(gè)題外話,這一行代碼:
Slot0 memory _slot0 = slot0; // SLOAD for gas optimization

function _modifyPosition(ModifyPositionParams memory params)
private
noDelegateCall
returns (
Position.Info storage position,
int256 amount0,
int256 amount1
)
{
...
if (params.liquidityDelta != 0) {
// 計(jì)算三種情況下 amount0 和 amount1 的值,即 x token 和 y token 的數(shù)量
if (_slot0.tick < params.tickLower) {
amount0 = SqrtPriceMath.getAmount0Delta(
// 計(jì)算 lower/upper tick 對(duì)應(yīng)的價(jià)格
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
} else if (_slot0.tick < params.tickUpper) {
// current tick is inside the passed range
uint128 liquidityBefore = liquidity; // SLOAD for gas optimization
...
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
);
}
}
}

function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) {
uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
require(absTick <= uint256(MAX_TICK), 'T');
// 這些魔數(shù)分別表示 1/sqrt(1.0001)^1, 1/sqrt(1.0001)^2, 1/sqrt(1.0001)^4....
uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000;
if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;
if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;
if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;
if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;
if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;
if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;
if (tick > 0) ratio = type(uint256).max / ratio;
// this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96.
// we then downcast because we know the result always fits within 160 bits due to our tick input constraint
// we round up in the division so getTickAtSqrtRatio of the output price is always consistent
sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));
}


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;
// 獲取當(dāng)前池中的 x token, y token 余額
if (amount0 > 0) balance0Before = balance0();
if (amount1 > 0) balance1Before = balance1();
// 將需要的 x token 和 y token 數(shù)量傳給回調(diào)函數(shù),這里預(yù)期回調(diào)函數(shù)會(huì)將指定數(shù)量的 token 發(fā)送到合約中
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);
// 回調(diào)完成后,檢查發(fā)送至合約的 token 是否復(fù)合預(yù)期,如果不滿足檢查則回滾交易
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);
}
這個(gè)函數(shù)關(guān)鍵的步驟就是通過(guò)回調(diào)函數(shù),讓調(diào)用方發(fā)送指定數(shù)量的 x token 和 y token 至合約中。
我們?cè)賮?lái)看 NonfungiblePositionManager.mint 的代碼:
function mint(MintParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (
uint256 tokenId,
uint256 amount0,
uint256 amount1
)
{
IUniswapV3Pool pool;
// 這里是添加流動(dòng)性,并完成 x token 和 y token 的發(fā)送
(amount0, amount1, pool) = addLiquidity(
AddLiquidityParams({
token0: params.token0,
token1: params.token1,
fee: params.fee,
recipient: address(this),
tickLower: params.tickLower,
tickUpper: params.tickUpper,
amount: params.amount,
amount0Max: params.amount0Max,
amount1Max: params.amount1Max
})
);
// 鑄造 ERC721 token 給用戶,用來(lái)代表用戶所持有的流動(dòng)性
_mint(params.recipient, (tokenId = _nextId++));
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})
);
// 用 ERC721 的 token ID 作為鍵,將用戶提供流動(dòng)性的元信息保存起來(lái)
_positions[tokenId] = Position({
nonce: 0,
operator: address(0),
poolId: poolId,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
liquidity: params.amount,
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
feeGrowthInside1LastX128: feeGrowthInside1LastX128,
tokensOwed0: 0,
tokensOwed1: 0
});
}
可以看到這個(gè)函數(shù)主要是將用戶的 Position 保存起來(lái),并給用戶鑄造 NFT token,代表其所持有的流動(dòng)性。至此提供流動(dòng)性的步驟就完成了。
流動(dòng)性的移除
移除流動(dòng)性就是上述操作的逆操作,在 core 合約中:
function burn(
int24 tickLower,
int24 tickUpper,
uint128 amount
) external override lock returns (uint256 amount0, uint256 amount1) {
// 先計(jì)算出需要移除的 token 數(shù)
(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);
// 注意這里,移除流動(dòng)性后,將移出的 token 數(shù)記錄到了 position.tokensOwed 上
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);
}
移除流動(dòng)性時(shí),還是使用之前的公式計(jì)算出移出的 token 數(shù),但是并不會(huì)直接將移出的 token 數(shù)發(fā)送給用戶,而是記錄在了 position 的 tokensOwed0 和 tokensOwed1 上。這樣做應(yīng)該是為了遵循實(shí)踐:Favor pull over push for external calls.