Revest Finance由于ERC-1155回调机制存在漏洞,攻击者利用该漏洞盗取了价值约200万美元的代币。BlockSec团队在分析攻击后,发现Revest TokenVault合约中存在另一个更严重的零日漏洞,并提出了修复建议。该漏洞允许攻击者通过更简单的方式获得大量代币,项目方已部署新的Revest合约以缓解潜在的攻击。
2022 年 3 月 27 日,以太坊上的 Staking DeFi 项目 Revest Finance 由于 ERC-1155 的回调机制而遭受攻击。大约价值 200 万美元的代币(即 BLOCKS、ECO、LYXe 和 RENA)被盗。我们第一时间分析了这次攻击,并在当晚(UTC+8)发推 分析了这次攻击。
事实上,在撰写 Twitter 时,我们仍然对 Revest TokenVault 合约中的一个函数持有一些疑问。我们研究了该合约,试图了解其功能。后来,我们发现存在另一个关键的零日漏洞,可以通过更简单的方式利用该漏洞,并可能造成同样的巨大损失(就像已经发生的攻击一样)。
然后,我们立即联系了 Revest Finance 团队,他们迅速做出回应,并提出了该漏洞的解决方法。在确认该漏洞无法被利用后,我们决定发布这篇博客。
这篇博客由三个部分组成:Revest Finance 的机制、最初的重入攻击和新的零日漏洞。
Revest Finance 的金融非同质化代币 (FNFT) 使得未来锁定资产的权利能够以无需信任的方式转移。入口合约(Revest 合约)提供了三种不同的接口,通过锁定底层资产来铸造 FNFT:
mintTimeLock
:底层资产将在一段时间后解锁。mintValueLock
:当底层资产的价值上升到高于或低于规定值时,底层资产将被解锁。mintAddressLock
:底层资产将由规定的帐户解锁。Revest 合约与其他三个合约连接,以锁定和解锁底层资产。
fnftId
。该锁定规定了创建时新 FNFT 的总供应量。FNFT 不能以其他方式铸造,但可以被销毁以解锁底层资产。我们以 mintAddressLock
为例来说明铸造 FNFT 的过程。
图 1
图 2
以上两张图基本描述了 FNFT 是如何创建、铸造和销毁的。具体来说,用户 A 将 100 WETH 锁定到 Revest Finance 中,从而创建了相应的 FNFT,其 fnftId
为 1。最后,它以指定的份额将 100 1-FNFT 铸造给指定的接收者。
请注意,一旦底层资产被解锁,每个 1-FNFT 都可以被销毁以接收一个 (1e18) WETH。如图 2 所示,用户 B 通过销毁 25 1-FNFT 来提取 25 ( 1e18) WETH。
此外,Revest 合约提供了另一个接口,名为 depositAdditionalToFNFT
,它会导致将在下面讨论的两个漏洞。
我们首先使用下面的两张图来描述这个函数的正常使用。
图 3
图 4
函数 depositAdditionalToFNFT
用于将更多的底层资产锁定到现有的锁定(由 fnftId
指定)。合理地(图 3),它要求指定的数量与指定的 FNFT 的总供应量相同,然后将添加的资产均匀地分配给每个指定的 FNFT。
否则(图 4),它会使用最新的 fnftId
创建一个新的锁定,销毁指定数量的旧 FNFT 并铸造指定数量的新 FNFT,然后将新锁定的 depositAmount
记录为旧锁定的 depositAmount
和指定数量的总和,如下面的代码所示。
// Now, we transfer to the token vault
if(fnft.asset != address(0)){
IERC20(fnft.asset).safeTransferFrom(_msgSender(), vault, quantity * amount);
}ITokenVault(vault).handleMultipleDeposits(fnftId, newFNFTId, fnft.depositAmount + amount);emit FNFTAddionalDeposited(_msgSender(), newFNFTId, quantity, amount);
由于 TokenVault 合约中记录的 depositAmount
指示了一个指定的 FNFT 可以提取的底层资产数量,该操作将指定数量的旧 FNFT 的价值从旧锁定转移到新锁定。(指定的数量大于总供应量将回滚事务)
在这一部分,我们将说明重入攻击是如何工作的,并讨论根本原因和修复方法。
图 5
图 6
图 7
以上三张图基本描述了重入攻击的整个过程。具体来说,攻击者首先锁定零 RENA 代币来铸造 2 个没有价值的 1-FNFT。第二,攻击者再次锁定零 RENA 代币,但铸造了 360,000 个(现在)也没有价值的 2-FNFT。在最后一步中,攻击者通过 FNFTHandler 的回调机制(继承自 ERC-1155 代币标准)重新进入 Revest 合约的 depositAdditionalToFNFT 函数,该回调机制在更新 fnftId
之前覆盖了 fnftId
为 2 的锁定的 depositAmount
。因此,攻击者获得了 360,001 个 depositAmount
为 1e18 的 2-FNFT,这意味着他可以从 TokenVault 合约中提取 360,001 * 1e18 RENA。此外,唯一的成本是 1e18 RENA。
修复方法
Revest Finance 的代码完全符合经典的重入模式:使用 fnftId
-> 带有回调机制的外部调用 -> 更新 fnftId
。因此,修复问题的最直接方法是打破这种模式。修复后的代码如下所示:
function mint(
address account,
uint id,
uint amount,
bytes memory data
) external override onlyRevestController {
require(amount > 0, "Invalid amount");
require(supply[id] == 0, "Repeated mint for the same FNFT");
supply[id] += amount;
fnftsCreated += 1;
_mint(account, id, amount, data);
}
首先,它将更新操作移到外部调用(_mint
)之前,这可以避免攻击。其次,由于系统不允许铸造零FNFT 和重复铸造相同的 FNFT,因此它添加了两个检查以确保系统按预期工作,这可以提高系统的安全性。
在分析 Revest Finance 的代码时,TokenVault 合约中的函数 handleMultipleDeposits
使我们感到困惑,其代码如下所示。
function handleMultipleDeposits(
uint fnftId,
uint newFNFTId,
uint amount
) external override onlyRevestController {
require(amount >= fnfts[fnftId].depositAmount, 'E003');
IRevest.FNFTConfig storage config = fnfts[fnftId];
config.depositAmount = amount;
mapFNFTToToken(fnftId, config);
if(newFNFTId != 0) {
mapFNFTToToken(newFNFTId, config);
}
}
在调用 depositAdditionalToFNFT
函数期间,handleMultipleDeposits
函数会更改旧锁定的 depositAmount
或记录新锁定的 depositAmount
。当 newFNFTId
为零时,它不会记录新锁定的 depositAmount
,因为这是一个向现有锁定添加额外资产的操作。
根据我们对协议的理解,当 newFNFTId
不为零时,它只需要记录新锁定的 depositAmount
,而不需要更改旧锁定的 depositAmount
。但是,代码告诉我们,它不仅记录了新锁定的 depositAmount
,还更改了旧锁定的 depositAmount
,这与我们的理解相矛盾。
我们认为这是一个严重的 零日 逻辑漏洞,然后编写了一个 PoC 来验证这一点。以下三张图描述了 PoC 是如何工作的。
图 8
图 9
图 10
具体来说,攻击者首先锁定零 RENA 来铸造 360,000 个 1-FNFT。之后,攻击者直接调用 depositAdditionalToFNFT
函数来创建一个新的锁定。由于该漏洞,TokenVault 合约错误地将旧锁定的 depositAmount
从零更改为 1e18。因此,攻击者获得了 359,999 个价值 359,999 RENA 的 1-FNFT。很明显,PoC 比真正的重入攻击简单得多,因为它不需要重入调用。
这是一个逻辑错误,我们建议使用以下代码来修复它。
function handleMultipleDeposits(
uint fnftId,
uint newFNFTId,
uint amount
) external override onlyRevestController {
require(amount >= fnfts[fnftId].depositAmount, 'E003');
IRevest.FNFTConfig memory config = fnfts[fnftId];
config.depositAmount = amount;
if(newFNFTId != 0) {
mapFNFTToToken(newFNFTId, config);
} else {
mapFNFTToToken(fnftId, config);
}
}
由于两个易受攻击的合约:TokenVault 和 FNFTHandler 存储了许多关键状态,因此该项目无法在不迁移状态的情况下重新部署 TokenVault 合约和 FNFTHandler 合约。为了避免对该漏洞的进一步攻击,该项目重新部署了一个轻量级的 Revest 合约,该合约禁用了更复杂的功能,以减少任何潜在攻击者可利用的攻击面。在检查了该解决方法后,我们认为轻量级的 Revest 合约可以缓解本博客中提到的可能的攻击。
使 DeFi 项目安全并非易事。除了代码审计之外,我们认为社区应该采取积极的方法来监控项目状态,并在可能的情况下阻止攻击。
- 原文链接: blocksecteam.medium.com/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!