慢雾: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 有交互的项目进行检查。不同项目的实现各具特点,因此审计人员需深入理解协议,并根据实际情况进行严格检查。对于正在开发的项目,慢雾安全团队建议开发者在开发过程中认真考虑这些检查项,以确保协议的安全性和可靠性。
比推快讯
更多 >>- 数字商会支持Crypto.com对SEC采取法律行动
- 日本金融厅将非托管钱包服务提供商排除在加密资产交易行业之外
- Sophon拟于12月之前上线主网和部署SOPH代币,但初始不会TGE
- Gunzilla Games 在 PlayStation 5 和 Epic Game Store 上推出了 大逃杀游戏Off the Grid 的抢先体验版
- 前俄罗斯调查员因收 7300 万美元比特币贿赂被判 16 年,为该国史上最大贿赂案
- Coinbase上比特币溢价自10月初以来保持为负值
- 联合国:东南亚使用加密货币进行欺诈和洗钱的情况日益增加
- 比特币协议 Babylon 质押存款增至 15 亿美元
- 前比特币开发者 Peter Todd 在 HBO 纪录片播出前否认自己是中本聪
- Canary Capital 向美SEC提交现货 XRP ETF 申请
- 美股收盘:三大股指齐涨,英伟达涨4%
- 美联储Bostic:经济过于强劲的风险可能会妨碍政策的重新调整
- 数据:Pump.fun 连续 12 天单日收入超过 90 万美元
- Arkham:某最初由以太坊基金会资助的地址向 Bitstamp 转移 300 万美元的ETH
- dApp基础设施开发商 Semantic Layer 完成 300 万美元种子轮融资,Figment Capital 领投
- 欧盟受监管代币化公司 Midas 向散户交易者开放 mTBILL 和 mBASIS 代币
- 桥水基金创始人Ray Dalio不认为美联储会“大幅”降息
- Kraken调查:83% 的加密投资者使用美元成本平均法来购买加密货币
- Lookonchain:美国政府或可自由出售从丝绸之路“Individual X”没收的 69,370 BTC,价值43.3 亿美元
- Arkham:富达在过去 24 小时内买入价值超过 1 亿美元的比特币
- 24分钟前Antpool向某地址转移1649枚BTC
- Coinbase将在 Solana 网络上增加对 io.net (IO)的支持
- 比特币 Layer 2 Bitlayer 以 3 亿美元估值完成 900万美元A轮融资,Polychain Capital领投
- Morning Consult最新民意调查:哈里斯的支持率为51%,领先特朗普6个百分点
- 过去 40 分钟内某巨鲸向币安存入11456枚ETH,价值2780万美元
- BTC跌破62000美元
- 币安要求加密经纪公司 FalconX 归还其自 2021 年以来持有的 135 万枚SOL
- 随着网络哈希率上升,比特币矿企 9 月份收入连续第三个月下降
- 渣打银行高管:如果特朗普当选总统,到 2025 年底Solana可能上涨五倍
- Jupiter:Jupiter Mobile零平台费 ,可接受 Apple Pay、信用卡等
- Layer-2 网络 Scroll 公布 SCR 空投计划,总代币供应量的 15% 将用于空投
- AI Web3 初创公司Bluwhale拟通过节点销售筹集高达 1000 万美元
- Arkham:除BTC和ETH之外,贝莱德还持有SPX、UBXS和MOG等
- 日本最大电信公司 NTT 加入 Injective 并成为其验证者
- Samsung Pay 通过与 Alchemy Pay 集成支持加密货币支付
- 马斯克:如果特朗普败选,我就完蛋了,你猜我会被判几年?
- 分析:中国投资者转向股票或对 Tether 产生影响
- RWA 发行商 Midas 获得欧洲监管机构批准,拟扩大代币化产品范围
- 某加密 KOL“意外”将 166 万枚 SUNDOG 发送到合约地址
- 美股高开,道指涨 120 点
- Crypto.com:已对美 SEC 提起诉讼
- 加密交易所 Crypto.com 收到美 SEC 的韦尔斯通知
- AI 初创公司 Kiva AI 完成 700 万美元种子轮融资,Coinfund 领投
- AI Web3 初创公司 Bluwhale 拟通过节点销售筹集 1000 万美元资金
- Gate.io 宣布对 TON 区块链投资 1000 万美元,助推 Telegram 项目发展
- Circle:原生 USDC 已正式上线 Sui 网络
- 比特币质押协议 Solv Protocol 已推出质押抽象层 SAL
- Nostra 发起提案,拟将 NSTR 流动性从以太坊迁移至 Starknet
- 做空机构兴登堡表示已做空 Roblox
- Solana Q3 桥接净流入 10 亿美元,市占率达 35%
比推专栏
更多 >>观点
项目
比推热门文章
- 数字商会支持Crypto.com对SEC采取法律行动
- 从小白到 Alpha 高手不能错过的这 50 种 DYOR 工具
- 日本金融厅将非托管钱包服务提供商排除在加密资产交易行业之外
- Sophon拟于12月之前上线主网和部署SOPH代币,但初始不会TGE
- 专访 HashKey 代币化负责人:为什么做这块业务?Tokenisation 与 STO 的难点与潜力
- Gunzilla Games 在 PlayStation 5 和 Epic Game Store 上推出了 大逃杀游戏Off the Grid 的抢先体验版
- 前俄罗斯调查员因收 7300 万美元比特币贿赂被判 16 年,为该国史上最大贿赂案
- Coinbase上比特币溢价自10月初以来保持为负值
- 联合国:东南亚使用加密货币进行欺诈和洗钱的情况日益增加
- 【比推每日新闻精选】渣打银行高管:如果特朗普当选总统,到 2025 年底Solana可能上涨五倍;Canary Capital 向美SEC提交现货 XRP ETF 申请;加密交易所 Crypto.com 收到美 SEC 的韦尔斯通知