《Rebasing 代币漫游指南 (stETH 和 aTokens)》

这篇文章深入探讨了Rebasing代币在DeFi智能合约集成中引入的复杂性,特别是余额波动和舍入误差等问题。作者详细分析了AaveV3和Lido stETH代币的四种特殊行为(“怪癖”),并强调了在智能合约中应以“份额”而非代币数量进行内部记账,以确保安全性和准确性。

A clean, graphic illustration of a cool penguin wearing a backpack, standing on the side of a digital blockchain highway with its thumb out to hitchhike.

伸缩性代币(Rebasing Tokens)搭车指南

Rebasing tokens 提供无缝的 UX,但给智能合约集成带来了额外的复杂性。由于余额根据全局指数而非简单的整数波动,舍入误差是不可避免的。我们将剖析导致 AaveV3 和 Lido 集成中逻辑回滚的四个基本“怪癖”,并解释为什么内部以“份额”(shares)进行会计处理是安全的黄金法则。

在 DeFi 收益型资产领域,开发者通常会遇到两类代币:一类是通过增加价格来增加价值的代币(如 RocketPool 的 rETH),另一类是通过增加钱包中代币数量来增加价值的代币。后者被称为 rebasing tokens(伸缩性代币)。

尽管 AaveV3 aTokensLido stETH 等 rebasing tokens 为终端用户提供了无缝体验,但它们给智能合约集成带来了一些开销。由于这些代币代表了对一个波动资产池的 1:1 所有权,它们的余额不是以简单的整数存储的。相反,它们是使用底层、每个用户持有的“份额”计数和全局十进制“指数”动态计算的。

这种动态计算引入了舍入误差,虽然这些误差在价值上可以忽略不计,但它们可能导致与它们交互的合约出现意外状态和逻辑回滚。因此,许多协议决定完全不支持 rebasing tokens。

希望与 Lido 集成而又不想面对 stETH 舍入算术相关代码复杂性的开发者,可以使用官方的 wstETH 代币,它是其非 rebasing 的对应物,与池份额而不是底层 ETH 进行 1:1 映射。另一种直接与份额交互的方式是使用自定义的 transferShares() / transferSharesFrom() 函数。这两种方法都在 Lido 的官方代币集成指南中有详细说明。

Aave 也存在类似的替代方案,由 stataTokens 代表:它们是符合 EIP-4626 的、围绕 aTokens 的非 rebasing 封装器。然而,它们在生态系统中的知名度和普及度较低,因此即使在链上集成中,aTokens 仍然占据主导地位。

虽然这些非 rebasing 变体通常是最佳选择,但有时 DeFi 协议就是_必须_支持 rebasing tokens:如果这是你的情况,或者你只是好奇想了解更多,那么请继续阅读。我们将剖析份额和余额之间的对应关系如何影响集成,并学习需要注意什么才能构建安全、生产就绪的保险库和策略。

四个怪癖

份额-余额转换中涉及的舍入打破了一些对于标准 ERC20 代币而言本应显而易见的属性,而开发者可能依赖这些属性来确保其逻辑的正确性。我们已经发现了四个这样的“怪癖”:

  1. 发送方和接收方的余额变化可能与转移金额不同。
  2. 两个余额的总和可能会改变,即发送方和接收方的余额变化可能不相等。
  3. 一次性转移全部余额总是可能的,但可能会留下少量“灰尘”(dust)。
  4. 将全部余额分成两部分并分别转移并非总是可行的。

我们已经调查了这些属性,并精确限定了由此产生的与非 rebasing 行为的“偏差”。下表总结了结果。请注意,Aave aTokens 和 Lido stETH 采用不同的舍入约定:

1. 定向舍入:谁来支付 Weis?

当用户调用 transfer(amount) 时,协议必须确定要移动多少份额 (σ)。由于金额和指数很少能完全整除,协议必须选择一个舍入方向。如果 a 是转移的金额,i 是(十进制)指数:

  • AaveV3 (向上舍入): 使用 σ= ⌈a/i⌉. Aave 优先考虑接收方,确保他们获得_至少_请求的金额。
  • Lido stETH (向下舍入): 使用 σ= ⌊a/i⌋. Lido 优先考虑发送方,确保他们绝不会花费超过请求的金额。

对于这两个协议,最大偏差最终为 ⌈i⌉ weis:更大的指数会放大舍入误差。例如,Lido stETH 的指数在撰写本文时约为 1.23,这意味着最大偏差为 2 weis:这是一种已知行为,并在相关的 GitHub 问题中正式描述。

  • 要点: 余额差异不一定等于转移金额;舍入方向很重要,因为它决定了转移的哪一方“受益”。

2. 1-Wei 波动

如果我们将余额视为具有完整精度的十进制数字,我们会发现发送方和接收方初始余额的小数部分可能不同。然后,当对两者应用相同的增量(计算为 σ⋅i,也是一个十进制数)时,其中一个余额可能会由于 σ⋅i 的小数部分而跨过整数阈值,而另一个则不会。因此,当最终向下舍入时,我们可能会看到两个(舍入后的整数)余额的变化量不相同:一个增量可能比另一个大一个 wei。

我们的概率分析(经模糊测试证实)表明,在两个协议中,余额总和大约 66% 的时间保持不变,而 33% 的时间会变化 ±1 wei。

  • 最佳实践: 不要依赖于发送方和接收方的余额差异相等。允许 1 wei 的容差。

3. “幽灵”份额 (Lido Dust)

Lido 的向下舍入会产生“灰尘”问题。当合约尝试转移其全部 stETH 余额 b 时,舍入逻辑 σ=⌊b/i⌋ 通常只会导致 s−1 份额被移动,其中 s 是用户的总份额计数。

  • 结果: 合约成功发送了代币余额,但自己却留下了 1 份额的“灰尘”。
  • 集成影响: 如果你的保险库逻辑要求零余额状态以关闭头寸或删除映射,这 1 wei 的“幽灵”份额将阻止状态清除。在这种情况下,建议直接使用 transferShares() 函数移动份额,以绕过舍入逻辑。

4. “分批转移”回滚 (Aave 风险)

这可能是开发者面临的最危险的陷阱。想象一个合约计算其总 aToken 余额,并尝试将 5% 发送给金库,95% 发送给用户。

由于 Aave 对每个单独转移所需的份额都进行向上舍入,这可能会累积正的舍入误差,并最终导致最后一次转移回滚:

在大约 66% 的情况下,这些向上舍入的份额总和将超过合约的总份额余额。第二次转移将因“余额不足”而回滚,即使你的余额追踪逻辑显示你有足够的余额。

Aave v3.5 升级:数学为何改变

值得注意的是,Aave 目前的行为是相对较新的。在 Aave v3.5 升级(2025 年 8 月)之前,aToken 转移使用“半向上”(银行家)舍入。

3.5 升级是通过 Aave 的治理系统执行的,有效地“在现有集成的开发者脚下”改变了数千个现有集成的底层数学。

  • 好处: 这次升级使 Aave 走向了明确的、定向舍入的 ERC-4626 理念。你现在可以确定接收方永远不会收到比他们被发送的更少的金额。
  • 风险: 通过选择一方,协议创建了一个永久的“舍入偏差”,该偏差会累积。正如我们在怪癖 #4 中看到的,总是将份额向上舍入意味着一系列小额转移的总成本通常会高于一次大额转移。

结论:掌握 rebasing token 集成

Rebasing tokens 是改善用户体验的强大工具,但它们打破了标准 ERC20 代币的“整数精确”思维模型。在构建集成时,请牢记这四个基本偏差:

  1. 增量不是金额: 预计 balanceOf(user) 的变化会比传递给 transfer 函数的 amount 略多 (Aave) 或略少 (Lido)。
  2. 会计不是零和: 所有用户余额的全局总和不是一个常数。它会随着舍入误差在账户之间转移而“波动”1 wei。在你的单元测试中使用近似相等。
  3. 清空余额并非简单: 在 Lido 中,转移你的“最大余额”通常会留下 1 份额。如果你的合约逻辑依赖于 0 余额状态(例如,关闭一个保险库),你必须明确清除剩余份额。
  4. 分批转移很危险: 在 Aave 中,“向上舍入”的偏差使得将余额分成多个转移成为高风险操作。最终的转移很可能会回滚,因为之前的转移“过度消耗”了底层份额。

黄金法则: 最安全的方法是以份额而不是代币金额进行内部会计处理。份额是合约存储中唯一的真相来源;代币余额只是该真相的一个移动的、舍入后的投影。

external-link

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

0 条评论

请先 登录 后评论
chainsecurity
chainsecurity
江湖只有他的大名,没有他的介绍。