6. Selfie

https://www.damnvulnerabledefi.xyz/challenges/selfie/

Objective

  • A lending pool offering flash loans of DVT tokens.

  • Includes a fancy governance mechanism to control it.

  • You start with no DVT tokens in balance, and the pool has 1.5 million.

  • Your goal is to take them all.

Approach

SelfiePool has a drainAllFunds function that can only be called by the SimpleGovernance contract.

Let us examine if we can subvert the governance contract for our purposes.

SimpleGovernance

Governance is based on the DVT token:

Voting power is based on the proportion of number of tokens held against half of total supply

This means that if msg.caller has sufficient DVT tokens such that its balance > halfTotalSupply, msg.caller has sufficient voting power and therefore can queue a governance action successfully.

Solution

  1. Attacker to take the largest flash loan from SelfiePool possible,

  2. Using voting power from flash loan to pass a governance action via SimpleGovernance using queueAction and executeAction

    1. call drainAllFunds with beneficiary as attacker address

queueAction and executeAction

queueAction takes in an address, calldata, and weiAmount as parameters. These fields are stored in a struct

  • address: target contract address to take action on ("receiver")

  • calldata: abi encoded function signature:

bytes memory data = abi.encodeWithSignature(
            "approve(address,uint256)", address(this), type(uint256).max
        );
(bool success, bytes memory data) = target.call{value: 111, gas: 5000}(data)ol
  • weiAmount: this is the value field for target.call{value: value}(data)

    • basically how much ether to send over

executeAction

  • external, payable

  • Anyone can call executeAction, passing an actionId

  • only constraints are:

    • action has not been executed yet

      • actionToExecute.executedAt == 0

    • action is ready to be executed (delay has been exceeded)

      • (block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS)

The core of executeAction is

actionToExecute.receiver.functionCallWithValue(actionToExecute.data, actionToExecute.weiAmount);
  • functionCallWithValue is from OpenZepplin's Address library:

Fundamentally, it is a low-level call, where value is the weiAmount and data is the data we passed earlier in queueAction as parameters.

Solution Code

  1. Initiate flashloan with borrow()

  2. selfiePool contract will callback receivetokens; this will lead to execution of the attack.

    1. queueAction: drainAllfunds

    2. return borrowed tokens

  3. executeAction can be called by anyone after the defined delay has passed. Therefore we do not need to execute this via contract.

  4. DamnValuableTokenSnapshot(token).snapshot();
    1. this is needed because queueAction calls _hasEnoughVoteswhich calculates addresses balances based on snapshots.

Note

The attack component must be shoeboxed into the callback function instead of the initial borrow function.

Execution of flashloan() only completes after the tokens are returned. This means that the following will not work:

    function borrow(uint256 amount) external {
        require(msg.sender == owner, "only owner");
        selfiePool.flashLoan(amount);

        bytes memory data = abi.encodeWithSignature("drainAllFunds(address)", address(owner));
        drainActionId = governance.queueAction(address(selfiePool), data, 0);

    }

Since the flashloan will be settled before the attack code is executed -> without the benefit of borrowed tokens.

Last updated