本文介绍了Uniswap V4的关键技术细节及其架构变化,与之前版本相比,Uniswap V4在池的创建、管理和交易成本优化方面进行了重大的改进,同时引入了Hook系统和简化了对原生ETH的支持。作者强调了在DeFi开发过程中需要关注的安全机制和代码实现细节,适合开发者和审核者深入学习。
本文标志着一个关于现代 DEX 实现系列的开始。这些文章的目标(与之前的“借贷”系列相同)是为 DeFi 开发者和审计人员提供对现代 DeFi 实现的全面回顾,包括所使用的算法、关键数据结构和函数。与其他资源不同,我们将不深入探讨协议的经济和金融方面,这些内容可以通过项目文档进行研究。相反,我们将专注于重要的代码段并进行详细讨论。
Uniswap V4 是著名的 Uniswap 协议的下一个版本。但是,与其前辈相比,Uniswap V4 拥有大量显著的变化。首先,Uniswap V4 允许创建具有多种逻辑类型的池。虽然 V2 池的流动性分布在整个价格范围,而 V3 池则具有集中流动性和价格区间,但 Uniswap V4 允许使用其 hooks 系统添加新的池类型。此外,还特别关注了优化交易成本,池管理的组织方式为单例而非工厂。值得注意的是,Uniswap V4 还恢复了交换本机 ETH 的能力,该功能之前只有通过将 ETH 包装为 ERC20 WETH 代币才能实现。
让我们深入探讨一下“它是如何实现的”。
[注意] 目前,Uniswap V4 尚未部署到主网,仅在测试环境中运行。请注意,讨论的所有主题与项目当前状态相关,未来可能会发生变化。
Uniswap V4 的主要架构变化是从工厂模式转移到单例模式。在所有先前的版本中,新池是通过池工厂进行部署的;现在新池可以使用单个合约进行创建和管理。我们从 PoolManager.sol 开始。
PoolManager 的主要操作结构是 PoolKey 结构体,包含以下元素:两个可交换代币的地址(currency0 和 currency1)、费用的数量和类型(静态或动态)、池的 hooks 地址,以及 tick 间距参数(请参见关于集中流动性的 文章)。池的 ID 是通过对 PoolKey 结构体中所有内容进行 哈希 生成的。
池的部署是通过 initialize() 函数完成的,该函数接受 PoolKey 结构体(包含代币地址)、初始 sqrtPrice 和池 hooks 的初始参数(在 PoolKey 结构体中设置)。
在进行第一次检查后,beforeInitialize hook 被 调用,允许 hooks 设置初始状态。然后池被 初始化 到其原始状态,返回与初始价格对应的 tick 编号。之后调用 afterInitialize hook,该函数结束。
上述的 PoolKey 是所有 Uniswap V4 池操作中的唯一标识符(如 swap() 或 modifyLiquidity())。通过将池地址计算外包给外部方,Uniswap 减少了 gas 成本并提高了效率。
现在,是时候看看 Uniswap V4 的主要安全机制——“解锁”模式和代币增量。Uniswap V4 中的每个操作都需要 解锁 PoolManager。加锁是通过 使用 临时存储实现的,这保持锁定状态持续到事务结束。这是必要的,因为在重入场景中无法使用内存,而使用存储进行重入锁定费用太高。
任何与流动性有关的操作都包括 onlyWhenUnlocked() 修饰符。“锁定/解锁”机制像重入锁一样,拒绝外部调用重新进入函数。然而,此外,在 unlock() 函数结束时,有一个重要的 检查,以确保非零代币增量。在 swap() 函数的结束处( 此处)、流动性管理( 此处)、以及 donate() 函数( 此处),我们看到了 _accountPoolBalanceDelta() 函数,该函数 计算 操作后的“非零”代币余额变化。这一计数器在 unlock() 完成工作时必须等于零,这意味着在交换和提供流动性内部的任何操作都必须以零借记/贷记余额结束。
这一模式与使用临时存储结合是非常强大的,DeFi 开发人员应当研究,以保持主协议一致,同时允许外部协议执行多种任意操作。在 Uniswap V4 中,池可以通过使用 hooks 进行灵活的自定义。让我们看看 Uniswap V4 的池。
池的初始化包括在 initialize() 函数中设置 Pool.State 结构,这与 Uniswap V3 中使用的池类似(在我们之前的 文章 中进行了描述)。如果在创建的池中未设置 hooks,那么该池将像使用集中流动性的 Uniswap V3 池一样工作。你可以查看 Pool.sol 的 swap() 函数,并提到它使用与 Uniswap V3 池相同的代码、价格 tick、sqrt 价格和费用管理。但在 Uniswap V4 的情况下,池的创建成本大大降低,因为不需要部署额外的合约。流动性管理(在没有自定义 hooks 的情况下)也与 Uniswap V3 类似。
另一个需要注意的重要特性是 donate() 函数,它允许直接向池费用的 feeGrowthGlobal* 累加器中添加代币。它可以用于直接激励池。然而,请注意,donate() 可以被超越。此外,该代码的这一部分清楚地显示了代币增量的处理。在这里,我们简单地 "添加" 负数金额到代币增量中,协议现在记录它有一个“负余额”。
累积的借记/贷记代币余额需要从协议发送/接收,这些操作由两个函数处理:
这种代币结算设计使得外围合约在 PoolManager 解锁期间执行多种操作成为可能:提供流动性、进行交换(附加 hooks)、捐赠费用,以及仅将最终代币数量发送到协议和结算。
hooks 系统是下一个步骤。如上所述,hooks 是修改池行为的关键机制。没有 hooks,我们就会得到一个众所周知的具有集中流动性的 Uniswap V3 池。但是,如果开发者愿意,他们可以为池设置 hooks,如 这里 所述。例如,两个 hooks:beforeSwap() 和 afterSwap(),返回代币增量,可以修改池的行为;例如,实现常规的 Uniswap V2 池。相同的方法也可以用于提供流动性和激励池的目的。
Uniswap V4 代码引入了许多新的功能和与先前版本的区别。让我们讨论一些。
一个显著的特征是支持本机 ETH 的回归。在早期版本中,将 ETH 包装和解包为 WETH 是一个繁琐的过程,并需要额外的 gas。由于 Uniswap 的主要交易池通常是与 ETH 相关的对(例如,ETH/USDC),这个变化特别重要。Uniswap V4 现在支持直接与本机 ETH 进行操作,而不需要包装或解包,增加了直接处理 ETH 的额外逻辑分支(如 这里)。
下一个有趣的部分是获取协议费用。在某些情况下,无效的协议费用控制器(由协议所有者设置)可能会打破池的初始化。为了避免这种情况,将分析对协议费用管理器的调用,如果出现回退(即调用未返回成功),则 返回 0 费用。此外,协议费用管理器可能会消耗过多的 gas,打破池的初始化。通过 这里 的额外 gas 限制,可以缓解这种情况。
另一个重要的方面是协议数学。Uniswap V4 使用“全规模” 256 位值,而早期版本的 Uniswap 使用 128 位值。因此,乘法和除法操作需要处理可能在中间结果中的溢出。例如,在 d=(a*b)/c 的情况下,可能发生在 (a*b) 部分的溢出。为了解决这些情况,Uniswap V4 实现了一个支持 512 位精度的 mulDiv() 函数。虽然这需要额外的计算,但 Uniswap V4 得以正确处理任何代币,具有任何 256 位的余额和精度,并避免在协议中进行额外的转换。
Uniswap V4 引入了较早版本的显著变化和优化。新架构允许将不同逻辑的交易池结合在一起,使这些池的创建及其交互变得更加便宜。
值得注意的是,工厂模式消失了,与池的操作现在通过 PoolManager 进行,使用临时存储实现“锁定/解锁”模式,有效地避免了与重入和预言机价格操纵相关的攻击。
总的来说,Uniswap V4 的设计选择和代码结构反映了其生态系统的系统性演变,专注于 gas 效率和可用性。
请继续关注我们的新文章!
MixBytes 是一个由区块链审计师和安全研究专家组成的团队,专注于为 EVM 兼容和 Substrate 基础项目提供全面的智能合约审计和技术咨询服务。请加入我们的 X,随时了解最新的行业趋势和见解。
- 原文链接: mixbytes.io/blog/modern-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!