现代DEX - Balancer V3 是如何构建的

  • mixbytes
  • 发布于 2024-10-25 19:12
  • 阅读 263

本文深入分析了Balancer V3协议的设计与实现,介绍了其三层架构、核心功能、流动性管理、交易逻辑及安全机制,包括动态费用计算和防重入攻击设计。文章通过丰富的代码示例与技术细节,为了解去中心化交易所(DEX)提供了全面的视角,适合希望深入了解现代DEX设计的开发者和审计人员。

简介

要完全理解本文及后续有关DEX的文章,你应该熟悉自动化做市商(Automated Market Maker,AMM)概念,包括Uniswap V2/V3的技术设计(在许多地方都有描述),包括Router<->Pool构造和回调机制。我们之前关于Uniswap V3和Uniswap V4的文章可以在这里这里找到。

今天深入分析的对象是Balancer V3,最新的去中心化交易协议之一。

高层设计

核心交易流程在Balancer V3文档中描述这里,可以分为三个主要组成部分:Router、Vault和Pool。

(来自Balancer V3文档的图片)

在Balancer V3中,兑换逻辑分为两个独立的部分:Vault和Pool,其中Vault负责账户管理和持有代币余额,而Pool部分负责不变量和兑换数量的计算。这种设计使得Balancer能够拥有多种类型的池(在这里有描述),并处理不同类型的逻辑,利用其数学模型,而“结算”层仍然保持一致。让我们继续实施。

核心

Vault

Vault中的核心操作是_supplyCredit()_takeDebt()函数。它们跟踪每种代币的协议债务和信用,并参与几乎所有操作。Balancer允许外部路由执行一系列操作,其中每个操作可以承担债务、提供信用,并在后续操作中使用这些信用。每个单独的操作可以收取多余的代币,将额外的代币提供给池,但在所有操作结束后,债务/供应余额的变化必须归零。这就是为什么协议中存在这样的会计。

为了实现这一点,_supplyCredit()和_takeDebt()函数使用了一个非常重要的函数:_accountDelta()。该函数计算给定代币余额的累积变化(正或负),并递增/递减计数器_nonZeroDeltaCount()。该计数器随后用于transient()修饰符,如果至少有一个代币的变化不为零,则会撤回整个操作包。

[注意] 这部分逻辑需要更详细的解释。 DEX用户不仅希望进行简单的代币兑换,还希望执行更复杂的操作,例如在同一交易中兑换代币或向多个池提供流动性。当单独执行这些操作包时,它们可能非常昂贵,而将它们组合成一个大的原子操作可以节省大量的Gas(为每个代币执行单个“最终”转移,而不是多个“中间”转移)。这使Vault能够简单地跟踪每个池的代币变化,并一次性最终化多个操作,以检查协议是否没有非零的信用或债务。

在这些场景中增加的复杂性是需要通过回调和Hook在不同合约中执行操作,这使得不能使用公共内存变量(由于执行框架的切换),而使用存储来跟踪中间余额变化则成本过高。然而,在Ethereum的Dencun升级中引入的EIP-1153引入了一种新的内存类型——瞬态存储。新的TLOAD/TSTORE操作码的成本远低于常规的SLOAD/SSTORE,只在当前交易期间存储数据。这种类型的内存在DEX中尤其有用,因为所有DEX都需要重入保护、处理代币许可并根据复杂的构造参数创建确定性池地址。所有这些情况都非常适合TLOAD/TSTORE。

这些操作在Balancer的瞬态修饰符中用于unlock()来解锁Vault。unlock()函数“包装”了路由中的任何操作(可以在Router.sol中找到,BatchRouter.sol和其他路由),确保无论路由中发生什么,最终的“所有代币的变化都为零”检查都会始终执行。这个函数的操作类似于瞬态重入锁,但它使用瞬态存储来保存每个代币的deltaIsZero标志。

使用unlock()来解锁Vault并将控制权返回给路由的示例可以在swapExactIn()中找到,位于BatchRouter.sol中。“解锁”Vault的另一个重要用途是,外部协议拒绝查询“解锁”Vault的状态,使其无法在重入场景中操纵流动性和价格。如我们所记得的,基于回调的代币转移方案在协议内引入了“自然”的重入矢量。此外,Balancer的自定义池可以使用外部Hook、费率提供商和池实现,这些都可能重入Vault。在多个DeFi攻击中,这种行为被利用,涉及只读重入和预言机价格操纵。

Balancer V3中的下一个重要核心函数是settle()sendTo()函数。它们处理代币在协议内外的转移,计算余额差异,更新协议储备,并执行supplyCredit/takeDebt操作,修改当前操作包中的代币变化。

settle()函数包含一个有趣的参数:amountHint,用于防止“捐赠”攻击,当代币直接发送到Vault时,改变Vault余额以操纵信用金额。通过使用这个条件来缓解,该条件使用amountHint代币作为信用,除此之外不会有其他额外的代币。任何额外的代币将被简单地添加到Vault的余额中。

Vault本身是一个ERC20MultiToken,持有池中流动性提供者代币的余额/授权。所有代币函数都将池的地址作为第一个参数,因此Vault以ERC20代币的形式跟踪用户在每个池中的余额。此外,每个池也都是ERC20代币,如在BalancePoolToken.sol中所述,但该池代币的所有函数均代理回Vault的ERC20MultiToken。这使得池能够拥有完全合规的ERC20代币,而Vault则保持对多个池代币余额管理的完全控制。

Vault包含了大量代码,超过以太坊主网上允许的最大合约大小。因此,Balancer V3由三个主要合约组成:

  • Vault.sol,主要合约,具有核心功能,如兑换或提供流动性。该合约还充当代理,将“非拥有”调用路由到附加扩展:VaultExtension:

  • VaultExtension.sol - 包含额外的、无需权限的功能,使用频率较低(如注册新池、多代币接口、只读查询等)。此合约也是一个代理,将调用转发到下一个合约:VaultAdmin

  • VaultAdmin.sol - 包含有权限的管理功能(暂停合约和池、设置收取费用等)

值得注意的是,使用ensureVaultDelegateCall()函数,用于检查某个扩展函数是否从Vault调用(并且仅通过delegate调用)。

Balancer核心功能的另一个关键方面是其费用系统。在Balancer V3中,费用直接在兑换代币时收取(类似于Uniswap V3/V4)。在Balancer V3中,费用分配的逻辑与Vault分开,Vault只知道全局兑换和收益费用,并简单地收集它们,使兑换和收益操作更便宜。

用于LP提供者兑换费用的收集在这里中完成,在SwapKind.EXACT_IN的情况下增加tokenIn数量,在其他情况下减少tokenOut数量。收取的费用简单地保留在池余额中,转化为LP提供者的收益。

与收益型代币相关的费用部分则更为复杂,如前所述,这些代币的实施总是颇具挑战性。与常规代币相比,这些代币余额的增加是以某种费率为准的,常规代币的费率为“1”,而收益型代币则由rateProvider控制。尽管这些代币的余额持续增加,但每次需要池中实际可用储备的操作都需要根据费率更新这些余额。这是在\_computeAndChargeAggregateSwapFees()函数中进行的,在兑换和添加/移除流动性功能中呈现。

协议与池创建者之间的费用分配在ProtocolFeeController.sol合约中进行,并具有一个公共的、无需权限的名为collectAggregateFees()的函数,附带有回调Hook。该函数同样利用Vault的瞬态“解锁”,在费用操作期间避免对Vault的代币余额进行操纵。

给定池的主要费用分配函数(这里)循环遍历所有池的代币(这里),并设置相应的_protocolFeeAmounts[pool][token]和_poolCreatorFeeAmounts[pool][token]值。

路由

在Balancer中,路由的角色与Uniswap的周边合约相似。路由合约充当通往Balancer V3协议的主要入口,负责接受用户的代币和兑换路径,解锁Vault并执行兑换和流动性管理。

RouterCommon.sol(路由的公共代码部分)中执行代币转移的关键函数为_takeTokenIn()_sendTokenOut()函数,处理代币转移;以及permitBatchAndCall(),执行带有用户授权的一系列操作。

值得注意的是,可能有许多种路由实现的变体,开发者可以创建自己的自定义路由。但是,特定的池和Hook可以拒绝不必要的路由,从而在协议内确保一定的控制和安全性。

Balancer V3的一个主要思想是将池的逻辑与代币操作分离。当“一体化”方法在单一池设计中良好工作时,当你希望允许用户添加自己的池、路由,并通过Vault与它们组合操作时,这种方法是有限的。这种“分层”方法对于灵活性至关重要,同时也意味着V3中的池与V2相比要轻量得多。实际上,V3中的池简单地是对不变量曲线的实现。即使池代币实际上也只是对Vault的ERC20MultiToken的一个接口。

目前有两个主要合约实现了池的逻辑:StablePoolWeightedPool,可用于根据起始参数部署不同类型的池。任何Balancer池都必须实现IBasePool接口,其关键函数为onSwap()。StablePool中的示例在这里,此变体使用了从Curve项目实现的StableSwap不变量,位于StableMath.sol库中。

WeightedPool使用WeightedMath.sol库,允许创建具有“加权”代币分布的池,从而减少权重较大代币的无常损失(同时,由于权重较小代币的流动性较少,可能会导致更高的滑点)。

在Vault中利用StableMath曲线实现的添加/移除流动性逻辑。因此,如果池使用不同的数学模型,就必须实现具有自定义逻辑的IPoolLiquidity接口。这些自定义逻辑将在Vault中被用于这部分添加流动性和这一部分移除流动性。

Hook

Hook接口是Balancer协议的重要组成部分,在V3中具有众多潜在影响。可以分配给新注册池的Hook有多种类型...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
mixbytes
mixbytes
Empowering Web3 businesses to build hack-resistant projects.