现代DEX - Curve StableSwapNG 是如何构建的

  • mixbytes
  • 发布于 2024-11-06 12:40
  • 阅读 34

Curve的StableSwapNG是一个去中心化交易所(DEX)项目,旨在高效交易稳定资产。文章详细介绍了其核心组件、池工厂、流动性池、交易过程、动态费用、流动性管理、流动性测量以及实现细节,强调了其在DeFi中的应用和灵活性。通过使用多维不变曲线,StableSwapNG优化了资产交易,吸引了来自不同协议的流动性提供者。

引言

Curve 的 StableSwapNG 是一个去中心化交易所(DEX)项目,旨在有效交易稳定资产或与相同价值挂钩的资产(例如 ETH 和 stETH)。它鼓励来自不同协议的流动性提供者参与,提高市场流动性,并确保平台用户能够轻松交易目标资产。
StableSwapNG 允许为不同代币部署兑换池,每个池最多可容纳八种不同的代币。该实现采用多维不变曲面,与 Uniswap 中传统的二维不变曲线相比,允许三维、四维甚至更高维度的曲线。
StableSwap 不变性及其相应的自动化市场制造商(AMM)设计在 StableSwap 的 白皮书 和其他文献中有详细描述。为了避免深入涉及兑换背后的复杂数学,此讨论将专注于 StableSwapNG 的实现细节。让我们开始吧。

核心 (Core)

我们将探索 stableswap-ng 仓库,从核心组件开始:池工厂 CurveStableSwapFactoryNG.vy。该合约部署两种主要类型的兑换池:PlainPool 和 MetaPool,以及一个特殊的 Gauge 合约,用于分配额外的奖励。

我们将审查 deploy_plain_pool()deploy_metapool() 函数,这两个函数用于创建这两种主要池类型。“plain pool” 旨在用于“标准”代币兑换,而 “meta pool” 可以兑换其他池的 LP 代币。在一个 meta pool 中,有一个“基础”池,其中 LP 代币是列出的第一个代币。

常见的池部署参数包括代币列表(最多八种)、小数位、类型、利率预言机和调用这些预言机的方法签名。其他关键参数是兑换费用和不变曲线的放大因子。代币类型可以 包括

  • 常规 ERC20 代币
  • 具有利率预言机的代币(如 wstETH)
  • 重基代币(如 stETH)
  • ERC4626 股权代币

这些类型的代币在某些情况下可能需要特殊处理(例如,这里)。正如在 Balancer V3 的 文章 中所见,管理 DEX 中的非标准代币总是一项复杂的工作。

值得强调的关键参数是 _implementation_idx。Curve StableSwapNG 允许部署各种类型的池,这些池必须能够升级其逻辑或应用修复。Factory 合约负责 设置 用于部署新逻辑的 PlainPool、MetaPool、Gauge、Math 和 Views 实现合约的新地址。因此,通过其索引从 self.pool_implementations 和 self.metapool_implementations 中选择特定实现。

工厂的部署过程涉及在 self.pool_data 中“注册”新池,配置其详细信息,并将代币地址添加到 self.markets 映射中。该映射允许识别给定代币对的池。为了搜索促进 coin1 和 coin2 之间的兑换的池,键的定义简单地为 uint256(coin1) XOR uint256(coin2)。使用 XOR 确保无论代币地址的顺序如何,都会生成相同的键。示例可以在 这里 找到。

工厂中的另一个池部署函数是 add_base_pool(),该函数仅可由工厂的管理员访问。该池作为 MetaPool 的基础池。

一个有趣的参数是 _ma_exp_time,该参数定义了池移动平均价格预言机的时间窗口。Curve 允许配置此参数,因为不同的代币集需要来自价格预言机的不同“反应时间”。某些代币,尤其是更不稳定的资产,响应需要更快,从而增加预言机对价格操纵的脆弱性。相反,其他代币可以容纳更大的滑动时间窗口,使价格预言机更能抵抗操纵。我们稍后将再次探讨此参数。

新池的部署是通过 Vyper 的内置函数 create_from_blueprint() 执行的。它接收合约实现地址并使用指定的构造函数参数进行部署。在 StableSwapNG 中,此函数不使用盐参数,这意味着所有部署都是使用 CREATE 操作码执行的(与 Uniswap 不同,Uniswap 中所有池地址都是从代币地址确定派生的)。

在 Curve 的 StableSwapNG 中,没有可配置的兑换/流动性钩子(与 Balancer V3 或 Uniswap v4 不同)。逻辑是固定的,只有池和 Gauge 的实现可以通过治理进行修改。

Plain Pool

我们将从 StableSwapNG 池开始——没有包装代币的“plain”池。它的大多数功能与第二种类型的池,即“meta pools”(CurveStableSwapMetaNG)相似。这部分将在稍后讨论。

部署过程的第一步是 初始化 代币列表、放大因子和预言机时间窗口。

在对所有代币的 循环 中,一个有趣的方面是将预言机地址和方法选择器 打包 成一个 256 位的值。通过将 20 字节地址和 4 字节选择器轻松打包在一起,节省了 gas。

接下来,我们处理 ERC4626 代币,这些代币具有基础资产。与这些代币的操作需要理解规模因子(来自小数位),以便与 ERC4626 以及基础代币正确操作。这些值存储在 call_amount 和 scale_factor 数组 中。它们在 这里 为 ERC4626 代币初始化,随后用于获取存储的费率。

池内的代币转移由两个函数管理:

  • _transfer_in():此函数修改池的 stored_balances[coin_idx],并包含两个逻辑分支:一个遵循传统的 transferFrom() 逻辑,而 另一个 假设“乐观”转移,池假定在调用 transfer_in() 之前,新代币已经转移。
  • transfer_out():此函数直接执行将代币转移到用户的操作——通过 transfer() 发送代币并更新 self.stored_balances。

兑换

交易过程始于以下任一函数:exchange()exchange_received()。两者在使用的 _transfer_in 方法上有所不同(要么是 transferFrom(),要么是上述的“乐观”方法)。值得注意的是,第二个“乐观”变种 不能 与重基代币一起使用,因为池的余额在转账和兑换过程中可能会发生变化。两个函数接受被兑换的代币的索引和一个 _min_dy 参数,该参数设置了获得代币的最低数量,从而允许设置滑点限制。

兑换过程中的下一步在 _exchange() 函数中,通过 获取 各种代币类型的实际余额来进行。通过调用 _balances() 函数,处理重基代币的方法是查询实际的当前 balanceOf(self),而不是依赖于 self.stored_balances[i]。然后 _xp_mem() 通过应用相应的费率来计算实际储备值。

兑换的核心是 计算 _dy,该值表示输出代币的结果数量。最后,使用 _transfer_out() 发送代币。

所有兑换相关的数学操作都发生在 __exchange() 函数中,其中主要池变量 D 表示(标准化余额下)总的代币数。放大参数 amp 与 D 和实际储备一起用于计算池中某个代币 y 的目标储备量(如果添加了 x 代币)。这有助于确定 dy,即要发送给用户的 y 代币数量。为了防止舍入问题,目标 dy 值会 减少 一个单位。这种舍入调整可以保护池免受因用户有利舍入而产生的潜在攻击。

计算出的 dy 值随后用于计算一个 动态 费用。该费用从 dy 中减去,并加到 self.admin_balances[j] 中。通过更新协议储备并维护预言机完成该兑换。

费用

让我们回到费用上。在 Curve 的 StableSwapNG 中,费用是动态的,并且从输出代币中扣除,从而增强池的余额(提高 D,增加 LP 代币的价格)。在不平衡的设置中,当添加或移除流动性时也会收取费用,否则用户可能会简单地使用一种代币添加或移除流动性,而不是执行兑换。正确、按比例添加或移除流动性则不收取费用。

“理想”代币数量的计算基于这样一个理念:如果 D 增加了 x%,每个代币余额也应增加相同的 x%。“理想”目标余额的确定使用 D 的这种增加(示例 见此处),并使用差异来 计算 目标费用。有关费用计算的更多细节可以在 此文档 中找到。

动态费用系数取决于两个代币 ij 之间当前的不平衡(公式和行为详述 见此处)。此外,还有一个特殊的 self.offpeg_fee_multiplier 值,该值在池失去其挂钩时由池的治理进行 设置

添加/移除流动性

add_liquidity() 函数接收所有给定数量的代币,并首先检查 D 是否没有 减少。下一部分至关重要,因为 StableSwapNG 对增加的流动性收取动态费用(这是 DEX 通常不常见的)。上述费用取决于与“理想”余额分布的 差异。添加的资产数量的不平衡越大,费用就越高。添加流动性还包括通过 _min_mint_amount 提供的滑点 保护,该参数指定必须收到的最小铸造 LP 代币数量。

流动性移除通过三种不同的函数执行。第一种是 remove_liquidity_one_coin(),用户简单地通过销毁相应数量的 LP 代币来提取指定数量的单一代币。该函数的滑点保护通过 _min_received 参数提供,设定接收的代币的最小数量。目标输出代币数量的计算在 _calc_withdraw_one_coin() 函数中执行,我们还可以遇到由于“不平衡”操作所导致的动态费用 计算。此外,在 _calc_withdraw_one_coin() 函数内部,还有另一个地方 可以通过舍入 将目标提取量减少,以处理潜在的舍入错误。

下一个用于移除流动性的函数是 remove_liquidity_imbalance()。它接受目标代币数量,使用 _max_burn_amount 参数进行滑点保护(限制要燃烧的 LP 代币数量),允许用户移除不同数量的每种代币。在该函数中,使用相同的“不平衡”费用机制。

移除流动性的第三种选择是 remove_liquidity() 函数。它接收要燃烧的池 LP 代币数量作为参数。通过 _min_amounts[] 数组实现滑点保护,设定必须收到的代币的最小数量。该变体简单地根据燃烧的 LP 代币发送 相应 数量的代币给接收者,并且由于在这种按比例流动性移除后不会造成池的失衡,因此不收取附加费用。这个函数的另一个关键区别是,它不会影响代币之间的兑换价格,从而消除更新预言机的必要性。只有 D 和 D_ma_time 的变化部分会被 更新。相反,前两种函数需要更新价格预言机。因此在 这里这里,使用了 upkeep_oracles() 函数(稍后将讨论)。

Meta Pools

在回顾了整数池和预言机后,是时候检查 meta pools 了。Meta pools 的功能与普通池相同,但代币列表中的第一个币是来自另一个池(基础池)的 LP 代币。此 LP 代币使 meta pool 能够将其与基础池中的代币进行兑换,增加了与基础代币相关的额外逻辑。

部署 meta pool 类似于部署普通池,但还需要 基础池地址和代币。此外,必须 批准 在 meta pool 与基础池之间进行代币操作。

meta pool 与基础池之间的这种交互在 transfer_in() 函数中引入了 额外逻辑。如果兑换完全在基础池中进行(即,第一个和第二个代币都是来自基础池的非 LP 代币 见这里),则完全 跳过 修改 meta pool 储备的过程。然而,如果交易中涉及 LP 代币,则该函数必须在 这里 缴纳来自基础池的 LP 代币并将其添加到 meta pool 储备。

_exchange_underlying() 函数是 meta pools 和普通池之间的关键差异。它涉及来自基础池的代币,要求 跟踪 在母“meta”池和子“基础”池中代币的索引。当不需要 LP 代币时(即,不进行 meta 兑换),可直接在基础池中进行兑换 可见于这里

在涉及基础池 LP 代币的 meta 兑换中,每当基础池代币的余额变化时,我们需要铸造/销毁相应数量的基础池 LP 代币。我们还必须添加/删除基础代币,并在存储队列中更新 meta pool 储备以反映这些变化( 见此部分)。

为 meta pool 添加流动性实际上意味着向基础池添加流动性(如 _meta_add_liquidity() 函数所示)。移除流动性类似于普通池的操作。

预言机

DEX 作为价格预言机在 DeFi 的所有领域中至关重要。然而,直接查询当前池的储备及其比例可能是危险的,因为“即时”价格可能会被储备的增加/移除操纵,尤其是在流动性不足的池中。为了降低这种风险,所有现代 DEX 提供的兑换价格是从之前的交易/区块聚合值。更长的时间窗口对于这些时间加权平均价格(TWAP)使得预言机价格更安全,但也可能减缓外部项目价格更新的速度,当 TWAP 落后于快速市场(如中心化交易所 CEX)时,会产生套利机会。

继续前面的部分,让我们进入 upkeep_oracles() 函数。

首先,所有代币的现货价格 通过 使用 _get_p() 函数确定。该函数返回一个动态的价格数组,表示在当前余额和放大影响下的每个代币在池中的调整相对价格。计算涉及对代币的两个循环:第一个 循环 计算 Dr,即调整后的 D,考虑了每个代币的“权重”。在 第二个循环,该值用于将每个代币进行“加权”,与第一个代币(用 xp0_A 值表示)进行比较。

这些结果现货价格被 保存 到 self.last_prices_packed 数组中,连同移动平均值,在我们的例子中是 EMA(指数移动平均)。这一类型的移动平均广泛应用于交易(解释 见此处),以更快地响应价格变化,为最近的数据点赋予更大的权重。StableSwapNG 池的时间窗口参数是 self.ma_exp_time(在池部署阶段设置)。该参数传递给 _calc_moving_average() 函数,该函数使用指定时间窗口为每个代币计算 EMA 价格。

从池中可以使用 个简单函数获取最后现货价格和 EMA 价格:last_price() 和 ema_price()。

流动性 Gauge

流动性 Gauge 是 Curve 的 StableSwapNG 协议中的一个关键组件,与激励、治理和高效市场操作息息相关。了解有关流动性 Gauges 的信息

通过工厂部署的 Gauge 连接 到特定池的 LP 代币。用户可以将 LP 代币存入 Gauge 以根据其提供的 LP 代币数量赚取 CRV 或其他代币的奖励。

非 CRV 奖励通过管理函数进行管理,例如 deposit_reward_token() 用于存入奖励, add_reward() 用于注册代币,以及 set_reward_distributor() 指定分配者地址。

LP 代币通过 deposit()withdraw() 函数添加到 Gauge 或从 Gauge 中移除,这取决于用户的 Gauge 余额。一个关键特性是这些函数内部的检查点机制。

Gauge 中的奖励累积遵循以下公式:

这里,r'(t) 代表通货膨胀率(来源于基准率和 Gauge 和 Gauge 类型的权重),b u (t) 表示用户的 LP 代币余额,而 S(t) 是所有用户提供的总 LP 代币。

改变余额或利率的操作会触发值的更新,促使 StableSwapNG 跟踪这些变化并为每个用户重新计算此积分。奖励检查点发生在 _checkpoint_rewards() 函数中,通过影响 LP 代币余额的函数调用。

积分计算分为两部分。所有用户的共同部分,用户特定余额被排除,集中在利率 r'(t) 和总 LP 代币供应 S(t),并存储在奖励代币配置中( 见此处)。该积分在 此部分 的 _checkpoint_rewards() 中更新。

“每用户”的积分检查点(与 b u (t) 相关)存储在 reward_integral_for[] 映射中,并在 _checkpoint_rewards() 的 下一分支 中更新。 后续操作与奖励旨在 保留 可领取的金额。

CRV 代币奖励通过 _checkpoint() 函数使用类似的跟踪逻辑来更新速率。在计算累积供应量后,积分的“周期”部分被 存储 并用于 更新 “每用户”奖励积分。

管理函数 set_killed() 可以在出现问题或不当行为时停止 Gauge 操作。

实现细节

StableSwapNG 协议的一个重要方面是其在计算中处理代币余额的方式。StableSwap 的 D 涉及代币余额的求和和乘法,而 DEX 必须管理精度不同的各种代币。为解决此问题,StableSwapNG 中用于计算的所有代币余额都规范化为 18 位精度。这些规范化的余额在代码中被称为 xp(在 这里 可以找到 _xp[] 数组使用的示例)。

如果所有代币都是标准 ERC20 稳定币,则规范化只涉及调整金额的小数点。在这种情况下,计算 D 只需将所有余额求和,然后规范化为 18 位精度。然而,在 StableSwapNG 中,规范化还适应了带有速率预言机的可重基代币,这使得 xp[] 余额变得复杂。xp[] 余额表示“以 D 的单位”计的代币数量。这些余额在 这里 计算,然后用于在 get_D() 函数中计算 D。

这个函数突出了另一个有趣的实现细节:D 和用于兑换的代币金额的迭代计算。在该协议中使用的 StableSwap 方程是:

在 Solidity 中直接求解包含多个代币余额的方程是不可行的,因为方程右侧的乘法/除法可能导致溢出或精度损失。为了在 DeFi 协议中实现这些计算,使用了迭代算法。尽管基础数学较为复杂,但在这些实现 说明 中做了更好的描述。在使用迭代算法进行这些计算时,需要考虑特定的场景(评论 在这里),但在池中“正常”的代币分布情况下,这些场景不太可能发生。如果确实发生,流动性可以从池中撤回。

StableSwapNG 的另一个重要数学方面是天然指数函数的 实现,灵感来自 这个 工作。如果你希望在协议中使用天然指数或对数函数,这可能特别有用。

外部协议利用 DEX 可能会面临挑战,需无“查询”功能来模拟兑换、计算费用、估计目标金额和设置适当的滑点限制。在 StableSwapNG 中,通过 CurveStableSwapNGViews.vy 接口来管理这些功能。这些函数对开发者探索特别有用,因为它们仅包含状态读取逻辑,没有任何额外的存储逻辑。

最重要的视图函数是 get_dx()get_dy(),允许你在计划兑换后计算输入/输出代币的数量。还有类似的函数 get_dx_underlying()get_dy_underlying(),适用于元池。

结论

Curve 的 StableSwapNG 是一个动态 DEX,采用 StableSwap 不变式形成多达八种不同代币的池。其复杂的数学模型促进了稳定币或与相同基础资产挂钩代币之间的高效兑换,最大限度地减少滑点。整个协议中的动态费用解决了流动性不平衡问题,保持了池的“健康”。

StableSwapNG 在容纳各种代币,包括可重基的收益凭证方面的灵活性,扩大了池的效用并吸引了多样的资产。Curve 通过激励 CRV 和外部代币的流动性提供,加大了提供者的参与。

为了进一步去中心化,StableSwapNG 鼓励用户参与治理投票,使社区能够对协议决策提供反馈,从而改善治理结构。

StableSwapNG 是一项对 DeFi 开发者和审计员感兴趣的创新 DEX 解决方案和流动性管理策略的有希望的探索。

这篇文章到此结束,我们下次再见!

  • 谁是 MixBytes?

MixBytes 是一支专家级区块链审计师和安全研究团队,专注于为 EVM 兼容和 Substrate 基础项目提供全面的智能合约审计和技术咨询服务。关注我们的 X,以获取最新的行业趋势和见解。

  • 原文链接: mixbytes.io/blog/modern-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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