值得信赖的区块链资讯!
uniswap – V3 源代码导读
理解了 uniswap V3 的技术白皮书,看对应的源代码相对轻松。uniswap V3 的逻辑复杂一些,代码写的还是比较清晰。强烈建议,先理解 uniswap V3 的技术白皮书,再查看源代码:
uniswap V3 的智能合约的代码链接如下:
https://github.com/Uniswap/uniswap-v3-core
https://github.com/Uniswap/uniswap-v3-periphery
1总体框架
和 V2 的代码逻辑一致,整个功能分成两部分:核心功能 (core) 和辅助功能 (periphery)。两个部分的关系如下:

辅助功能也分为两个部分:交易池 (Position) 管理和 swap 路由管理。NonfungiblePositionManager 负责交易池的创建以及流动性的添加删除。SwapRouter 是 swap 路由的管理。UniswapV3Factory 是交易池 (UniswapV3Pool) 统一创建的接口。UniswapV3Pool 由 UniswapV3PoolDeployer 统一部署。UniswapV3Pool 是核心逻辑,管理了 Tick 和 Position,实现流动性管理以及一个交易池中 swap 功能实现。每个 Pool 中的 Position 都做成了 ERC721 的 Token。也就是说,每个 Position 都有独立的 ERC721 的 Token ID。
2创建交易池 (Pool)
NonfungiblePositionManager 负责交易池的创建以及流通性的添加 / 删除。先介绍一些全局变量的定义:
/// @dev IDs of pools assigned by this contract
mapping(address => uint80) private _poolIds;
/// @dev Pool keys by pool ID, to save on SSTOREs for position data
mapping(uint80 => PoolAddress.PoolKey) private _poolIdToPoolKey;
/// @dev The token ID position data
mapping(uint256 => Position) private _positions;
/// @dev The ID of the next token that will be minted. Skips 0
uint176 private _nextId = 1;
/// @dev The ID of the next pool that is used for the first time. Skips 0
uint80 private _nextPoolId = 1;
每一个 Pool 都有一个唯一编号,编号从 1 开始(_nextPoolId)。_poolIds 记录所有交易池的地址和编号的对应关系。每个交易池的关键信息由 PoolKey 表示(定义在 libraries/PoolAddress.sol):
struct PoolKey {
address token0;
address token1;
uint24 fee;
}
每个交易池由交易池的两个 Token 以及收取的费用唯一标示。_poolIdToPoolKey 记录交易池编号和 PoolKey 的对应关系。
所有交易池中的 Position 都归总管理,并赋予一个全局唯一的编号(_nextId),从 1 开始。 每个 Position 由创建地址以及边界唯一确定:
function compute(
address owner,
int24 tickLower,
int24 tickUpper
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(owner, tickLower, tickUpper));
}
接着看看 NonfungiblePositionManager 的构造函数:
constructor(
address _factory,
address _WETH9,
address _tokenDescriptor_
) ERC721Permit('Uniswap V3 Positions NFT-V1', 'UNI-V3-POS', '1') PeripheryImmutableState(_factory, _WETH9) {
_tokenDescriptor = _tokenDescriptor_;
}
_factory 是核心功能 (core) 中的 UniswapV3Factory 的地址。_WETH9 是 ETH 智能合约的地址。__tokenDescriptor_是 ERC721 描述信息的接口地址。
通过 createAndInitializePoolIfNecessary 函数创建一个交易池:
function createAndInitializePoolIfNecessary(
address tokenA,
address tokenB,
uint24 fee,
uint160 sqrtPriceX96
) external payable override returns (address pool) {
逻辑比较简单,通过 UniswapV3Factory 查看是否已经存在对应的交易池,如果没有,创建交易池,如果有了但是还没有初始化,初始化交易池。深入查看两个函数:createPool 和每个交易池的 initialize 函数。
-
createPool
核心逻辑是调用 UniswapV3PoolDeployer 的 deploy 函数创建 UniswapV3Pool 智能合约并设置两个 token 信息,交易费用信息和 tick 的步长信息:
pool = deploy(address(this), token0, token1, fee, tickSpacing);
接着查看 deploy 函数,创建 UniswapV3Pool 智能合约。注意每个交易池的地址的设置,是 token0/token1/fee 的编码后的结果。也就是说,每个交易池有唯一的地址,并且和 PoolKey 信息保持一致。通过这种方法,从 PoolKey 信息可以反推出交易池的地址。
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;
}
-
initialize
每个交易池的 initialize 函数初始化交易池的参数和状态。所有交易池的参数和状态用一个数据结构 Slot0 来记录:
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;
}
/// @inheritdoc IUniswapV3PoolState
Slot0 public override slot0;
注意的是,在初始化的时候,初始化了交易价格。这样可以把所有流动性的添加逻辑统一。
3添加流动性
NonfungiblePositionManager 的 mint 函数实现初始的流动性的添加。increaseLiquidity 函数实现了流动性的增加。这两个函数的逻辑基本一致,都是通过调用 addLiquidity 函数实现。mint 需要额外创建 ERC721 的 token。
addLiquidity 实现在 LiquidityManagement.sol:
struct AddLiquidityParams {
address token0;
address token1;
uint24 fee;
address recipient;
int24 tickLower;
int24 tickUpper;
uint128 amount;
uint256 amount0Max;
uint256 amount1Max;
}
/// @notice Add liquidity to an initialized pool
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});
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
流动性添加的核心逻辑由交易池的 mint 函数实现。mint 函数又是由两个子函数实现:_modifyPosition 和_updatePosition。
-
_updatePosition
为了便于计算,流动性的状态更新是通过流动性 (position) 边界上的 Tick 的 liquidityNet 来表示:
function _updatePosition(
address owner,
int24 tickLower,
int24 tickUpper,
int128 liquidityDelta,
int24 tick
) private returns (Position.Info storage position) {
_updatePosition 主要就是更新 Poisition 对应边界的 Tick 信息:
flippedLower = ticks.update(
tickLower,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
false,
maxLiquidityPerTick
);
flippedUpper = ticks.update(
tickUpper,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
true,
maxLiquidityPerTick
);
-
_modifyPosition
除了更新 Tick 信息外,_modifyPosition 需要计算在当前价格情况下一定流动性对应资金金额。当前的价格存在_slot0.tick 中,所以大体的逻辑如下:
if (_slot0.tick < params.tickLower) {
...
} else if (_slot0.tick < params.tickUpper) {
...
liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);
} else {
...
}
具体的计算公式可以查看技术白皮书的 6.29 和 6.30 公式。值得注意的是,在添加流动性时,如果添加的流动性包括当前的价格,当前的流动性需要更新。也就是上述代码的 liquidity 的更新。每个交易池中的 liquidity 保存了当前价格对应的流动性总和。
交易池的 mint 函数只是实现了当前价格下添加对应流动性的两种 Token 的金额的计算。代币的转账通过 uniswapV3MintCallback 函数实现。
4删除流动性
删除流动性的逻辑,和添加流动性的逻辑调用关系类似,调用交易池的 burn 函数。burn 函数的核心也是调用_modifyPosition 函数实现流动性的调整。_modifyPosition 函数实现了正负流动性的调整。
在删除完流动性后,每个流动性对应需要取回的资金金额暂时存储在 tokensOwed0 和 tokensOwed1 变量:
position.tokensOwed0 +=
uint128(amount0) +
uint128(
FullMath.mulDiv(
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
position.liquidity,
FixedPoint128.Q128
)
);
position.tokensOwed1 +=
uint128(amount1) +
uint128(
FullMath.mulDiv(
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
position.liquidity,
FixedPoint128.Q128
)
);
如果某个流动性为 0,并且所有的手续费已经收取,可以通过 NonfungiblePositionManager 的 burn 函数删除该流动性对应的 ERC721 的 Token 。
5Swap 流程
swap 的逻辑实现在 SwapRouter.sol,实现了多条路径互连 swap 逻辑。总共有两套函数:
-
exactInputSingle/exactInput
-
exactOutputSingle/exactOutput
exactInputSingle 和 exactOutputSingle 是单交易池的 swap 函数,一个是从指定 swap 的输入金额,换取一定的输出,一个是指定 swap 的输出金额,反推需要多少输入金额。
无论是 exactInputSingle,还是 exactOutputSingle,最终都是调用交易池的 swap 函数:
function swap(
address recipient,
bool zeroForOne,
int256 amountSpecified,
uint160 sqrtPriceLimitX96,
bytes calldata data
) external override noDelegateCall returns (int256 amount0, int256 amount1) {
recipient 是发起 swap 的发送地址,zeroForOne 的意思是,是否是 Token0 转换为 Token1,amountSpecified 是需要转换的金额,sqrtPriceLimitX96 是价格上限。
exactInput 还是 exactOutput 通过传入的金额正负进行区分:
bool exactInput = amountSpecified > 0;
整个函数的主体由一个 while 循环组成。也就是说,swap 过程分解成多个小步骤,一点点的调整当前的 Tick,直到满足所有的交易量:
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
-
计算下一个可能的 Tick,并更新价格
(step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
state.tick,
tickSpacing,
zeroForOne
);
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
-
计算swap的Token0/Token1以及交易费用
(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
);
在一个价格范围内的 Token0/Token1 量的变化,可以通过 getAmount0Delta/getAmount1Delta 函数(SqrtPriceMath.sol)计算,也就是 6.14/6.16 的公式。
-
计算费用
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信息
int128 liquidityNet =
ticks.cross(
step.tickNext,
(zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
(zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128)
);
在 swap 完成后,结合 IUniswapV3SwapCallback 接口实现 Swap 的两种代币转账:
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 (exactInput/exactOutput)是在 exactInputSingle/exactOutputSingle 的基本上构建而成。
6提取交易费
NonfungiblePositionManager 提供了 collect 函数提取手续费。每个 Position 中记录在流动性不变的情况下的一定时间内的费用增长率(feeGrowthInside)。在每个 Position 更新流动性时会更新一次增长率。如果不更新流动性,在提取交易费时,先调用交易池的 burn 函数更新一下增长率,并主动计算出可以收取的手续费:
pool.burn(position.tickLower, position.tickUpper, 0);
再调用交易池的 collect 函数,完成交易费的收取。
(amount0, amount1) = pool.collect(recipient, position.tickLower, position.tickUpper, amount0Max, amount1Max);
总结:
uniswap V3 的核心是在一定区间提供流动性。相对 V2,代码复杂度增加不少。整个代码主要分为两部分:核心逻辑和辅助功能。核心逻辑又分为两部分:交易池以及 Position 的管理和 Swap 功能逻辑。交易池中的每个 Position 设计并实现成 ERC721 的 Token。Swap 核心逻辑在 Tick 以及 Position 的管理的基础上实现。
比推快讯
更多 >>- 中国互联网金融协会发布《关于 OpenClaw 在互联网金融行业应用安全的风险提示》
- 下周宏观展望:七大央行轮番轰炸市场,英伟达 GTC 2026 大会召开
- Santiment:比特币鲸鱼群体近期持续增持,推动比特币站上 7.1 万美元
- 数据:分析:比特币鲸鱼近期重启积累,预计修正仍将持续
- Claude:未来两周非高峰时段及周末消息限额自动翻倍
- 曾做多价值 8400 万美元 BTC 及 ETH 鲸鱼平仓,转而现货增持 ETH
- 数据:当前加密恐慌贪婪指数为 14,处于极度恐慌状态
- 两艘印度油轮穿过霍尔木兹海峡,Hyperliquid 原油价格短时跌破 100 美元
- 特朗普称与泽连斯基达成协议“困难得多”
- MetaDAO 将于 3 月 26 日上线 P2P.me,最低融资目标 600 万美元
- 贝莱德数字资产主管:全球 ETF 前 20 仅比特币 ETF 亏损,90%投资者越跌越买
- 分析:G7史诗级石油释储或无法解决石油危机
- 数据:LA 涨近 10%,多个代币出现探底回升
- 数据:164.41 枚 BTC 从匿名地址转出,经中转后转至另一匿名地址
- RootData:ID 将于一周后解锁价值约 287 万美元的代币
- 加密交易所 BITGIN 洗钱案主犯在中国台湾被起诉,涉案金额逾 1.5 亿新台币
- Vitalik:应重新审视以太坊信标链与执行客户端分离架构
- Erik Voorhees 再次加仓黄金代币,目前仓位价值 2376 万美元,均价 4896 美元
- 英国前首相称比特币为“旁氏骗局”,认为其价值依赖市场信心
- 英国前首相称比特币为旁氏骗局,EricTrump 发文反驳
- 比特币突破 7.1 万美元,以太坊突破 2100 美元
- 英国前首相称比特币是旁氏骗局,Eric Trump 带头驳斥
- 以太坊合并后流通量已增加超 100 万枚,年化通胀率约为 0.24%
- 金融博客零对冲,VIX 指数与整体波动性脱节严重
- Anduril 创始人:美国缺乏对伊朗发动地面战的政治意愿,美国需要从世界警察角色中转型
- Scam Sniffer:某地址因签署钓鱼邮件损失逾 72 万美元 valBUSD 与 valTUSD
- 数据:Hyperliquid 平台鲸鱼当前持仓 34.18 亿美元,多空持仓比为 1.01
- USDH 发行方 Native Markets 将推出代币化保证金 pmUSDH
- 高端奢侈珠宝和腕表品牌 Jacob&Co.推出可挖矿腕表,限量 100 只
- Tether 近几个月内高频出手投资,已披露金额超 16 亿美元
- 中国山东女警撕开虚拟货币地下钱庄黑幕:锁定 12 名主要犯罪嫌疑人、100 余个资金账户
- 某鲸鱼卖出 634 枚 XAUT,获利约 25 万美元
- 分析:标普 500 期货流动性已较历史均值低 61%,数百万美元订单即可推动指数波动
- Galaxy 研究主管:若加密法案 4 月底前未过参议院委员会审议,2026 年通过概率将大幅下降
- Aave 正就“将年回购预算从 5000 万美元下调至 3000 万美元”进行投票
- 瑞士邮政银行扩展加密交易服务,新增 ARB、NEAR、SUI 等资产
- Aave 将推出 Aave Shield 功能,默认阻止价格影响超过 25%的 Swap
- 消息人士称美伊均无意停火,中东战事或持久化
- 以太坊基金会 OTC 向 BitMNR 出售 5000 枚 ETH,均价 2043 美元
- 巨鲸卖出 50 枚 BTC 买入 1693 枚 ETH,并 10 倍杠杆做多 LINK
- 美国法院维持美联储裁决,驳回 Custodia 申请主账户重审请求
- 特朗普:条件还不够好 目前并不准备与伊朗达成协议(金十数据 APP)
- 数据:ETH 当前全网 8 小时平均资金费率为 -0.0014%
- BTC 突破 71000 USDT,24H 涨幅 0.31%
- Polymarket 加密货币板块新增 DOGE、BNB、HYPE 价格预测
- 以色列拦截导弹库存被曝“严重不足”
- 以太坊基金会向 BitMine 出售 5,000 枚 ETH,价值约 1022 万美元
- 数据:过去 24 小时加密货币市值前 100 代币涨跌
- 数据:过去 24 小时全网爆仓 1.78 亿美元,主爆多单
- 黎巴嫩和以色列将在未来几天举行直接会谈
比推专栏
更多 >>- 懂王:登陸那個島|0314 Middle East
- 懂王:那就大家一起難受吧|0313亞盤後
- 当黄金被「困」在迪拜,是时候旗帜鲜明「唱多」香港了
- 東大、波斯、阿拉伯【第七次/進展/能源變量】|0310東3.5
- 从 HSK 到 USDGO:香港两大持牌机构,开始「脱钩」
- There is no new boss YET
- New situation and new games|0305 Asian
- B52 Were on the way to Iran|0304 Middle East
- 开放独角兽门票:从 Robinhood 到 MSX,一场 Pre-IPO 的链上平权实验
- Big player's 『Trigger moment』|0227Europe
观点
比推热门文章
- 下周宏观展望:七大央行轮番轰炸市场,英伟达 GTC 2026 大会召开
- Santiment:比特币鲸鱼群体近期持续增持,推动比特币站上 7.1 万美元
- 数据:分析:比特币鲸鱼近期重启积累,预计修正仍将持续
- Claude:未来两周非高峰时段及周末消息限额自动翻倍
- 曾做多价值 8400 万美元 BTC 及 ETH 鲸鱼平仓,转而现货增持 ETH
- 数据:当前加密恐慌贪婪指数为 14,处于极度恐慌状态
- 两艘印度油轮穿过霍尔木兹海峡,Hyperliquid 原油价格短时跌破 100 美元
- 特朗普称与泽连斯基达成协议“困难得多”
- MetaDAO 将于 3 月 26 日上线 P2P.me,最低融资目标 600 万美元
- 贝莱德数字资产主管:全球 ETF 前 20 仅比特币 ETF 亏损,90%投资者越跌越买
比推 APP



