Skip to content
On this page

閃光ローンで狙え! 埋蔵金「WETH」を回収せよ! – Naive Receiver チャレンジ解説

もしあなたが、ブロックチェーンの世界に眠る財宝「WETH」を、大胆かつスマートに回収する方法を探しているなら、この「Naive Receiver」チャレンジはあなたにぴったりです。これは、Damn Vulnerable DeFi v4 の CTF(Capture The Flag)問題の一つで、巧妙に隠された脆弱性を突いて、プールとユーザーコントラクトに眠る全てのWETHを、指定された回収アカウントへ移動させるという、スリリングなミッションです。

チャレンジの舞台裏:

このチャレンジの核心は、NaiveReceiverPool というフラッシュローンを提供するコントラクトにあります。このプールは、1000 WETH を元手に、1 WETH の固定手数料でフラッシュローンを提供しています。さらに、BasicForwarder というコントラクトとの連携により、メタトランザクション(ガス代を別の主体に肩代わりしてもらう仕組み)にも対応しています。

一方、ユーザー(あなた)は、10 WETH を保有するサンプルコントラクト FlashLoanReceiver をデプロイしています。このコントラクトは、フラッシュローンを実行できる能力を持っています。

危険信号:全ての資金がリスクに!

この状況は、一見すると正常に見えますが、実は潜んだ脆弱性があります。チャレンジの目的は、この脆弱性を突いて、プールとユーザーコントラクトに眠る全てのWETHを、指定された「回収アカウント」へ無事に送り届けることです。

攻撃の鍵:BasicForwarderFlashLoanReceiver の連携

このチャレンジを解く鍵は、BasicForwarder がどのように NaiveReceiverPool のフラッシュローン機能と連携しているかを理解することにあります。

  1. NaiveReceiverPoolflashLoan 関数: この関数は、指定された receiver(フラッシュローンを受け取るコントラクト)に WETH を転送し、そのコントラクトの onFlashLoan 関数を呼び出します。重要なのは、onFlashLoan 関数が成功した後に、receiver からローン額と手数料を徴収する処理です。

  2. FlashLoanReceiveronFlashLoan 関数: この関数は、フラッシュローンを受け取った際に実行されるコールバック関数です。注目すべきは、この関数内部で _executeActionDuringFlashLoan() が呼び出されている点です。しかし、この _executeActionDuringFlashLoan() 関数は空っぽです! つまり、フラッシュローンを受け取った際に、本来行うべき「ローンの返済」や「何らかのアクション」が、このコントラクトでは実装されていません。

  3. BasicForwarder とメタトランザクション:BasicForwarder は、外部から署名されたトランザクションを受け付け、指定されたコントラクトの関数を実行する機能を持っています。これにより、ユーザーは自らのコントラクトから直接トランザクションを発行するのではなく、BasicForwarder を介して、あたかも他のアカウント(この場合は deployer)からトランザクションが発行されたかのように見せかけることができます。

脆弱性の発見と攻略法:

この構造から、以下のような脆弱性が浮かび上がります。

  • FlashLoanReceiveronFlashLoan 関数は、フラッシュローンを受け取った後、ローンの返済処理を実際には行っていません
  • NaiveReceiverPool は、onFlashLoan のコールバックが成功したとみなすと、FlashLoanReceiver から WETH を引き出そうとします。
  • しかし、FlashLoanReceiver が実際には NaiveReceiverPool に WETH を返還しないため、NaiveReceiverPoolFlashLoanReceiver から WETH を徴収できず、結果として NaiveReceiverPool から WETH が流出します。
  • さらに、BasicForwarder を利用することで、この WETH の流出操作を、あたかも deployer が指示したかのように偽装できます。

具体的な攻撃手順(CTF解答コードより抜粋):

  1. フラッシュローンを繰り返し実行:pool.multicall(calls); の部分では、NaiveReceiverPool に対して、FlashLoanReceiverreceiver として指定し、flashLoan 関数を複数回呼び出します。これにより、FlashLoanReceiver は繰り返し WETH を受け取りますが、返済処理を行わないため、プールから大量の WETH が FlashLoanReceiver に転送されます。

  2. 全資産の引き出しと署名: 次に、NaiveReceiverPoolwithdraw 関数を呼び出すためのデータ (withdrawData) を作成します。この際、引き出す量はプールと FlashLoanReceiver に存在する全ての WETH を合算した額にします。 そして、BasicForwarderRequest 構造体を構築し、withdrawDatadata フィールドに含めます。fromdeployer に設定し、targetpool に、deadline は現実的な未来の時間に設定します。 vm.sign(deployerPk, digest); により、deployer の秘密鍵でこのリクエストに署名します。

  3. メタトランザクションによる実行: 最後に、forwarder.execute{value: 0}(req, sig); を実行します。これにより、BasicForwarder は署名を検証し、deployer が指示したかのように pool.withdraw 関数を実行します。この時、FlashLoanReceiver から徴収されなかった WETH が、最終的に指定された recovery アドレスに移動します。

まとめ:

「Naive Receiver」チャレンジは、フラッシュローンの仕組み、メタトランザクションの応用、そしてコントラクト間の連携における見落としがちな脆弱性を巧みに突いた問題です。このチャレンジをクリアすることで、あなたはブロックチェーンの複雑な相互作用を理解し、より安全なスマートコントラクト開発に活かせる貴重な洞察を得られるでしょう。

このスリリングな冒険に、ぜひ挑戦してみてください!

Built with AiAda