​More than Re-entrancy : Revest Finance 被攻击事件分析

avatar
BlockSec
2年前
本文约2376字,阅读全文需要约3分钟
以太坊上的staking DeFi项目 Revest Finance遭到黑客攻击,损失约200万美元。

2022年3月27日,以太坊上的staking DeFi项目 Revest Finance 遭到黑客攻击,损失约200万美元。BlockSecTeam团队第一时间介入分析,并在tweeter上向社区分享了我们的分析成果。事实上,就在我们通过tweeter向社区分享我们的分析成果时,我们发现了Revest Finance的TokenVault合约中还存在着一个critical zero-day vulnerability。利用该漏洞,攻击者可以用更加简单的方式盗取协议中的资产。于是我们立刻联系了Revest Finance项目方。在确定该漏洞已经被修复后,我们决定向社区分享这篇blog。

0. Whats the Revest Finance FNFT

Revest Finance是针对DeFi领域中staking的解决方案,用户通过Revest Finance参与的任何DeFi的staking,都可以直接生成一个NFT,即 FNFT (Finance Non-Fungible Token) , 该NFT代表了这个staking仓位的当前以及未来价值。用户可以通过Revest Finance 提供的3个接口和项目进行交互。质押自己的数字资产,mint 出相应的 FNFT 。

mintTimeLock : 用户质押的数字资产在一段时间之后才能被解锁。

mintValueLock : 用户质押的数字资产只有在升值或者贬值到预设数值才能被解锁。

mintAddressLock : 用户质押的数字资产只能被预设的账户解锁。

Revest Finance 通过以下3个智能合约完成对用户存入的数字资产的锁定和解锁。

FNFTHandler : 继承自ERC-1155 token(openzepplin实现) 。每次执行lock操作时,fnftId会进行自增(fnftId 类似于ERC721中的tokenId)。FNFT在被创建时,用户需要指定它的totalSupply。当用户想要提走FNFT背后的underlying asset,需要burn掉相应比例的FNTF。

LockManage : 记录FNFT被解锁(unlock)的条件。

TokenVault : 接收和发送用户存入的underlying asset,并记录每一种FNFT的metadata 。例如fnftId =1的FNFT背后质押的资产类型。

因为此次攻击,黑客攻击的入口是mintAddressLock函数,那么我们以该函数为例,讲述FNFT的生命周期。

​More than Re-entrancy : Revest Finance 被攻击事件分析

User A调用Revest的 mintAddressLock 函数

  • unlocker : User X -> 只有User X 可以解锁这笔资产recipients : [User A , User B , User C] quantities : [50 , 25 , 25] -> mint 数量为100 (sum (quantities)) , User A , User B , User C 各拥有50 , 25 ,25 枚 。asset : WETH -> mint 出的 FNFT 以 WETH 为抵押品。depositAmount : 1e18 -> 每一枚 FNFT 背后的抵押品数量为 1枚WETH ( WETH decimal 为18 )

  • 假设当前系统中没有其他FNFT, User A 通过mintAddressLock 与系统进行交互,FNFTHandler返回的fnftId = 1 

  • LockManger 为其添加相应的记录

  • fnftId : 1unlocker : User X 

  • Token Vault 为其添加相应的记录

  • fnftId : 1asset : WETHdepoistAmount : 1e18

接着Token Valut 要从 User A 这里转走 100 * 1e18 数量的WETH 。

最后系统分别给 User A , User B , User C mint 50 , 25 ,25 枚 01-FNFT 。

通过mintAddressLock 函数铸造FNFT 就完成了。

​More than Re-entrancy : Revest Finance 被攻击事件分析

当User X 解锁 01-FNFT 后,用户B 便可以通过withdrawFNFT提走underlying asset 。如图二所示,User B 想要提取自己手中持有的25个 01-FNFT 质押的数字资产。

协议首先检查01-FNTF是否已经 unlock ,如果已经unlock, 那么协议会burn掉User B的25个01-FNFT,并给他转25*1e18数量的 WETH 。此时01-FNFT 的 totalSupply 为 75 。

Revest 合约还提供了另外一个接口,叫做depositAdditionalToFNFT,以便让用户为一个已经存在的 FNFT 添加更多的underlying asset 。下面我们用2张图描述它的“正常”用法。

​More than Re-entrancy : Revest Finance 被攻击事件分析

这里有三种情况 

一.quantity == 01-FNFT.totalSupply() 如图三所示

以图二中的场景为上下文,User A 要为 01-FNFT 添加更多的抵押物。

quantity = 75 -> 为75个 01-FNFT 追加质押 。

amount = 0.5*1e18 -> 每一枚 01-FNFT 追加0.5*1e18 数量的WETH 。

于是User A 需要向 Token Vault 转入 37.5*1e18 WETH (75 * 0.5*1e18) Token Vault 修改系统记账,将depositAmount修改为 1.5*1e18。现在每一枚01-FNFT 承载的资产为1.5*1e18 WETH 。

此时User C 调用withdrawFNFT ,burn掉他持有的25枚 01-FNFT ,他可以拿走25*(1.5*1e18) = 37.5*1e18 WETH 。

于是,此时01-FNFT 的totalSupply为50 。

​More than Re-entrancy : Revest Finance 被攻击事件分析

二.quantity < 01-FNFT.totalSupply() 如图四所示 

以图三中的场景为上下文,User A 继续为01-FNFT添加更多的抵押物。

  • quantity = 10 -> 为10枚 01-FNFT 追加质押。amount = 0.5*1e18 -> 为10枚 01-FNFT 每一枚追加0.5*1e18 WETH

  • 由于quantity < 01-FNFT.totalSupply()
    于是,User A 向协议支付 5*1e18 WETH 系统将会burn掉 10枚 01-FNFT ,mint 出10枚 02-FNFT ,并将burn掉的10枚01-FNFT承载的资产和User A 新转入的资产,注入到02-FNFT中。于是就有

  • fnftId : 2asset : WETHdepositAmount : 2.0*1e18 (1.5*1e18 + 0.5*1e18) 

  • 此时 

  • 01-FNFT.totalSupply : 40 01-FNFT.depositAmount : 1.5*1e18 (逻辑上应该如此,见后文: the New Zero-day Vulnerability)02-FNFT.totalSupply : 10 02-FNFT.depositAmount : 2.0*1e18

三.quantity > 01-FNFT.totalSupply()

这种情况,交易会revert。

1. Whatt the Re-entrancy vulnerability

在理解了mintAddressLock 函数 和 depositAdditionalToFNFT 函数的基本工作流程后,来看一下攻击者使用的重入手法。 假定the lastest fnftId = 1(不影响理解)

​More than Re-entrancy : Revest Finance 被攻击事件分析

如图五所示
第一步:
攻击者调用 mintAddressLock 函数 

depositAmount = 0 

quantities = [2] 

mint 出了 2枚 01-FNFT , 由于攻击者将 depositAmount 设置为0 ,因此他没有转入任何数字资产。相当于01-FNFT 背后承载的underlying asset 为0 。

第二步:攻击者再次调用 mintAddressLock 函数 

depositAmount = 0

quantities = [360000] 准备mint 36w 枚02-FNFT depositAmount为0 。

在mint 的最后一步,攻击者利用ERC-1155 的call-back 机制重入了 depositAdditionalToFNFT 函数 。(详见下面给出的 _doSafeTransferAcceptanceCheck函数)

​More than Re-entrancy : Revest Finance 被攻击事件分析

​More than Re-entrancy : Revest Finance 被攻击事件分析

在depositAdditionalToFNFT 中 , 攻击者传入 

quantity = 1

amount = 1*1e18

fnftId = 1 

因为 quantity < fntfId.totalSupply(),因此协议会burn掉攻击者1枚01-FNFT, 铸造1枚02-FNFT。(02-FNFT在协议中已经存在,但是fnftId 更新延迟)然后修改fnftId =2 的depositAmount 为 amount。相信你已经发现,这一步,攻击者通过重入将 fnftId = 2 的 depositAmount 从0修改为1.0*1e18 , 仅仅花费1*1e18 RENA 就获得了 (360000 +1 ) * 1*1e18 RENA 的系统记账。

​More than Re-entrancy : Revest Finance 被攻击事件分析

最后攻击者调用withdrawNFNFT函数,burn掉 360,001枚02-FNFT,取走了360,001*1e18 RENA 。

  • 建议修复方法

​More than Re-entrancy : Revest Finance 被攻击事件分析

2. the New Zero-day Vulnerability

在blockSecTeam团队分析Revest Finance 的代码时,handleMultipleDeposits 函数引起了我们的注意。

​More than Re-entrancy : Revest Finance 被攻击事件分析

当用户调用 depositAdditionToNFT 函数追加抵押物时,该函数会改变 FNFT 的 depositAmount 。从代码中我们可以发现,当newFNFTId != 0 时,该函数既改变了 fnftId 对应的 FNFT 的depositAmount 也改变了 newFNFTId 对应的 depositAmount 。 

按照常理,当 newFNFTId !=0 时,系统应该只记录 newNFTId 对应的 depositAmount 。不应该改变 fnftId 对应的depositAmount 。 

我们认为这是一个非常严重的逻辑bug ,利用该漏洞,攻击者可以很轻松提走系统中的数字资产。下面用3张图描述模拟攻击的原理。
假定 the latest fnftId = 1 

​More than Re-entrancy : Revest Finance 被攻击事件分析

首先攻击者调用 mintAddressLock 函数,mint出360000个01-FNFT 。攻击者将 amount 设置为0 因此他不必转入任何资产到Revest Finance 协议中。
mint 结束后,攻击者拥有360000 枚 depositAmount =0 的 01-FNFT 。

​More than Re-entrancy : Revest Finance 被攻击事件分析

然后攻击者调用 depositAdditionalToFNFT 函数,参数如下

fnftId = 1

amount = 1 * 1e18

quantity = 1

协议转走攻击者 amount * quantity 数量的代币 ,即 1 * 1e18RENA
协议会burn掉攻击者1枚01-FNFT , 并为其铸造一枚02-FNFT(假定 latest fnftId = 2)
按照 handleMultipleDeposits 函数中的逻辑, fnftId = 2 的资产,其 depositAmount 会被设置为 1.0*1e18。
但是 fnftId = 1的资产,其depositAmount 也会被设置为 1.0*1e18 ,而这个值本应该为0!

​More than Re-entrancy : Revest Finance 被攻击事件分析

第三步,攻击者直接提款,将手中所有的 01-FNFT 提现。不考虑gas费,他将净赚 359,999 * 1e18 数量的 REAN 代币。 

很显然,使用这种方式进行攻击,比真实的重入攻击更加简单直接。

  • 建议修复方法

针对该漏洞,blockSecTeam团队给出了相应的patch方法。

​More than Re-entrancy : Revest Finance 被攻击事件分析

3. 项目方的修复方式

由于TokenVault and FNFTHandler 两个漏洞合约存储了许多关键的状态,无法在短时间内重新部署它们, 为了快速恢复使用, Revest Finance 官方重新部署了Revest 合约 (https://etherscan.io/address/0x36c2732f1b2ed69cf17133ab01f2876b614a2f27#code)的精简版本。该版本关闭了大部分复杂的功能,以避免被进一步攻击。项目方将在未来迁移状态并重新部署修复过的合约。

4. 总结

提升DeFi项目的安全性不是一件容易的事情。除了代码审计,我们认为社区应该采取更加主动的方式,例如项目监控预警、甚至是攻击阻断 使得DeFi社区更加安全。(https://mp.weixin.qq.com/s/o41Da2PJtu7LEcam9eyCeQ).

参考文献

*[1]: https://blocksecteam.medium.com/revest-finance-vulnerabilities-more-than-re-entrancy-1609957b742f

原创文章,作者:BlockSec。转载/内容合作/寻求报道请联系 report@odaily.email;违规转载法律必究。

ODAILY提醒,请广大读者树立正确的货币观念和投资理念,理性看待区块链,切实提高风险意识;对发现的违法犯罪线索,可积极向有关部门举报反映。

推荐阅读
星球精选