2022年3月27日、Ethereum上のステーキングDeFiプロジェクトRevest Financeが、ERC-1155のコールバックメカニズムにより攻撃を受け、約200万ドル相当のトークン(BLOCKS、ECO、LYXe、RENA)が盗難されました。私たちはまず、この攻撃を分析し、その夜(UTC+8)に分析結果をツイートしました。
実際、Twitterに投稿した時点では、Revest TokenVaultコントラクト内の関数について、まだいくつかの疑問がありました。その機能性を理解するために、コントラクトを調査しました。その後、これはよりはるかに簡単な方法で悪用可能で、(発生した攻撃と)同様の巨額の損失を引き起こす可能性のある、別のクリティカルなゼロデイ脆弱性**であることが判明しました。
その後、すぐにRevest Financeチームに連絡し、彼らは迅速に対応し、脆弱性に対する回避策を提案しました。脆弱性がトリガーされないことを確認した後、このブログを公開することにしました。
このブログの続きは、Revest Financeのメカニズム、元のリエントランシー攻撃、そして新しいゼロデイ脆弱性の3つの部分で構成されます。
Revest Finance FNFTとは
Revest FinanceのFinancial Non-Fungible Token(FNFT)は、ロックされた資産の将来の権利のトラストレスな移転を可能にします。エントリーコントラクト(Revestコントラクト)は、基盤となる資産をロックしてFNFTをミントするための3つの異なるインターフェースを提供します。
mintTimeLock:基盤となる資産は、一定期間後にアンロックされます。mintValueLock:基盤となる資産は、その価値が所定の値を超えたり下回ったりしたときにアンロックされます。mintAddressLock:基盤となる資産は、所定のアカウントによってアンロックされます。
Revestコントラクトは、他の3つのコントラクトを接続して、基盤となる資産をロックおよびアンロックします。
-
FNFTHandler:ERC-1155トークンから継承しています。ロックごとにインクリメントする
fnftIdを持つ新しいFNFTを作成します。ロックは、作成時に新しいFNFTの総供給量を規定します。FNFTは他の方法ではミントできませんが、基盤となる資産をアンロックするためにバーンすることができます。 -
LockManager:作成時に各ロックのアンロック条件を記録し、アンロック時にロックがアンロック可能かどうかを判断します。
-
TokenVault:基盤となる資産を受け取り、送信し、各FNFTのメタデータ(指定されたFNFTの値など)を記録します。
FNFTのミントプロセスを説明するために、mintAddressLockを例に取ります。


上記の2つの図は、FNFTがどのように作成、ミント、バーンされるかを説明しています。
具体的には、ユーザーAがRevest Financeに100 WETHをロックし、fnftIdを1とする対応するFNFTを作成します。最後に、指定された受領者に指定されたシェアで100の1-FNFTをミントします。
基盤となる資産がアンロックされると、1-FNFTあたり1 (*1e18) WETHを受け取るためにバーンできることに注意してください。図2に示すように、ユーザーBは25の1-FNFTをバーンして25 (* 1e18) WETHを引き出します。
さらに、RevestコントラクトはdepositAdditionalToFNFTという別のインターフェースを提供しており、これは以下の2つの脆弱性を引き起こします。
まず、この関数の通常の利用方法を説明するために、以下の2つの図を使用します。


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は、1つの指定されたFNFTが引き出すことができる基盤となる資産の量を示すため、その操作は古いロックの指定された数量の価値を古いロックから新しいロックに転送します。
(指定数量が総供給量を超える場合は、トランザクションはロールバックされます)
リエントランシー脆弱性とは
このパートでは、リエントランシー攻撃がどのように機能するかを説明し、根本原因と修正方法について議論します。



上記の3つの図は、リエントランシー攻撃の全体的なプロセスを基本的に説明しています。具体的には、攻撃者はまずゼロのRENAトークンをロックして、価値のない2つの1-FNFTをミントします。次に、攻撃者は再びゼロのRENAトークンをロックしますが、価値のない(現時点では)360,000の2-FNFTをミントします。最後のステップで、攻撃者は、ERC-1155トークン標準から継承されたFNFTHandlerのコールバックメカニズムを介してRevestコントラクトのdepositAdditionalToFNFT関数に再入力し、fnftIdが更新される前に、fnftIdが2のロックのdepositAmountを上書きします。その結果、攻撃者はdepositAmountが1e18の360,001の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の繰り返しミントを許可しないため、システムが期待どおりに機能することを保証するために2つのチェックを追加し、システムの安全性を向上させることができます。
新しいゼロデイ脆弱性
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も変更すると述べています。
これは深刻なゼロデイロジック脆弱性であると we believed され、それを検証するためにPoCを作成しました。以下の3つの図は、PoCがどのように機能するかを説明しています。



具体的には、攻撃者はまずゼロのRENAをロックして360,000の1-FNFTをミントします。その後、攻撃者はdepositAdditionalToFNFT関数を直接呼び出して新しいロックを作成します。ロジックバグにより、TokenVaultコントラクトは古いロックのdepositAmountをゼロから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);
}
}
2つの脆弱なコントラクト(TokenVaultとFNFTHandler)は多くの重要な状態を格納しているため、プロジェクトは状態の移行なしにTokenVaultコントラクトとFNFTHandlerコントラクトを再デプロイできません。この脆弱性へのさらなる攻撃を回避するために、プロジェクトはLiteバージョンのRevestコントラクトを再デプロイし、より複雑な関数を無効にして、潜在的な攻撃者のための表面積を減らしました。回避策を確認した後、Lite Revestコントラクトは、このブログで言及されている可能性のある攻撃を緩和できると we believe しています。
まとめ
DeFiプロジェクトを安全にすることは簡単な仕事ではありません。コード監査に加えて、コミュニティはプロジェクトのステータスを積極的に監視し、攻撃が発生する前にブロックするべきだと考えています。
BlockSecについて
BlockSecは、世界的に著名なセキュリティ専門家グループによって2021年に設立された、先駆的なブロックチェーンセキュリティ企業です。同社は、新興のWeb3の世界のセキュリティとユーザビリティの向上に専念しており、その普及を促進しています。この目的のために、BlockSecはスマートコントラクトとEVMチェーンのセキュリティ監査サービス、セキュリティ開発と脅威のプロアクティブなブロックのためのPhalconプラットフォーム、資金追跡と調査のためのMetaSleuthプラットフォーム、そしてWeb3ビルダーが仮想通貨の世界を効率的にサーフィンするためのMetaDock拡張機能を提供しています。
これまでに、同社はMetaMask、Uniswap Foundation、Compound、Forta、PancakeSwapなど300以上の著名なクライアントにサービスを提供し、Matrix Partners、Vitalbridge Capital、Fenbushi Capitalなどの著名な投資家から2回の資金調達で数千万米ドルを獲得しています。
公式ウェブサイト:https://blocksec.com/
公式Twitterアカウント:https://twitter.com/BlockSecTeam



