WETH10

Idea

    function execute(address receiver, uint256 amount, bytes calldata data) external nonReentrant {
        uint256 prevBalance = address(this).balance;
        Address.functionCallWithValue(receiver, data, amount);

        require(address(this).balance >= prevBalance, "flash loan not returned");
    }

execute() is basically RCE but we only can do limited things with it. The weth pool has 10 ETH and 0 WETH at the beginning and there is no way to convert ETH to WETH then transferFrom() all the WETH to us.

    function withdrawAll() external nonReentrant {
        Address.sendValue(payable(msg.sender), balanceOf(msg.sender));
        // @audit-issue _burnAll() = _burn(msg.sender, balanceOf(msg.sender));
        // @audit-issue We can transfer WETH out in `fallback()` so that `balanceOf(msg.sender) == 0`
        // @audit-issue Therefore no WETH will be burnt
        _burnAll();
    }
    function _burnAll() internal {
        _burn(msg.sender, balanceOf(msg.sender));
    }

This is similar to a reentrancy attack (but not exact): we let sendValue() trigger the callback in our fallback/receive function and find a way to let balanceOf(msg.sender) == 0 in that stage. But how?

To achieve balanceOf(msg.sender) == 0, we have to transfer all WETH we have at the moment to somewhere else. Recall that we have RCE via execute(), which means we can get max allowance by calling approve(). With that privilege, we can "donate" WETH to weth pool during fallback/receive (so that balanceOf(msg.sender) == 0) and take the WETH back via transferFrom() since we have max allowance. In the next round, we deposit WETH and repeat above process until the weth pool is completely drained.

PoC

Last updated