Free Rider

Description

A new marketplace of Damn Valuable NFTs has been released! There’s been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.

The developers behind it have been notified the marketplace is vulnerable. All tokens can be taken. Yet they have absolutely no idea how to do it. So they’re offering a bounty of 45 ETH for whoever is willing to take the NFTs out and send them their way.

You’ve agreed to help. Although, you only have 0.1 ETH in balance. The devs just won’t reply to your messages asking for more.

If only you could get free ETH, at least for an instant.

TL;DR

buyMany() is a payable function and it calls _buyOne() in a for loop, which means we can pay 1 unit of money and get multiple units of NTFs. Another bug is token.ownerOf(tokenId)) is set to the buyer after token.transferFrom(), so the payment is sent to the buyer instead of the seller. In other words, we get paid by buying NFT, pretty nice deal isn't it 😎

Code Audit

In the Foundry setup we can see there exists a Uniswap V2 DVT/WETH pool:

        assertEq(uniswapV2Pair.token0(), address(dvt));
        assertEq(uniswapV2Pair.token1(), address(weth));
        assertGt(uniswapV2Pair.balanceOf(deployer), 0);

token0 and token1 may map to DVT/WETH or WETH/DVT depending on the DVD version you are working with, so be aware of this.

Looking at FreeRiderNFTMarketplace.sol, an obvious bug pops up:

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

_buyOne() handles price based msg.value and it is called in a loop, this is clearly a bug where attacker can pay 1 unit of money and get multiple uints of NFTs.

Another less obvious bug is that token.ownerOf(tokenId) is set to the buyer after the token.safeTransferFrom() call, therefore payable(token.ownerOf(tokenId)).sendValue(priceToPay) actually sends ETH to the buyer instead of the seller.

Combining the above two observations, we can come up with an attack:

  1. Flash swap 15 WETH from Uniswap V2 DVT/WETH pool and convert it to 15 ETH.

  2. Call buyMany() and buy all 6 NFTs. For each iteration, we "get paid" 15 ETH. In the end, we will have 90 ETH (15 * 6 - 15).

  3. Send all 6 NFTs to FreeRiderBuyer.

  4. Swap 15 ETH to 15 WETH in order to payback the flash swap.

Note that all the operations mentioned above should happen inside the flash swap callback uniswapV2Call(). Also don't forget to implement ERC721 callback onERC721Received() since the NFT marketplace sends NFTs via safeTransferFrom().

Building PoC

Last updated