Damn Vulnerable DeFi is an Ethereum smart contract wargame developed by @tinchoabbate from OpenZeppelin. The competition includes 8 unique challenges educating players about various DeFi vulnerabilities.
In this article, I will share basic set up steps to get you started on the challenges and go over the first challenge.
To begin playing the wargame, you have to set up your local environment first. Start by cloning the challenges repository from Github and installing Node dependencies:
% git clone https://github.com/OpenZeppelin/damn-vulnerable-defi.git
% cd damn-vulnerable-defi
% npm install
Once you install all of the dependencies you can test the environment by listing available challenges as follows:
If you run into issues with the Node set up try reinstalling it or updating packages by running npm update.
The wargame is designed to run completely on users’ local machines using OpenZeppelin Test Helpers. So for every challenge you will be running a respective *.challenge.js file which simulates contract deployment and attacker actions. For example, to run the first challenge, Unstoppable, you would execute test/unstoppable/unstoppable.challenge.js test file by running the following command:
% npm run unstoppable
Solutions will need to be added to the above challenge file to make it satisfy the required condition.
At this point, we should all of the necessary tools to begin playing the Damn Vulnerable DeFi!
SPOILER ALERT: The walkthroughs below include complete solutions to all of the challenges. You may want to stop reading here, if you would like to solve these yourself.
Challenge #1 — Unstoppable
The first challenge invites players to stop a DeFi lending contract from making any future loans:
There's a lending pool with a million DVT tokens in balance, offering flash loans for free.If only there was a way to attack and stop the pool from offering flash loans ...You start with 100 DVT tokens in balance.
Running the first challenge gives us the expected fail case that we will need to solve:
Let’s look at the test/unstoppable/unstoppable.challenge.js file to see where it fails:
Based on the code snippet above, the solution requires us to execute the executeFlashLoan() function in the ReceiverUnstoppable contract in such a way that it triggers a revert. The exact attack logic needs into the /** YOUR EXPLOIT GOES HERE */ section.
The function executeFlashLoan() performs a bare minimum owner check, before calling flashLoan() in the UnstoppableLender contract:
The flashLoan() function in-turn lends funds to the user while performing several balance checks:
The function above includes a particularly interesting assertion which requires variables poolBalance to be equal to balanceBefore. If we were to get those two functions to fall out of sync, then we would successfully cause this DeFi app to fail.
The variable balanceBefore is initialized as a local variable in the flashLoan() by querying the current token balance:
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
poolBalance is a global variable which is initialized at contract deployment time and gets updated every time a deposit is made using depositTokens():
The DeFi app makes a false assumption that the only way to send tokens to the pool contract is using the depositTokens() function. However, if we were to send tokens directly using standard ERC-20 transfer the two values should fall out of sync. Let’s edit the unstoppable.challenge.js file by adding the transfer call to the pool contract as follows:
And rerun the challenge:
Success! We have managed to break the the DeFi and complete the first challenge. While it is designed as a quick introduction into the wargame, funds locking bugs are common in the DeFi space. Only recently, Percent Finance permanently locked up $1M worth of crypto due to an error in their smart contract implementation.
I hope you enjoyed reading this walkthrough and inspired to continue solving future challenges.