ERC-4626 Vaults和类Vault合约的共享漏洞:深度分析 第2部分

  • cr0___
  • 发布于 2023-10-18 22:53
  • 阅读 11

本文深入探讨了 vault 和 vault-like 合约中常见的两种漏洞:份额膨胀和不正确的舍入。份额膨胀发生在攻击者操纵 shares-to-deposited-assets 比例时,通过成为第一个存款人并直接向合约转移资产来实现。不正确的舍入可能导致用户在提取资产时获得比预期更多的份额,从而耗尽合约资金。文章还提供了具体的攻击场景和代码示例,并将在后续文章中介绍针对这些漏洞的缓解措施。

由 Alexis Williams 于 2023 年 6 月 15 日撰写。

Vault 和类 vault 合约的共有漏洞

现在我们已经介绍了 ERC-4626 标准,并确定了符合 ERC-4626 的 vault 合约、非 ERC-4626 的 vault 合约和类 vault 合约之间的共同特征,我们可以讨论这些合约之间共有的漏洞。为了简单起见,当我在本文中提到智能合约时,我指的是所有这三类 vault 合约。

份额膨胀

当用户将一定数量的资产存入合约时,他们会收到一定数量的新资产,或份额(share),该数量与存入的总资产成正比。这个份额可能是一个单独的代币,但在某些情况下,它只是一个内部状态变量。因此,份额与底层资产之间的关系是一个比率,其中分子是份额的总量乘以要存入的资产数量,分母是当前底层资产的总量。

这个计算是我们第一个漏洞的来源:份额膨胀。当攻击者操纵份额与已存入资产的比率时,就会发生份额膨胀。攻击者要操纵这个比率,必须满足两个先决条件:

  1. 合约没有或存款余额非常低。
  2. 合约的份额计算分母必须能够独立于分子移动。

所有已创建的 ERC-4626、非 ERC-4626 和类 vault 合约都满足第一个先决条件。需要注意的是,并非在所有情况下都需要满足这个先决条件,我们将在第三部分中详细讨论。第二个先决条件有点难以理解,所以让我们来详细了解一下。

你可能还记得,我们之前将存款计算的分子映射为总份额乘以已存入的资产数量,分母映射为合约中的总资产数量。考虑到这种映射,我们可以将第二个先决条件重新表述为:

合约的当前总资产数量必须能够独立于总份额乘以已存入的资产数量而移动。

奖励机制

对于所有 ERC-4626、非 ERC-4626 和类 vault 合约来说,这个说法本质上是成立的,因为它启用了允许合约奖励已将资产存入合约的用户的机制。合约的奖励会直接转移到合约本身,从而增加用户持有的每个份额的价值。当用户将份额兑换成资产时,由于奖励已直接存入合约,他们将收到比他们存入的更多的资产。例如,如果一个合约有 100 个资产和 100 个份额,那么每个份额价值 1 个资产。如果随后将 100 个资产作为奖励转移到合约,那么每个份额现在价值 2 个资产。

重要的是要注意,正常的存款机制不能用于向合约添加奖励。如果使用存款机制,将创建份额并将其提供给存款人(很可能是协议中的另一个智能合约),从而使份额与已存入总资产的比率保持不变。这种方法不会为用户存入资金提供任何激励。

滥用奖励机制

用户因将资产存入合约而获得奖励的方式也是攻击者可以用来操纵合约初始状态,从而过度膨胀其份额价值的机制。通过成为第一个存款人,攻击者能够设置合约中底层资产的数量,并设置份额与底层资产的初始比率。通常,攻击者将存入 1 wei 的任何资产并收到 1 个份额。这是因为第一次存款不能使用比率计算,因为它会涉及到除以零。因此,第一次存款计算只是返回与存入的资产数量相等的份额数量。

// mint the first deposit to the receiver
if (totalSupply() == 0) {
    IERC20(asset()).safeTransferFrom(msg.sender, address(this), amount);
    _mint(receiver, amount);
    return amount;
}

对于已经实施 ERC-4626 vault 的读者来说,上面的代码片段可能看起来很眼熟。事实上,这段代码片段来自 OpenZeppelin 自己的 ERC-4626 合约模板 v4.7!令人惊讶的是,很少有 ERC-4626 合约模板实施自己的缓解份额膨胀漏洞的措施。这意味着份额膨胀很可能存在于实施 ERC-4626 的协议中。然而,OpenZeppelin 最近发布了 ERC-4626 模板的新版本,该版本缓解了份额膨胀。在本深度剖析系列的最后一部分中,我们将详细讨论份额膨胀的缓解措施。

攻击

现在我们了解了此漏洞存在的条件并有我们的代码片段参考,我们可以讨论攻击场景。请注意,在前面的代码片段中,第一次存款只是返回与存入的资产数量成正比的份额数量。这意味着如果用户存入 1 WETH,或者由于小数缩放而存入 1e18 WETH,他们将收到 1e18 个份额。

因此,作为第一个存款人,攻击者将能够通过存入 1 wei 的资产并收到 1 个份额,将份额与已存入资产的比率设置为任意小的值。然后,当受害者提交交易以存入资产时,攻击者会启动攻击的第二步。通过像合约本身奖励用户的方式一样,直接将一定数量的资产转移到合约,攻击者能够独立于分子来操纵存款计算的分母。如果攻击者转移的资产足够多,以至于当受害者用户去存款时,分母大于分子,攻击者可能会导致计算期间的除法截断。请注意,攻击者需要将其直接存款交易紧挨着受害者的合法存款交易之前,因此需要抢先交易。

关于除法截断的简要说明:在 Solidity 中,当你将一个整数除以另一个整数时,输出的预期类型是一个整数(参考)。 此外,Solidity 不支持浮点数或定点数,即小数。 因此,当你将一个整数除以另一个整数,结果是小数时,小数会四舍五入为零。 这也称为截断。 例如,如果你在 Solidity 中将 5 除以 2,则输出将为 2 而不是 2.5,因为 0.5 被截断为 0。如果你将 2 除以 5,结果将为 0。

现在回到黑客攻击。 通过在受害者存入自己的资产之前,将足够的资产直接转移到合约中,使得分母大于分子,攻击者能够导致除法截断。 这意味着受害者收到的存款份额为 0,并且将无法提取他们刚存入的资产,因为他们没有任何可用于兑换已存入资产的份额! 然后,当攻击者赎回 1 个份额时,它的价值是合约中的全部资产,因此是 1 wei + 攻击者直接存入的资产 + 受害者存入的资产。 攻击者拥有的 1 个份额现在比最初的价值更高,这是因为攻击者操纵了合约,因此称为“份额膨胀”。

让我们通过一个示例场景来了解一下如何通过实际数字进行此攻击:

  1. 部署了一个新的 ERC-4626、非 ERC-4626 或类 vault 合约,它接受的资产是 WETH。 WETH 的小数位数为 18。
  2. 攻击者 Alice 注意到已部署了一个新合约。 她将 1 wei 的 WETH 存入合约并收到 1 个份额。
  3. 受害者 Bob 决定将 1 WETH(1e18 wei)存入合约,根据公式:( (1 share * 1e18 wei) / 1 wei ),预计会收到 1e18 个份额。
  4. 在 Bob 的交易完成之前,Alice 抢先了他的交易。 她直接将 1 WETH 转移到合约。
  5. 现在,当计算 Bob 的存款时,计算结果为:

( (1 share * 1e18 wei) / (1e18 wei + 1 wei) )

由于分母大于分子,因此发生除法截断,Bob 的 1 WETH 存款收到 0 个份额。

  1. Alice 的 1 个份额现在价值 2 WETH + 1 wei,而 Bob 损失了他的存款。

现在我们已经了解了份额膨胀的基础知识,我将向你介绍关于此漏洞的最后一个题外话。 上面的示例主要侧重于直接操纵合约中的总资产。 但是,攻击者可能无法直接将资产转移到合约,但仍然能够操纵总余额。 可能发生间接操纵的假设场景是,如果攻击者能够直接增加授予合约的奖励金额。 这将具有与独立于分子任意增加分母的相同效果。

不正确的舍入

ERC-4626 标准兼容的 vault 合约、非 ERC-4626 vault 合约和类 vault 合约之间共享的第二个漏洞涉及舍入,这是一个困扰许多智能合约的问题。 具体来说,对于这三类 vault 合约,当存款和取款资产对用户有利时,舍入会成为一个问题。 有利的舍入意味着用户相对于他们存入的资产数量收到更多的份额,并且相对于他们赎回的份额数量收到更多的资产。 当开发人员没有相对于用户调用的函数在正确的方向上进行舍入时,就会发生这种情况。

向上或向下舍入

ERC-4626 标准对执行各种操作时以哪种方式进行舍入有非常具体的规定。 从 ERC-4626 标准 释义,指定的舍入方向如下:

  • 如果 (1) 该函数正在计算要为用户提供的特定数量的底层代币发行多少份额,或者 (2) 它正在确定要转移给他们以换回特定数量的份额的底层代币数量,则应向下舍入。
  • 如果 (1) 该函数正在计算用户必须提供多少份额才能收到给定数量的底层代币,或者 (2) 它正在计算用户必须提供多少底层代币才能收到特定数量的份额,则应向上舍入。

不幸的是,ERC-4626 中舍入规范的措辞令人困惑,使得编码 vault 实现的开发人员很容易误解它。 让我们来看一个有利舍入的例子:

在我们合约的上面的代码片段中,我们看到用户可以指定他们想要提取的底层资产的数量。 然后,该函数使用计算来确定要从用户那里扣除的相应份额数量。 但是,通过向下舍入,该函数为攻击者操纵合约提供了机会。 如果攻击者提供他们想要提取的资产数量,使得计算出的份额数量向下舍入为 0,那么他们实际上是免费提取了资产(请记住,如果分子小于分母,Solidity 会舍入为 0)。

攻击

现在,让我们通过一个使用上述代码片段的场景,其中包含实际数字:

  1. 攻击者 Alice 和受害者 Bob 都已将 1 WETH 存入新合约。 Alice 和 Bob 都拥有 1e18 份额。 请注意,份额使用的数量级与底层资产相同,并且 WETH 有 18 个小数位。
  2. 合约收到 1 WETH 的奖励,该奖励直接转移到合约。 现在,合约中的总资产为 3 WETH。
  3. Alice 决定从合约中提取 1 wei 的 WETH。 合约使用以下计算来计算要从 Alice 中扣除的份额数量:

(1 wei * 2e18 shares) / (3e18 wei)

由于分子小于分母,因此将从 Alice 的帐户中扣除 0 份额。

  1. Alice 将收到 1 wei 的 WETH,但不会从她在合约中的余额中扣除任何份额。 Alice 可以一遍又一遍地重复此过程,以耗尽合约的资金。

请注意,在上面的示例中,分母必须独立于分子变化,这是此攻击的先决条件。 如果份额的总供应量(分子)和资产的总量(分母)相同,那么不可能发生除法截断。 如前所述,为了奖励存款人,所有符合 ERC-4626、非 ERC-4626 和类 vault 合约都必须能够让分母独立于分子移动。 此外,Alice 可以通过直接将 WETH 转移到合约来自己移动分母。 但是,Alice 每次只能窃取 1 wei 的 WETH,由于此攻击向量的 gas 成本通常超过利用带来的回报,因此此攻击不太可能在实际合约上发生。 Alice 可以直接转移大量的 WETH 以使攻击更有利可图,但这需要更大数量级的 WETH(100、1,000、10,000 等),这进一步限制了此漏洞的可行性。

这种舍入行为还有另一个后果。 当 Bob 尝试提取他预期的资产总量时,合约将遇到整数下溢并恢复。 这是因为 Alice 在没有减少她的份额数量的情况下移除了资产,从而导致合约中出现赤字。

结论

在我们分为三个部分的深度剖析的第二部分中,我们讨论了三个已识别的 vault 类别之间共享的两个漏洞。 请继续关注我们深度剖析的最后一部分,我们将讨论针对份额膨胀的几种拟议的有效和无效的缓解措施,并介绍不正确舍入的缓解措施。

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

0 条评论

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