
慢雾:Uniswap v3 协议分析与审计要点
随着去中心化金融(DeFi) 的快速发展,Uniswap 作为领先的去中心化交易所一直走在创新的前沿。本文将深入分析 Uniswap v3 协议的核心机制,并详细解读其功能设计,包括集中流动性、多重费率、代币兑换及闪电贷等关键功能,同时为审计人员提供相关的审计要点。(注:本文中的图片可在 https://www.figma.com/board/QyIpAUR93MxZ4XZZf2QjDk/uniswap-v3 查看高清版,点击阅读原文可直接跳转。)
架构简析
Uniswap v3 协议主要由四个模块组成:
-
PositionManager:用户进行流动性操作的主要接口,用户可以通过它创建代币池、提供/移除流动性,并使用 ERC721 作为流动性提供者(LP) 的凭证。
-
SwapRouter:用户进行代币交换的入口,用户可以通过该模块完成代币的交换操作。
-
Pool:负责实现代币交易、流动性管理、收取交易手续费,以及 Oracle 数据的管理功能。其中,Tick 机制将价格范围划分为多个精细的刻度。
-
Factory:用于创建和管理 Pool 合约。
流程梳理
创建代币对
用户可以通过 createAndInitializePoolIfNecessary 函数来完成。用户需传入代币对的 token0、token1、手续费(fee) 以及初始价格()。首先,系统会通过 getPool 函数检查该代币对是否已存在,如果尚未创建,则调用 createPool,并使用 CREATE2 指令进行交易对的部署。最后,通过 initialize 函数完成价格、手续费、tick、预言机等相关参数的初始化。
提供流动性
用户可以通过 mint 函数创建新的流动性头寸并生成对应的 NFT,或通过 increaseLiquidity 函数为现有的 NFT 流动性头寸增加流动性。首先,系统会检查交易是否在规定的时间范围内执行,然后调用 addLiquidity 函数完成具体操作。在该函数中,首先计算出池子的地址和流动性的大小,接着调用 _updatePosition 更新用户的 Position,修改 lower、upper tick 以及累计的手续费总额。随后,系统通过 _modifyPosition 添加流动性,确保 tick 满足上下限条件,返回计算出的 token0 和 token1 数量(int256),并将其发送到池中。最后,系统根据用户的 tokenId 更新对应的 Position 信息。
移除流动性
用户可以通过 decreaseLiquidity 函数来移除流动性。首先,系统会检查 LP 凭证的权限以及交易的时间有效性。在确保池子拥有足够流动性的前提下,调用 burn 函数来移除流动性。随后,系统会核实实际移除的代币数量是否满足用户设定的最小限度要求,并相应地更新用户的 Position 信息。
swap
用户可以通过 exactInput 函数指定支付的 token 数量以及期望获得的最小 token 数量,或通过 exactOutput 函数指定支付的最大 token 数量并设定期望获得的 token 数量。系统首先解析路径(path),然后依次调用 exactInputInternal 或 exactOutputInternal 函数完成每一步的 swap 操作。
在 swap 函数中,系统首先锁定 unlocked 状态,防止其他交易干扰状态变量的更新。进入循环后,系统通过 tick 找到下一个交易价格,并调用 computeSwapStep 函数计算每一步的交换,直到 tokenIn 或 tokenOut 达到用户预期。同时,系统会更新手续费、流动性、tick 以及价格的相关值。如果 tick 发生变化,还需要更新 Oracle 数据。完成这些操作后,系统将 tokenOut 支付给用户,用户再通过回调函数 uniswapV3SwapCallback 支付 tokenIn,这种机制可以被视为一种闪电交换(flash swap)。随后,系统会检查合约余额是否匹配,并在确认无误后解锁 unlocked 状态。
当路径中的所有 swap 操作都完成,且交易符合用户的预期时,交易即成功结束。
flash
用户可以通过 flash 函数来进行闪电贷操作。首先,系统会计算借贷的手续费,然后将用户所需的 token 发送到指定的借贷地址。接下来,系统回调用户实现的 uniswapV3FlashCallback 函数,用户在此函数中完成还款操作。系统会在回调后检查合约余额的变化,确保其与用户借贷的数量相符,同时更新相应的手续费。除了 flash 函数,用户也可以通过 swap 操作实现类似的闪电贷功能,即在交易过程中先借入再偿还 token。
审计要点
1. 检查 swap 操作后是否有调用 refundETH
在 exactInput 函数中,用户需要指定支付的 token 数量和预期获得的最小 token 数量。在调用 uniswapV3SwapCallback 之前,系统会重新计算 amount0 和 amount1,以确保用户可以精确地发送 token。然而,当使用 ETH 进行交换时,用户需要随交易一起发送 ETH。即便在交易过程中未使用完所有的 ETH,函数不会自动退回多余部分。exactInput 函数仅返回 amountOut,因此交易者无法直接得知此次交换实际消耗了多少 ETH。
此外,任何人都可以调用 refundETH 函数,从合约中提取未使用的 ETH。因此,建议检查 swap 操作后是否调用 refundETH 以防止用户未使用的 ETH 遗留在协议中,或使用 MultiCall 函数在一次操作中完成多个函数的调用。
function refundETH() external payable override { if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance); }
2. 检查是否实现 TWAP 来获取预言机价格
当将 Uniswap 作为价格来源时,外部协议直接访问 Slot0 获取 sqrtPriceX96 可能存在价格操纵的风险。攻击者能通过 swap 等方式操纵流动性池的状态,从而在执行交易时获得有利的价格。
为了降低这种风险,建议开发者进一步实现时间加权平均价格(TWAP) 来获取价格,因为 TWAP 能有效减少短期内价格的剧烈波动影响,使操纵价格的难度增加。
function observe(
Observation[65535] storage self,
uint32 time,
uint32[] memory secondsAgos,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) internal view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) {
require(cardinality > 0, 'I');
tickCumulatives = new int56[](secondsAgos.length);
secondsPerLiquidityCumulativeX128s = new uint160[](secondsAgos.length);
for (uint256 i = 0; i < secondsAgos.length; i++) {
(tickCumulatives[i], secondsPerLiquidityCumulativeX128s[i]) = observeSingle(
self,
time,
secondsAgos[i],
tick,
index,
liquidity,
cardinality
);
}
}
3. 建议允许用户自行设置滑点参数
当其他协议使用 Uniswap v3 进行 swap 操作时,建议开发者根据业务场景设置滑点保护,并允许用户自行调整参数,以防止遭受三明治攻击。在此 swap 函数中,第四个参数 sqrtPriceLimitX96 用于指定用户愿意执行交换的最低或最高价格。这一参数可有效防止在交易过程中价格出现极端波动,从而降低用户因滑点过大而产生的损失。
function swap( address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) external override noDelegateCall returns (int256 amount0, int256 amount1) { ...}
4. 建议引入流动性池白名单机制
在 Uniswap v3 中,基于不同的手续费(fee),同一对 ERC20 代币可能同时存在多个流动性池(Pool)。通常,少数流动性池拥有绝大部分的流动性,而其他池的总锁仓量(TVL) 可能非常少,甚至尚未创建。这些 TVL 较低的池更容易成为价格操纵的目标。
因此,项目方在选择使用流动性池数据时,应该避免简单地以 LP 为数据源。为确保数据的可靠性,建议引入白名单机制,筛选出流动性充足且较难操纵的池。这种机制可以显著降低风险,确保价格引用数据的安全性和准确性,同时防止因 TVL 过低的池被操纵而引发的潜在损失。
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); }
5. 检查是否在 TickMath.sol、FullMath.sol 和 Position.sol 中使用 unchecked
TickMath、FullMath 和 Position 等模块在 Uniswap v3 中用于执行复杂的数学计算,这些计算依赖于 Solidity 中的溢出处理机制。在早期的 Solidity 版本(<0.8.0)中,整数溢出和下溢行为默认不抛出异常,因此代码可以基于这种假设进行正常运行。然而,自 Solidity 0.8.0 版本开始,溢出和下溢会自动抛出异常,这会影响现有代码的执行。为确保这些模块在 Solidity 0.8.0 及更高版本中正常运行,开发者需要在特定函数中使用 unchecked 代码块,手动禁用溢出检查。这可以恢复之前版本中的行为,并确保高效执行溢出敏感的运算。
官方已经针对 Solidity 0.8.0 及更高版本做了相应的支持和调整,详情可参见此更新(https://github.com/Uniswap/v3-core/commit/6562c52e8f75f0c10f9deaf44861847585fc8129)。这一改动确保在新版编译器下,TickMath、FullMath 和其他相关模块能够继续正确运行。
6. 检查 path 编码解码方式是否相同
在 Uniswap v3 的 exactInput 和 exactOutput 函数中,用户需要输入 path 参数,该路径必须按照固定格式进行编码和解码,即 tokenA-fee-tokenB,用于逐步进行代币交换操作。这个路径结构明确指定了每一跳交易中涉及的两个代币以及它们之间的手续费级别。如果外部协议在使用 Uniswap v3 的代币交换功能时选择了不同的路径解码方式,可能会导致与 Uniswap 预期的路径格式不符。这种情况下,协议可能无法正确解析路径,从而无法成功执行预期的代币交换操作。
因此,建议开发者在集成 Uniswap v3 的代币交换功能时,确保外部协议严格遵循 Uniswap 的路径编码规则。为防止出现路径解码错误,外部协议应在调用 exactInput 和 exactOutput 时,仔细检查 path 参数的格式,以避免交易失败或获得意外的结果。
function decodeFirstPool(bytes memory path) internal pure returns ( address tokenA, address tokenB, uint24 fee ) { tokenA = path.toAddress(0); fee = path.toUint24(ADDR_SIZE); tokenB = path.toAddress(NEXT_OFFSET); }
7. 检查代币顺序是否影响项目逻辑
在 Uniswap 中,token0 是排序顺序较低的代币,用作基础代币(base token),而 token1 是排序顺序较高的代币,用作报价代币(quote token)。Uniswap 会根据两个代币的地址按字典序进行排序,确保代币对的顺序在池子中始终保持一致。
然而,由于同一代币在不同区块链网络上的合约地址可能不同,尤其是跨链部署的合约,代币的排序顺序可能会发生变化。这种变化会导致 token0 和 token1 的角色互换,从而影响价格表现。例如,在某些链上,特定代币可能是 token0,但在其他链上,它可能被排序为 token1,导致基础代币和报价代币的关系不同,最终影响显示的价格。因此,建议开发者检查代币顺序是否会影响项目逻辑,特别是在跨链环境中,务必考虑代币顺序可能导致的价格问题,以避免对价格表现和交易逻辑产生不利影响。
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
总结
上述基础检查项基于 Uniswap v3 当前版本,供审计人员对与 Uniswap v3 有交互的项目进行检查。不同项目的实现各具特点,因此审计人员需深入理解协议,并根据实际情况进行严格检查。对于正在开发的项目,慢雾安全团队建议开发者在开发过程中认真考虑这些检查项,以确保协议的安全性和可靠性。
比推快讯
更多 >>- 美民主党参议员提出修正案:拟禁止官员通过推广加密货币获利
- 马斯克呼吁在美成立新政党,继续抨击“大而美”法案
- 黄金创2007年以来最大半年涨幅
- 欧盟将接受特朗普提出的统一关税方案,但寻求关键豁免
- 特朗普本周将与贸易团队会面,以确定国家关税税率
- 特朗普:利率应降至1%,鲍威尔及其委员会都难辞其咎
- 高盛将美联储降息预期时间提前至9月
- Robinhood(HOOD)涨幅扩大至 9.39%,续创历史新高
- Robinhood 计划到年底前将支持的代币化美股种类扩展至“数千种”
- Robinhood 正向欧洲用户赠送 OpenAI 和 SpaceX 代币
- Robinhood 旗下 Layer 2 暂定名为 Robinhood Chain
- 市场消息:REX-OSPREY SOL 现货 ETF 将于周三开始交易
- 消息人士:白宫拟推动将 Cynthia Lummis 提出的条款纳入《美丽大法案》
- Robinhood:ETH 和 SOL 质押服务已面向美国客户开放
- 当前主流 CEX、DEX 资金费率显示市场仍处于看跌态势
- “内幕哥”40 倍比特币和 25 倍以太坊空单目前整体浮盈 19.7 万美元
- 两家韩国今日机构已申请韩元稳定币商标
- 美联储博斯蒂克:没必要通过加息来应对通胀
- 比特币早期布道者 Lou Kerner 加盟播客平台 PodcastOne 担任加密顾问
- WLFI:稳定币 USD1 的 24 小时交易量超越 USDC,达 33.7 亿美元续创新高
- WeWork 联创拟重启区块链碳信用平台 Flowcarbon 的代币发行计划
- Origin Protocol:拟用协议收入回购 OGN 代币
- 白宫经委会主任哈塞特:美联储可能会降低利率
- 美联储博斯蒂克:预计明年将有三次降息
- 某新建地址从 Coinbase 提币 2.46 万枚 SOL 并进行质押
- Metaplanet CEO:今年二季度比特币收益率达到 129.4%
- Robinhood(HOOD)上涨 6.72%再创历史新高,11 时将公布加密相关重要公告
- 彭博 ETF 分析师:支持质押的 SOL 现货 ETF 或将于本周上线
- CertiK 安全报告:钓鱼攻击造成近 4 亿美元损失,成 2025 Q2 最大威胁
- 日本加密交易所运营商 BACKSEAT 完成种子轮融资,累计融资达约 969 万美元
- 1inch 宣布钱包新增支持 Solana
- 美财长:稳定币立法或将于 7 月中旬完成
- 比特币挖矿难度下调 7.48%至 116.96 T
- 美国财长贝森特:关税可能回退至 4 月 2 日水平,由特朗普决定
- 美财长:7 月 9 日前一周将出现一波贸易协议的密集签署
- 美国财长贝森特:即将寻找美联储主席鲍威尔的继任者
- 美国财长贝森特:可能会看到利率下降,通胀非常温和
- 美财长贝森特:特朗普预计将在 7 月 4 日前签署税收法案
- 美股加密货币股普涨,Circle(CRCL) 涨 3.74%
- 最高法院驳回保护 Coinbase 用户数据免受美国国税局审查的请求
- 不丹王国政府地址 20 分钟前将 137 枚 BTC 存入 Binance
- Amber International 签署总额为 2550 万美元的私募认购协议
- 德国储蓄银行拟向私人客户开放加密货币交易服务
- Aptos Labs CEO Avery Ching 被任命为美国 CFTC 数字资产小组委员
- Greeks.live:多数交易者认为空头清算密集可能推动 BTC 价格至 11.3 万美元甚至创新高
- XRP Ledger 的以太坊兼容侧链已在主网上线
- Fundstrat 联创 Tom Lee 被任命为 BitMine 董事长
- 观点:市场对美联储降息前景担忧,本周非农数据或为美指提供支撑
- 罗素 200 强价值指数新增 Strategy
- 拉斯维加斯巨型球 MSG Sphere 展示比特币 Logo