Challenge 1: Unstoppable, Damn vulnerable defi V4 lazy solutions series

Siddharth PatelSiddharth Patel
4 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.

V4 Intro

Read more comments and details about the changes in the full release announcement:

Releasing Damn Vulnerable DeFi V4


Problem statement:

Our aim is to halt the vault. If we do think little bit then it can be possible if we somehow manage to make smart contract transaction fail every time, It could be lets say finding out and making that one condition fail every time👀.

There are 2 smart contracts mainly,
UnstoppableVault.sol and UnstoppableMonitor.sol

Let's focus on main functionality, flashLoan of UnstoppableVault

    function flashLoan(IERC3156FlashBorrower receiver, address _token, uint256 amount, bytes calldata data)
        external
        returns (bool)
    {
        if (amount == 0) revert InvalidAmount(0); // fail early
        if (address(asset) != _token) revert UnsupportedCurrency(); // enforce ERC3156 requirement
        uint256 balanceBefore = totalAssets();
        if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement

        // transfer tokens out + execute callback on receiver
        ERC20(_token).safeTransfer(address(receiver), amount);

        // callback must return magic value, otherwise assume it failed
        uint256 fee = flashFee(_token, amount);
        if (
            receiver.onFlashLoan(msg.sender, address(asset), amount, fee, data)
                != keccak256("IERC3156FlashBorrower.onFlashLoan")
        ) {
            revert CallbackFailed();
        }

        // pull amount + fee from receiver, then pay the fee to the recipient
        ERC20(_token).safeTransferFrom(address(receiver), address(this), amount + fee);
        ERC20(_token).safeTransfer(feeRecipient, fee);

        return true;
    }

There are 4 require conditions,

  1. flashloan amount should not be equal to zero.

  2. asked token should be always token for which vault is built for.

  3. receiver.onFlashLoan should always return keccak256("IERC3156FlashBorrower.onFlashLoan")

  4. Also in between one weird condition is,

    convertToShares(totalSupply) != balanceBefore()

Let's focus on this,

uint256 balanceBefore = totalAssets();
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement

Look at convertToShares and totalAssets function for reference,

function convertToShares(uint256 assets) public view virtual returns (uint256) {
    uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
    return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets());
}
function totalAssets() public view override nonReadReentrant returns (uint256) {
    return asset.balanceOf(address(this));
}

what's this?

This code is meant to enforce the ERC4626 requirement, which is a standard proposed for tokenized vaults in DeFi. The aim of ERC4626 is to create a standardized approach for handling user deposits and shares within a vault, ultimately determining the rewards for staked tokens.

Understanding ERC4626 and the Accounting System

In the context of this challenge, there are two fundamental terms to comprehend: assets and shares.

  • The assets represent the underlying token, in this case, DVT that users deposit and withdraw from the vault.

  • Shares, on the other hand, symbolize the vault tokens, denoted as tDVT minted or burned for users based on the proportion of their deposited assets.

The challenge leverages the convertToShares() function as per ERC4626. This function calculates the number of shares the vault should mint, taking the user’s deposited assets into account.

vulnerability

Enforcing Equal TotalSupply and TotalAssets

The line (convertToShares(totalSupply) != balanceBefore) enforces a condition that requires the totalSupply of vault tokens to always match the totalAssets of underlying tokens before any flash loan execution. This condition acts as a safeguard to ensure that the flashLoan function remains inactive if the vault deviates assets to other contracts.

Tracking Assets and the TotalAssets Function

The totalAssets function has been overridden to return the vault contract's asset balance (asset.balanceOf(address(this))). This introduces an alternative accounting system that relies on monitoring the supply of vault tokens.


The Attack Strategy

The attack strategy revolves around creating a conflict between the two distinct accounting systems. This is achieved by manually transferring DVT tokens directly to the vault.

This manipulation disrupts the balance and leads to a scenario where the condition (convertToShares(totalSupply) != balanceBefore) fails.

Consequently, the flashLoan function is deactivated due to the divergence in accounting systems, since the transaction will always be reverted without dependance on the “user” input.

Solution code:

Unstoppable.t.sol

    function test_unstoppable() public checkSolvedByPlayer {
        require(token.transfer(address(vault), 1));   
    }

Test it,

forge test --mp test/unstoppable/Unstoppable.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.