最常见的 Vault 漏洞(源自真实审计)

本文深入分析了构建与集成以太坊Vault(如ERC-4626)时的常见安全漏洞与逻辑陷阱。内容涵盖了首位存款人攻击、坏账抢跑、锁定机制漏洞及收益优化等核心问题,并提供了基于OpenZeppelin库的修复方案,旨在帮助开发者规避技术风险。

构建 Vault

  1. 首位存款者攻击

  2. 作为第一人的收益过高

  3. 抢跑坏账

  4. 利用 $1$ wei 戏耍锁定期

  5. 灰尘(Dust)如何导致资不抵债

与 Vault 集成

  1. 你的提现顺序正在抹杀收益

  2. convertToAssets + convertToShares == 彻底完蛋

  3. Token 闲置在那里,永远无人领取

  4. 闲置资产正在偷走用户的收益

构建 Vault

1. 首位存款者

我早就知道了”、“我要跳过这部分”,你现在心里一定是这么想的。那你干脆把整篇文章都跳过吧,蠢货,因为我不仅要讨论基础的 Bug,还会包含它的其他变体。现在,收起那种态度,我们继续。

这是 Vault 最常见的攻击,发生在没有虚拟份额(Virtual Shares)或偏移(Offset)机制的情况下。核心问题在于份额是如何计算的:

Solidity 会向下取整,如果 totalShares 非常小(例如 $1$ wei)且 totalAssets 大于 assets,那么 shares 将向下取整为 $0$。

post image

常见模式如下:

  1. 攻击者存款 $1$ wei 资产以换取 $1$ 个份额

  2. 攻击者向 Vault 发送 $1000e18$ 资产

  3. Vault 的份额比例变为 $1 \text{ share} = 1000e18 + 1 \text{ assets}$

  4. 受害者存款 $1000e18$ 资产,铸造出 $0$ 个份额

  5. 攻击者的份额现在价值 $2000e18$ 资产

切入点:

无法铸造 $0$ 份额

post image

捐赠受害者资产的 $51\%$,以便让他能铸造 $1$ 个份额,这样你就能窃取他 $25\%$ 的资产(总份额为 $2 \text{ total shares}$ - 总资产为 $1500e18 \text{ total assets}$,则 $1 \text{ share}$ 价值 $750e18$)。

Vault 不通过余额追踪资产 —— 环顾四周,看看是否有办法在不增加份额的情况下增加该变量。这些年来我注意到的一些有趣的变体包括:

  • 通过存款并配合向下取整的提现来接收较少的资产,从而增加 totalAssets(重复此操作几十次)

  • 清算你的仓位,使你拥有的份额减少,而 Vault 拥有额外的资产(Vault 具有某些额外功能)

使用 $shares = assets \times (totalShares + 1) / (totalAssets + 1)$ 进行 $1$ 的偏移 —— 攻击不再有利可图,但份额价值仍然可以被拉高以对 Vault 进行恶意骚扰(Griefing)。

更大的偏移 —— 既不利可图,也不会对比例产生太大改变,不被视为攻击。

修复:

OZ(OpenZeppelin)提供了一个非常简单且易于实现的修复方案,它使用 _decimalsOffset 来创建虚拟份额。但基本上,你可以将 _convertToShares_convertToAssets 重新实现为:

post image

2. 作为第一人的收益过高

这里的问题在于,大多数分配奖励的 Vault 都是基于时间(Epoch 或滴灌方式)进行的,但它们没有考虑到有时可能没有人来领取奖励(没有存款者)。

这种问题的常见模式是:

  1. Vault 创建后立即开始分配奖励

  2. 第一位存款者/质押者在 $X$ 小时/天后加入

  3. 无论他质押多少,由于他控制了 $100\%$ 的份额,他将获得那段没有质押者的 $X$ 小时/天内的所有奖励

切入点:

他们可能会处理这种情况,但如果所有人离开 Vault 一段时间后,有一个用户存款,奖励会暂停还是他会收到那段错过时间内的所有奖励?

根据存款者进入时间分配奖励 —— 那么在没有人留在 Vault 时生成的奖励该怎么办,它们会被锁死吗?

修复:

由于该问题更多地与设计/业务逻辑相关,因此没有特定的代码修复方案。但在结构上,它应该仅在 $shares > 0$ 时分配奖励,并在第一个用户加入时开始追踪奖励开始时间。

3. 抢跑坏账

如果 Vault 存在任何会导致利润或亏损的外部因素,但无法实时接入链上,或者可以在发生前被观察到,那么份额价格就可能被 MEV。

这种描述使问题复杂化了,但归结起来就是:

  1. 用户 1 将被清算,并导致 Vault 产生坏账

  2. 用户 2 抢跑清算交易(TX)并提现

  3. 用户 1 被清算,将份额价格从 $1$ 降至 $0.9$

  4. 用户 2 再次存款

用户 2 避免了坏账社会化,本质上是通过 MEV 攻击 Vault,以保护自己的资产免受坏账影响。

切入点:

坏账没有被社会化 —— 那是另一个问题。如果不核算损失,会导致什么后果,是否会导致资不抵债?

修复:

如果此类事件可以在发生前被观察到,请考虑让用户发出提现请求,然后设置一段延迟时间(如 $1$ 小时、$24$ 小时等),在此时间之后他们才能进行提现。

4. 利用 $1$ wei 戏耍锁定期

存款 -> 你的资产被锁定 -> $T$ 时间后你可以持有或立即提现。我注意到的关于归属(Vesting)最常见的问题是,归属通常在第一次存款/提现(简称 D/W)时设置一次,之后的每次 D/W 只是增加金额。这可以通过 D/W $1$ wei 来利用,等归属到期后再处理剩余部分。

示例:

  1. 用户 1 存款 $1$ 个 Token

  2. 他的 Token 被锁定 $1$ 周

  3. 他现在可以提现了,但他又存入了剩余的 $99$ 个 Token

  4. 他现在拥有 $100$ 个 Token 的存款,并且可以随时提现

当存款是即时的,但你需要通过归属才能提现时,情况也是一样的。

切入点:

每次操作都会重置你的归属时间 —— 我们能否为别人进行 D/W 并重置他们的归属时间?

每次 D/W 都是一个独立的仓位,拥有自己的归属时间和金额 —— 账务处理会变得非常混乱,因为对于每个用户,我们需要追踪多个仓位及其总合并价值。这会增加很多额外的逻辑。

修复:

最佳建议是在每次新实例中重置 D/W 归属时间,并阻止其他用户进行存款。

然而,如果你希望用户可以随时 D/W 而不受到锁定期惩罚,那么可以考虑将每次 D/W 变成独立的仓位,并在 Mapping 中追踪用户总额。这会增加很多复杂性,但如果处理得当,这是最好的解决方案。

5. 灰尘(Dust)如何导致资不抵债

到目前为止,这是所有问题中最常见的一个。但它是什么呢?

众所周知,Solidity 的数学运算并不精确,所有的除法都会向下取整,如果你在销毁多少份额时向下取整,你本质上是在“奖励”用户(尽管金额只是灰尘级)。

最简单的例子就在这里,当份额价格发生变化时,我们可能会少销毁 $1$ 个份额,本质上多给了用户 $1$ 个资产。

post image

但是 $1$ wei 怎么会危险呢?通常情况下它并不危险,这些问题通常被归类为低风险(Low)或信息(Info),但在极少数情况下,它会导致资不抵债。这通常发生在资产是固定且与 Vault 余额分开核算时(例如,使用一个变量而不是 usdc.balanceOf(vault))。

切入点:

但它在存款时也会取整,所以它抵消了! —— 是的,除非存款和提现的金额完全一致。用户可以存款一次,然后提现 $1000$ 次。

修复:

最简单的解决方案是使用 OZ Math 并简单地向有利于系统的方向取整。

post image

向有利于系统的方向取整意味着你总是给用户更少的资产,并销毁用户更多的份额。

与 Vault 集成

1. 你的提现顺序正在抹杀收益

另一个简单但经常被忽略的 Vault Bug。当将 Vault 作为收益来源,或处理任何其他赚取收益的实体组合时,如何以及何时向每个实体存款,以及如何以及何时从中提现(即你的优先级排序)至关重要。

最简单的例子是项目使用基础的 FIFO(先进先出)顺序,通常顺序如下:

  1. Vault1 - $15\%$ APY

  2. Vault2 - $10\%$ APY

  3. Vault3 - $5\%$ APY

从第一印象来看,这似乎没问题。我们先存入并填满第一个 Vault,因为它收益最高,然后是第二个和第三个。

但如果你是个聪明人,你会注意到提现时也是同样的顺序。我们总是从 APY 最高的 Vault 中提现,这意味着它永远不会被完全填满。这反过来会显著降低收益。

示例:

  • 所有 Vault 的上限均为 $100k$

  • Vault1 和 Vault2 已达到上限

  • Vault3 存入了 $50k$

  1. 我们赚取 $15\% \times 100k + 10\% \times 100k + 5\% \times 50k = 27.5k$

  2. 用户提现 $50k$

  3. Vault1 的 totalAssets 减少到 $50k$

  4. 现在我们赚取 $15\% \times 50k + 10\% \times 100k + 5\% \times 50k = 20k$

而如果我们从 Vault3 提现,我们本可以赚取:

$15\% \times 100k + 10\% \times 100k + 5\% \times 0 = 25k$

修复:

不要使用 FIFO,而是使用 FILO(先进后出),并根据我们赚取的 APY 对数组进行排序,以优化最高收益。

2. convertToAssets + convertToShares == 彻底完蛋

想象一下,你需要算出你的份额代表多少资产。你的合约会调用什么函数?最显而易见的莫过于 convertToAssets,这个名字暗示它会将我们的份额转换为资产,对吧?

那是错误的答案。

它的问题在于它们没有考虑任何提现费用或交易滑点,这意味着它将返回一个虚高的资产数量。

绝不能包含对 Vault 中的资产收取的任何费用。

post image

修复:

将份额转换为资产的正确函数是 previewWithdraw。它将返回一个精确的版本,即如果你调用提现函数,你会提现到多少资产,但不考虑任何 Vault 或用户的限制(即返回你拥有多少资产)。

post image

反向函数 convertToSharespreviewDeposit 也是如此。

3. 奖励

Vault 不仅以份额价格上涨的形式分配收益,而且还会为特定策略奖励某种激励 Token,这非常常见。最简单的例子,也是我们最近在一次审计中发现的,是一个在 Morpho 上赚取收益的 Vault,但它没有处理 Morpho 奖励

是否处理了奖励 Token?

最基础的错误 —— Vault 集成了一个发放奖励 Token 的协议,但从未领取或分配它们。它们不断累积,闲置在那里并永远被锁死。

是什么 Token,它们会改变吗?

协议可以随时增加或轮换激励 Token。你的 Vault 是否将它们硬编码为一种 Token —— 这不好。建议为奖励 Token 使用一个 Map。

谁可以触发领取?

某些协议允许任何人调用领取函数,这意味着奖励 Token 可能会在没有任何预警的情况下直接发送到你的合约地址。如果你的 Vault 没有被设计为处理意外的 Token 转移,那些奖励就会被锁死。

4. 上限和静态 Token

另一个很容易发现(如果你知道的话)的 Bug 是,某些 Vault 设有最大上限。无论是出于何种原因,它们都不允许存入超过 $X$ 数量的资产。

简单明了,那问题出在哪呢?

问题在于大多数开发者集成此类 Vault 的方式,因为他们不知道或没有考虑到 Vault 填满且存款交易回滚的可能性。

有时没有进行检查,即使 Vault 中仍有一些剩余空间,存款也会直接回滚。

示例:

  1. 指数 Vault 向其他 Vault 存款

  2. 还有 $5000$ 资产的存款空间

  3. 用户调用存款存入 $8000$ 资产,但交易回滚

  4. 用户离开

post image

一些开发者通过实现 maxDeposit 来修复它。现在我们确保填满了 Vault,所有资产都得到了利用!

post image

但这算修复吗?你看到这里的问题了吗?

这个 Bug 并不是关于当 Vault 填满时存款回滚,而是关于当它没填满时。这里的问题在于,在内部 Vault 已填满的最后一次存款之后,我们仍然允许更多存款。这些额外的资产不会以产生收益的方式被使用,但会参与该收益的分配。通过数学计算可以最好地展示这一点:

  1. 指数 Vault1 拥有 $100k$ 资产

  2. $50k$ 存入 $15\%$ APY 的 Vault

  3. $50k$ 存入 $5\%$ APY 的 Vault

  4. 用户赚取 $10\%$ APY

对比:

  1. 指数 Vault2 拥有 $120k$ 资产

  2. $50k$ 存入 $15\%$ APY 的 Vault

  3. $50k$ 存入 $5\%$ APY 的 Vault

  4. $20k$ 处于闲置状态,因为两个 Vault 都已填满

  5. 用户赚取 $8.3\%$ APY

修复:

将所有 Vault 填满至 $100\%$,然后阻止更多用户存款。

仅针对开发者

正在构建 Vault 或任何处理份额的系统吗?

如果你在未来 $60$ 天内有主网发布、升级或 Token 活动,且你的合约尚未经过重新审计,那你就是在凭运气运行。

在此预约我们的安全审计评审电话:

https://phagesecurity.com/request-audit

或者在 TG 上联系我:@Pyro3b

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

0 条评论

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