# Damn Vulnerable DeFi #1: Unstoppable Solution

By [tanh](https://paragraph.com/@tanh) · 2023-03-28

---

Introduction
============

Hey there, I’m creating a series of my solutions to the Damn Vulnerable DeFi challenges here starting with challenge #1 “Unstoppable”. This challenge looks at a flash loan implementation that contains a vulnerability. But first, what is a flash loan and how is it implemented?

Flash Loans and ERC3156
=======================

Flash Loans were first introduced by Aave as part of Aave V3 which you can learn more about [here](https://docs.aave.com/developers/guides/flash-loans). They provide a way for users to borrow crypto without needing to put down any collateral first. The catch is that the entire loan must be paid back within the same transaction or the entire transaction will fail. This provides a way for users to make high leverage trades without any upfront capital.

ERC3156 was developed as a standardized interface for interacting with Flash Loan contracts. The standard consists of two contracts, the lender and borrower (`IERC3156FlashLender` and `IERC3156FlashBorrower`). You can read more about the standard [here](https://eips.ethereum.org/EIPS/eip-3156#specification). At a high level, these interfaces provide the following functions

    interface IERC3156FlashLender {
    
        /**
         * @dev The amount of currency available to be lent.
         * @param token The loan currency.
         * @return The amount of `token` that can be borrowed.
         */
        function maxFlashLoan(
            address token
        ) external view returns (uint256);
    
        /**
         * @dev The fee to be charged for a given loan.
         * @param token The loan currency.
         * @param amount The amount of tokens lent.
         * @return The amount of `token` to be charged for the loan, on top of the returned principal.
         */
        function flashFee(
            address token,
            uint256 amount
        ) external view returns (uint256);
    
        /**
         * @dev Initiate a flash loan.
         * @param receiver The receiver of the tokens in the loan, and the receiver of the callback.
         * @param token The loan currency.
         * @param amount The amount of tokens lent.
         * @param data Arbitrary data structure, intended to contain user-defined parameters.
         */
        function flashLoan(
            IERC3156FlashBorrower receiver,
            address token,
            uint256 amount,
            bytes calldata data
        ) external returns (bool);
    }
    

    interface IERC3156FlashBorrower {
    
        /**
         * @dev Receive a flash loan.
         * @param initiator The initiator of the loan.
         * @param token The loan currency.
         * @param amount The amount of tokens lent.
         * @param fee The additional amount of tokens to repay.
         * @param data Arbitrary data structure, intended to contain user-defined parameters.
         * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan"
         */
        function onFlashLoan(
            address initiator,
            address token,
            uint256 amount,
            uint256 fee,
            bytes calldata data
        ) external returns (bytes32);
    }
    

The borrower contract can inherit `IERC3156FlashBorrower` by implementing the `onFlashLoan` contract. After this, the borrower can call the `flashLoan` function with the amount and token requested from the lender contract. On the lender side, `flashLoan` is implemented to send those tokens to the receiver, call the `onFlashLoan` callback function and then pull the tokens plus a fee back from the borrower contract. The code execution flow looks like this:

![Flash Loan execution flow](https://storage.googleapis.com/papyrus_images/190f6ee58aaaee1bf2e35947041b7728d6ca1854d578a3d7a33407bc25be680f.png)

Flash Loan execution flow

The borrower can implement any logic that they want within the `onFlashLoan` function, but the transaction will only be successful if they approve the loaned tokens plus a fee to the lender at the end. The next thing to understand about this challenge is ERC4626 vaults

Vaults and ERC4626
==================

ERC4626 was developed to standardized tokenized yield bearing vaults. Simply put, this vault allows users to hold a share of some underlying asset that is controlled in a vault. For example, consider a vault holding 100USDC. Alice could hold 69% of the vault and Bob could hold 20% of the vault. This gives Alice access to 69USDC from the vault and Bob 20USDC from the vault. This is a simple example, but the idea can easily be extended to more complex vaults such as [Yearn](https://yearn.finance/vaults), which generate yield on the assets in the vault. The yield can be distributed to users based on how much stake they have.

Contract overview
=================

Alright, with that background out of the way, we can take a look at the vulnerable [contract](https://github.com/tinchoabbate/damn-vulnerable-defi/blob/33120e97e9dbe8c297496a99549e6408a3944e95/contracts/unstoppable/UnstoppableVault.sol). At a high level, this contract is an `ERC4626` that supports lending using `ERC3156`. There is some grace period logic that allows users to lend funds without paying any fees within the first 30 days.

The `maxFlashLoan` function returns 0 if the asset is not supported and returns the `totalAssets` in the vault otherwise. The `flashFee` function has the logic for returning 0 if within the grace period (and the user is not attempting to borrow all the funds in the vault) or 5% of the borrowed amount otherwise. `setFeeRecipient` is gated to allowing only the contract owner to call it and sets a new recipient for all fees.

The logic in `totalAssets` looks a bit scary, so I’ll break it down

    function totalAssets() public view override returns (uint256) {
        assembly { // better safe than sorry
            if eq(sload(0), 2) {
                mstore(0x00, 0xed3ba6a6)
                revert(0x1c, 0x04)
            }
        }
        return asset.balanceOf(address(this));
    }
    

First, we check if the value in the first storage slot is 0. To find this storage slot, we need to check the first line of the contract

    contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626 {
    

Storage is ordered by the contracts that are inherited first by the order that they are inherited. Since `IERC3156FlashLender` has no storage, we check `ReentrancyGuard`. The first storage item here is the `locked` flag which ensures that reentrancy does not happen. If the value is 2 (the function has been entered), then this will revert. To be honest, I’m not sure why this check is done since none of the calls that use `totalAssets` has the `nonReentrant` modifier. Secondly this could be implemented by using the `nonReentrant` modifier directly on `totalAssets` and probably use less gas on the deployment but 🤷.

Finally, we get to the meat of this contract - the `flashLoan` function. In here, we check a bunch of conditions transfer the requested funds to the caller, call the callback and get the funds back plus a fee.

The hack
========

The exploit we are looking for is a way to block all users from being able to use the vault to make flash loans. Looking closely at the code in `flashLoan` there is this line:

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

This struck out to me because there is an assumption that the total number of tokens in the vault will always be equal to the total number of tokens owned by the vault. If we can somehow make the vault receive assets (update `totalAssets`) without updating the `totalSupply`, then this contract will become bricked and will always revert until this was somehow resolved. Digging into the `ERC4626` and `ERC20`, we see that `totalSupply` is only updated during a `mint` or `burn` event. The `mint` is called by the deployer who has 100% of the shares in the vault.

However, transferring tokens does not affect `totalSupply`. Thus, we can simply transfer some tokens from our user to the vault and all further loans will revert.

    it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        await token.connect(player).transfer(vault.address, 1n * 10n ** 18n);
    });
    

![Test passes](https://storage.googleapis.com/papyrus_images/f4a8c518fe8ef0b8a6bbf511d7fec6a9e478d96640c5915909fdc33de4e6cd81.png)

Test passes

For more details, we can `console.log` the `balanceBefore` and `convertToShares(totalSupply)` values after doing our token transfer. We see the following:

    totalSupply 1000000000000000000000000
    convertToShares(totalSupply) 999999000000999999000000
    balanceBefore 1000001000000000000000000
    

Since the balance has gone up, but the total supply of shares in the vault remains the same, `convertToShares` returns a value smaller than `balanceBefore`.

Resolution
==========

An exploit like this can be resolved by blocking transfers of the token into the vault directly. These direct transfers break the internal accounting of ERC4626.

Credits
=======

Credit to Jason Dent for the cover photo

[https://unsplash.com/photos/3wPJxh-piRw?utm\_source=unsplash&utm\_medium=referral&utm\_content=creditShareLink](https://unsplash.com/photos/3wPJxh-piRw?utm_source=unsplash&utm_medium=referral&utm_content=creditShareLink)

---

*Originally published on [tanh](https://paragraph.com/@tanh/damn-vulnerable-defi-1-unstoppable-solution)*
