Damn Vulnerable DeFi 6 - 10 Solutions

In this post, I will share my explanations for DamnVulnerableDeFI challenges 6 through 10.

You can find the code here.

Let’s begin.

Selfie

As per usual, this challenge involves a pool providing flash loans of 1.5 million DVT tokens. Our goal is to take them all.

Fortunately, we can immediately see that the pool has a drainAllFunds(...) function, which sends all pool DVT tokens to the address passed as a parameter. Unfortunately for us, it can only be called by governance.

SelfiePool is the first flash loan provider that has a governance mechanism attached to it, which, as stated in the challenge description, will be our way in. Looking into it we see that the SimpleGovernance contract allows for actions to be queued, provided they have enough votes. _hasEnoughVotes() checks that the msg.sender DVT balance is higher than half of the pool token balance.

SelfiePool also allows for an action to be executed, provided it has never been executed before and two days have passed since being queued. During a given action execution, the action.receiver will be called using the action.data and action.weiAmount as the call function parameters.

While looking through the SimpleGovernance contract, you'll hopefully have noticed that there are practically no access control mechanisms in place. This means that, as long as we can bypass queueAction(...) and executeAction(...) verifications, we are able to queue an action that will call the SelfiePool drainAllFunds(...) function and send them to us. It will work because the call is coming from governance.

To bypass queueAction(...) we need to have more than 750k DVT tokens at the time of the last snapshot. This is new - the pool implements an ERC20Snapshot version of the DVT token. Fortunately for us, anyone can take a DVT snapshot.

However, we are only interested in taking a snapshot when we do have enough DVT tokens, which we can get by implementing a contract that gets a SelfiePool flash loan.

All that's left is finding a way to bypass executeAction(...), which is easily done by waiting two days!

So, one way to solve this challenge is to:

  1. Implement a contract that is able to get a flash loan from the SelfiePool contract;

  2. After receiving the flash loan, take a snapshot and queue an action with:

    • data to make a call to the drainAllFunds(...) with our address as a parameter;

    • receiver as the SelfiePool address;

    • weiAmount must be 0, unless you funded the attacker contract;

  3. Pay the flash loan back;

  4. Wait two days;

  5. Execute the action.

Compromised

To me, the hardest challenge so far. I had to use some guessing to get this to work. I'll explain my rationale below.

The challenge setup is simple: We have an Exchange selling and buying DVNTF tokens at a median price obtained from averaging 4 TrustfulOracle price feeds. The goal is simple as well: Drain Exchange using only our 0.1 ETH.

Looking at the contracts, I did not notice any obvious exploit. The only possible way to drain the Exchange contract that I found consisted of manipulating the DVNFT price. To that effect, the challenge description came in handy. This is where the guessing started.

Since it is called "Compromised" I assumed the leaked information from the HTTP response would contain some private keys.

I looked at the leaked data and decided to convert it to a string. Unfortunately, that did not result in a private key. Eventually, it occurred to me that the string I obtained may very well be encoded, as it is an HTTP response. I used google and found out that often HTTP response data is base64 encoded. So I converted the leaked data string using base64. At last, something that resembles a private key. I created wallets using the decoded leaked data and was happy to see that they corresponded to two trusted oracle addresses.

After that, it was just a question of manipulating the price using the compromised oracles.

Here's every step:

  1. Decode the leaked data and obtain two private keys;

  2. Create 2 wallets using the private keys obtained;

  3. Set the price to an affordable value. I used 0.005 ETH;

  4. Buy one DVNFT using the attacker account;

  5. Set the price to equal Exchange ETH balance;

  6. Sell the attacker DVNFT;

  7. Reset the price to the original value;

Puppet

This challenge is quite easy.

There is a PuppetPool that allows users to borrow DVT tokens, as long as the user deposits double that amount in ETH as collateral. The pool has a balance of 100000 DVT. Our goal is to get our hands on all of them.

The pool uses an Uniswap V1 DVT/ETH pair to calculate the collateral needed. This pair starts with 10 ETH / 10 DVT liquidity.

Lastly, our attacker starts with a balance of 25 ETH and 1000 DVT.

Since there are no flash loans, our best option is to somehow lower the collateral needed to borrow the 100000 DVT tokens from the PuppetPool.

Looking at how the price is calculated we see the following equation amount * _computeOraclePrice() * 2 / 10 ** 18;.

Looking at the _computeOraclePrice() we see that the equation used is uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair);. This is problematic, because if the Uniswap V1 exchange has a sufficiently larger balance of DVT tokens than ETH, the price to borrow tokens will decrease significantly.

Luckily for us, our attacker has 1000 DVT tokens, so if we deposit these to the Uniswap V1 exchange, our 25 ETH, plus whatever ETH we receive from depositing to the Uniswap V1 exchange, will be more than enough to borrow the PuppetPool 100000 DVT tokens.

The only detail we must not forget is that to pass the challenge the attacker DVT balance must be higher than the initial 100000 DVT PuppetPool balance. This means we cannot deposit all of the attackers 1000 DVT tokens to the Uniswap V1 exchange.

To recap:

  1. As the attacker, deposit an amount of DVT tokens large enough to lower the borrowing price to acceptable levels but less than 1000;

  2. Determine the amount of ETH needed as collateral to borrow the PuppetPool 100000 DVT tokens;

  3. Borrow them.

It's this simple.

Puppet V2

The set up of this challenge is very similar to the previous one.

The major differences are:

  1. The PuppetV2Pool now uses an Uniswap V2 exchange to calculate the price;

  2. WETH is used instead of ETH;

  3. We are now required to deposit triple the borrow amount in WETH as collateral;

Despite the challenge description mentioning that developers learned from the original Puppet implementation, and in fact this is a more secure implementation, the same problem remains.

The Uniswap V2 pair used to calculate the borrowing price still has low liquidity, meaning that users with sufficient amounts of either of the pair tokens can affect significant price swings.

Luckily for us, our attacker has a significant amount of DVT tokens, meaning we can swing the price in our favor once again.

So, the steps to solve this challenge are similar to the previous one:

  1. Swap the attacker DVT tokens for ETH using the Uniswap V2 pair;

  2. Convert the attacker ETH to WETH;

  3. Borrow all PuppetV2Pool DVT tokens;

Free Rider

The set up of this challenge consists of a FreeRiderNFTMarketplace that is selling 6 NFTs for 15 ETH each. We also have a FreeRiderBuyer which told us that the marketplace is exploitable, and if we exploit it and send them the 6 NFTs, it will pay us 45 ETH. This is our goal.

The challenge description mentions that we start with 0.5 ETH - which is clearly not enough to buy the 6 NFTs. It also implies that there's a way to get some more ETH. If we look at the script that sets up the challenge, we can see that there's an Uniswap V2 WETH/DVT pair.

First things first, we need to find the exploit in the FreeRiderNFTMarketplace. Looking at the contract we can see that we can only call the buyMany(...) function to buy NFTs. This function calls the _buyOne(...), which is the function that actually handles the purchase. The exploit must be here.

Luckily for us, it is easy to find what's wrong with this function - it is the way that it checks the price we have to pay. Suppose we call the buyMany(...) function with the ids of the 6 NFTs, the require(msg.value >= priceToPay, "Amount paid is not enough") check is wrong because it is only checking the price of one NFT, not the sum of all 6. This means that for the price of one NFT, 15 ETH, we can buy all six!

As a bonus, there's also another bug. It sends the 15 ETH to token.ownerOf(tokenId), the owner of the NFT. However, before doing so it transfers the NFT to the buyer - meaning that not only is 15 ETH enough to buy all 6 NFTs, we actually get the 90 ETH we were supposed to pay, because we are the new owner.

All that's left is figuring out how to get the 15 ETH we need. For that, you'll need to look into the Uniswap V2 documentation. This is the difficult part of the challenge.

You'll hopefully find that we can take a flashswap from the WETH/DVT pair, which we will be able to payback easily due to our expected profit from the attack.

We will need to perform this attack using a contract, not only because we need to implement a uniswapV2Call(...) function to receive the flashswap, but also because we need an onERC721Received to receive the NFTs.

Here's the full attack:

  1. Get a flashswap of 15 WETH;

  2. Unwrap that WETH;

  3. Buy the 6 NFTs;

  4. Send them to the buyer;

  5. Pay the flashswap back;

  6. Send the all profits to the attacker account;