2022 年 3 月 27 日,以太坊上的 DeFi 質押項目 Revest Finance 遭到攻擊,原因是 ERC-1155 的回調(call-back)機制,導致價值約 200 萬美元的代幣(即 BLOCKS、ECO、LYXe 和 RENA)被盜。我們在第一時間分析了此次攻擊,並在當晚(UTC+8)發佈了相關推文解析。
事實上,在撰寫推文時,我們對 Revest TokenVault 合約中的一個函數仍存有疑慮。我們深入研究了該合約以瞭解其功能,隨後發現這實際上是另一個關鍵的「零時差」(zero-day)漏洞。該漏洞可以透過一種遠為簡單的方式進行利用,並可能造成同樣巨大的損失(如已發生的攻擊事件)。
我們隨後立即聯繫了 Revest Finance 團隊,他們反應迅速並針對該漏洞提出了變通方案。在確認漏洞無法被觸發後,我們決定發佈這篇博客。
本文接下來將分為三個部分:Revest Finance 的運行機制、原始的重入攻擊,以及新的*「零時差」漏洞*。
什麼是 Revest Finance FNFT
Revest Finance 的金融非同質化代幣(Financial Non-Fungible Token, FNFT)實現了鎖定資產未來權益的去中心化轉移。入口合約(Revest 合約)提供了三種不同的介面,透過鎖定底層資產來鑄造 FNFT:
mintTimeLock:底層資產將在一段時間後解鎖。mintValueLock:底層資產將在價值高於或低於規定值時解鎖。mintAddressLock:底層資產將由指定的帳戶解鎖。
Revest 合約連接了另外三個合約,用於鎖定和解鎖底層資產。
-
FNFTHandler:繼承自 ERC-1155 代幣。它為每次鎖定創建一個具有遞增
fnftId的新 FNFT。鎖定時會規定新 FNFT 的總供應量。FNFT 無法透過其他方式鑄造,但可以透過銷毀來解鎖底層資產。 -
LockManager:在創建鎖定時記錄其解鎖條件,並在請求解鎖時判斷該鎖定是否可以解鎖。
-
TokenVault:接收並發送底層資產,並為每個 FNFT 記錄元數據,例如指定 FNFT 的價值。
我們以 mintAddressLock 為例來說明鑄造 FNFT 的過程。


以上兩張圖描述了 FNFT 是如何創建、鑄造和銷毀的。
具體來說,用戶 A 將 100 WETH 鎖定到 Revest Finance 中,創建了 fnftId 為 1 的對應 FNFT。最後,它向指定的收件人鑄造了 100 個 1-FNFT,並分配了指定的份額。
請注意,一旦底層資產解鎖,每個 1-FNFT 都可以銷毀以換取 1 個(1e18)WETH。如圖 2 所示,用戶 B 通過銷毀 25 個 1-FNFT 提取了 25 個( 1e18)WETH。
此外,Revest 合約還提供另一個名為 depositAdditionalToFNFT 的介面,該介面引入了兩個將在下文討論的漏洞。
我們先使用下圖說明該函數的正常用法。


depositAdditionalToFNFT 函數將更多的底層資產鎖定到現有的鎖定中(由 fnftId 指定)。在合理的情況下(圖 3),它要求指定的數量與指定 FNFT 的總供應量相同,然後將添加的資產平均分配給每個指定的 FNFT。
在其他情況下(圖 4),它會創建一個帶有最新 fnftId 的新鎖定,銷毀指定數量的舊 FNFT 並鑄造相同數量的此新 FNFT,然後將新鎖定的 depositAmount 記錄為舊鎖定的 depositAmount 與指定金額之和,如下面代碼所示:
// 現在,我們轉移到代幣金庫
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 的價值從舊鎖定轉移到了新鎖定中。
(指定數量超過總供應量時將會回滾交易)
什麼是重入漏洞
在本部分,我們將說明重入攻擊是如何工作的,並討論其根本原因和修復方法。



以上三張圖基本描述了整個重入攻擊的過程。具體而言,攻擊者首先鎖定 0 個 RENA 代幣,鑄造了 2 個沒有價值的 1-FNFT。其次,攻擊者再次鎖定 0 個 RENA 代幣,但鑄造了 360,000 個 2-FNFT(當時也沒有價值)。在最後一步中,攻擊者通過 ERC-1155 代幣標準繼承的 FNFTHandler 回調機制,重入了 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) 之前,這可以避免攻擊。其次,由於系統不允許鑄造 0 個 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 或記錄新鎖定的該值。當 newFNFTId 為 0 時,它並不記錄新鎖定的 depositAmount,因為這是一個向現有鎖定添加額外資產的操作。
按常理,當 newFNFTId 不為 0 時,它應該只記錄新鎖定的 depositAmount,而不應更改舊鎖定的值。然而,代碼顯示它不僅記錄了新鎖定的 depositAmount,還更改了舊鎖定的值。
我們認為這是一個嚴重的**零時差(zero-day)**邏輯漏洞,並隨後編寫了 PoC 來驗證。以下三張圖描述了該 PoC 的工作原理。



具體而言,攻擊者首先鎖定 0 個 RENA 以鑄造 360,000 個 1-FNFT。隨後,攻擊者直接調用 depositAdditionalToFNFT 函數創建一個新的鎖定。由於該邏輯錯誤,TokenVault 合約錯誤地將舊鎖定的 depositAmount 從 0 更改為 1e18。結果,攻擊者獲得了價值 359,999 個 RENA 的 359,999 個 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)存儲了大量關鍵狀態,該項目無法在不遷移狀態的情況下重新部署這兩個合約。為了避免該漏洞受到進一步攻擊,項目方重新部署了一個精簡版的 Revest 合約,該版本禁用了更複雜的函數,以減少潛在攻擊者的攻擊面。在檢查過變通方案後,我們認為這個精簡版的 Revest 合約可以減輕本文提到的攻擊風險。
總結
保護 DeFi 項目並非易事。除了代碼審計外,我們認為社區應採取主動的方法來監測項目狀態,並在攻擊發生前就進行攔截。
關於 BlockSec
BlockSec 是一家開創性的區塊鏈安全公司,由一群全球傑出的安全專家於 2021 年創立。公司致力於增強新興 Web3 世界的安全性和可用性,以促進其大規模採用。為此,BlockSec 提供智能合約與 EVM 鏈安全審計服務、用於安全開發與主動攔截威脅的 Phalcon 平台、用於資金追蹤與調查的 MetaSleuth 平台,以及幫助開發者在加密世界中高效航行的 MetaDock 擴充功能。
迄今為止,公司已為 MetaMask、Uniswap Foundation、Compound、Forta 和 PancakeSwap 等超過 300 家知名客戶提供服務,並獲得包括經緯創投(Matrix Partners)、Vitalbridge Capital 和分佈式資本(Fenbushi Capital)在內的頂尖投資者兩輪數千萬美元的融資。



