forked from barryclark/jekyll-now
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add blog post Damn Vulnerable Defi Challenges barryclark#8
- Loading branch information
1 parent
4a38d9c
commit d49447f
Showing
1 changed file
with
123 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
--- | ||
layout: post | ||
title: Solving Damn Vulnerable Defi Challenges Series (V). Puppet | ||
excerpt_separator: <!--more--> | ||
--- | ||
|
||
Hello, coming back from some holidays! I had some time to continue solving Damn Vulnerable Defi Challenges. Today's post explains the way I approached and solved Challenge #8 - Puppet. Enjoy!. | ||
|
||
<!--more--> | ||
|
||
# Solving Puppet Challenge | ||
|
||
This challenge was harder to deploy in Brownie than solving it!, at least for me. It required to deploy an instance of [Uniswap](https://es.cointelegraph.com/explained/what-is-uniswap-and-how-does-it-work) V1 to work. I had to invest some time learning how to deploy it and also [learning how others done it](https://github.com/wuwe1/damn-vulnerable-defi-brownie/blob/ec115648cc319811ef09519c433bce9772753cd9/tests/test_puppet.py). | ||
|
||
In this challenge we are presented with the following statement: | ||
|
||
``` | ||
There's a huge lending pool borrowing Damn Valuable Tokens (DVTs), where you first need to deposit twice the borrow amount in ETH as collateral. The pool currently has 100000 DVTs in liquidity. | ||
There's a DVT market opened in an Uniswap v1 exchange, currently with 10 ETH and 10 DVT in liquidity. | ||
Starting with 25 ETH and 1000 DVTs in balance, you must steal all tokens from the lending pool. | ||
``` | ||
|
||
Based on the statement and looking at the [Pool's contract](https://github.com/tinchoabbate/damn-vulnerable-defi/blob/v2.0.0/contracts/puppet/PuppetPool.sol) I looked how the pool performed the most critical actions: | ||
|
||
## Borrowing function analysis | ||
|
||
First I reviewed how the borrowing function worked: | ||
|
||
``` | ||
// Allows borrowing `borrowAmount` of tokens by first depositing two times their value in ETH | ||
function borrow(uint256 borrowAmount) public payable nonReentrant { | ||
uint256 depositRequired = calculateDepositRequired(borrowAmount); | ||
require(msg.value >= depositRequired, "Not depositing enough collateral"); | ||
if (msg.value > depositRequired) { | ||
payable(msg.sender).sendValue(msg.value - depositRequired); | ||
} | ||
deposits[msg.sender] = deposits[msg.sender] + depositRequired; | ||
// Fails if the pool doesn't have enough tokens in liquidity | ||
require(token.transfer(msg.sender, borrowAmount), "Transfer failed"); | ||
emit Borrowed(msg.sender, depositRequired, borrowAmount); | ||
} | ||
``` | ||
|
||
As we can see it first calculates how much the user has to deposit as collateral via `calculateDepositRequired` (more on this later). | ||
|
||
Later, it checks that these amount was sent by the user via `msg.value`. | ||
|
||
If everything is OK, in tries to transfer the borrowed amount back to the user (it will fail if the pool does not have enough tokens). | ||
|
||
So nothing extrange here. Once I finished with this function I decided to take a look at `calculateDepositRequired` which is another critical function that defines how much the user needs to deposit!, let's take a look at it. | ||
|
||
## Calculating deposits required as collateral | ||
|
||
Function `calculateDepositRequired` is implemented like this: | ||
|
||
``` | ||
function calculateDepositRequired(uint256 amount) public view returns (uint256) { | ||
return amount * _computeOraclePrice() * 2 / 10 ** 18; | ||
} | ||
``` | ||
|
||
So, nothing complicated, I assumed that some price was returned from an Oracle, and took a look at the `_computeOraclePrice()` function: | ||
|
||
``` | ||
function _computeOraclePrice() private view returns (uint256) { | ||
// calculates the price of the token in wei according to Uniswap pair | ||
return uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair); | ||
} | ||
``` | ||
|
||
This is interesting!. The price returned by this function depends on two things: | ||
|
||
1. The balance of `uniswapPair`. | ||
2. The amount of DVT tokens that `uniswapPair` has. | ||
|
||
So it was key to understand what `uniswapPair` was. Looking at the contract's code we can see that this variable is passed upon initialization in the contract's constructor: | ||
|
||
``` | ||
constructor (address tokenAddress, address uniswapPairAddress) { | ||
token = DamnValuableToken(tokenAddress); | ||
uniswapPair = uniswapPairAddress; | ||
} | ||
``` | ||
|
||
Looking at the code in charge of deploying the contract we finally uderstand that this variable holds the address of the Uniswap Exchange: | ||
|
||
``` | ||
// Deploy the lending pool | ||
this.lendingPool = await PuppetPoolFactory.deploy( | ||
this.token.address, | ||
this.uniswapExchange.address | ||
); | ||
``` | ||
|
||
Now with all the parts in place, let's analyze the problem again: | ||
|
||
The goal for the attacker is to steal all the DVT tokens from the pool. The problem is that to borrow 100000 DVTs, and under normal circumnstances, *twice* the amount in ETH is needed to be deposited as collateral. The attacker only has 25 ETH. | ||
|
||
To be true the fact that twice the amount of ETH has to be deposited, `_computeOraclePrice()` must return `1`. This is true only if the relationship between `uniswapPair.balance` and `token.balanceOf(uniswapPair)` is 1:1. By default, the pool has 10 ETH and 10 DVT, and hence, the function returns 1. | ||
|
||
And here relies the key issue that we will exploit to solve the challenge! | ||
|
||
## Solution | ||
|
||
The attacker can manipulate this relationship. If the exchange's ETH balance (uniswapPair.balance) is 0 (or close), the result of the division `uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair)` will be 0 or a very small number. | ||
|
||
To perform this attack, an attacker could swap his DVT tokens for ETH, lowering the Exchange's ETH balance to a small number (or 0) and then borrow all the Pool's DVT tokens having to deposit as collateral nearly 0 ETH. | ||
|
||
I implemented this solution [here](https://github.com/nahueldsanchez/dvd_brownie/tree/master/puppet). I also ported this challenge to Brownie, you can find it [here](https://github.com/nahueldsanchez/dvd_brownie/) along with all the previous ones. | ||
|
||
Thanks for reading!. | ||
|
||
## Sources | ||
|
||
- https://es.cointelegraph.com/explained/what-is-uniswap-and-how-does-it-work | ||
- https://github.com/wuwe1/damn-vulnerable-defi-brownie |