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.
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
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.
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.
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.
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)
.
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.
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.