2022年3月27日、Ethereum上のステーキングDeFiプロジェクトであるRevest Financeが、ERC-1155のコールバック 機構を悪用した攻撃を受け、約200万 USD相当のトークン(BLOCKS、ECO、LYXe、RENA)が盗まれました。私たちはいち早くこの攻撃を分析し、その夜(UTC+8)に分析結果をツイートしました。
実は、そのツイートを書いた時点で、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が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という別のインターフェースを提供しており、これが以下で説明する2つの脆弱性の原因となっています。
まず、次の2つの図を使って、この関数の通常の使い方を説明します。


関数depositAdditionalToFNFTは、fnftIdで指定された既存のロックに原資産を追加します。指定数量が指定されたFNFTの総供給量と等しい場合(図3)、追加された資産は各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の1個当たりで引き出せる原資産の量を示します。そのため、この操作では、指定数量の古いFNFTの価値が古いロックから新しいロックに移転します。
(指定数量が総供給量を超えると、トランザクションはリバートされます)
リエントランシー脆弱性とは
このセクションでは、リエントランシー攻撃の仕組みを説明し、根本原因と修正方法を検討します。



上の3つの図は、リエントランシー攻撃の一連の流れをおおむね示しています。具体的には、攻撃者はまず0 RENAトークンをロックし、価値のない1-FNFTを2個ミントします。次に、再び0 RENAトークンをロックし、この時点では同じく価値のない2-FNFTを360,000個ミントします。最後のステップでは、攻撃者はFNFTHandlerがERC-1155トークン規格から継承したコールバック機構を通じて、RevestコントラクトのdepositAdditionalToFNFT関数に再入します。これにより、fnftIdが更新される前に、fnftIdが2であるロックのdepositAmountが上書きされます。その結果、攻撃者はdepositAmountが1e18である2-FNFTを360,001個取得します。つまり、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の繰り返しミントを許可しないため、想定どおりに動作することを確保する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を変更するか、新しいロックのそれを記録します。newFNFTIdがゼロの場合、これは既存のロックに資産を追加する操作なので、新しいロックのdepositAmountは記録しません。
通常であれば、newFNFTIdがゼロでない場合には、新しいロックのdepositAmountだけを記録し、古いロックのdepositAmountは変更しないはずです。しかし、コードでは新しいロックのdepositAmountを記録するだけでなく、古いロックのdepositAmountも変更しています。
私たちはこれを深刻なゼロデイのロジック上の脆弱性と考え、検証のためにPoCを作成しました。次の3つの図は、PoCの動作を示しています。



具体的には、攻撃者はまず0 RENAをロックし、1-FNFTを360,000個ミントします。その後、関数depositAdditionalToFNFTを直接呼び出して新しいロックを作成します。ロジック上のバグにより、TokenVaultコントラクトは古いロックのdepositAmountを誤ってゼロから1e18に変更します。その結果、攻撃者は359,999 RENA相当の1-FNFTを359,999個取得します。この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には重要な状態が多数保存されているため、プロジェクトは状態を移行せずに両コントラクトを再デプロイすることはできません。この脆弱性を利用したさらなる攻撃を避けるため、プロジェクトはRevestコントラクトの軽量版を再デプロイしました。この軽量版では、攻撃を企てる者が利用できる攻撃対象領域を減らすため、より複雑な機能を無効にしています。回避策を確認した結果、軽量版のRevestコントラクトは、このブログで述べた攻撃の可能性を緩和できると考えています。
まとめ
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



