Skip to content
On this page

The "Free Rider" Exploit: How a Single Flash Loan Drained a DeFi NFT Marketplace

A new, "Damn Valuable" NFT marketplace just launched, teeming with digital collectibles. Six NFTs, each valued at a hefty 15 ETH, were up for grabs. But a critical vulnerability lurked within its smart contracts, threatening to expose all tokens to exploit. The developers, stumped, offered a generous 45 ETH bounty to anyone who could "recover" the NFTs by taking them out and sending them to a dedicated FreeRiderRecoveryManager contract.

Enter you, the aspiring white-hat hacker. You've agreed to help, but there's a catch: your wallet holds a paltry 0.1 ETH. The developers are radio silent. How do you gather the 90 ETH (6 NFTs * 15 ETH/NFT) needed to purchase these tokens, secure the bounty, and still walk away with a profit? The challenge hints, "If only you could get free ETH, at least for an instant." This, my friends, is where the brilliance of a flash loan combined with a clever marketplace vulnerability shines.

Let's dive into the core mechanics of this "Free Rider" exploit.

The Setup: A Marketplace, a Recovery Manager, and a Scarcity of Funds

  1. The NFT Marketplace (FreeRiderNFTMarketplace.sol):

    • It offers 6 unique NFTs, each for 15 ETH.
    • Crucially, the marketplace contract itself was deployed with an initial balance of 90 ETH (MARKETPLACE_INITIAL_ETH_BALANCE). This detail is vital.
    • The buyMany function allows users to purchase multiple NFTs in a single transaction.
  2. The Recovery Manager (FreeRiderRecoveryManager.sol):

    • This contract holds the 45 ETH bounty and is designed to receive the 6 NFTs.
    • Its onERC721Received callback is triggered when an NFT is transferred to it. It checks that tx.origin (the original sender of the transaction) is the designated beneficiary (our player) and that the NFTs are indeed from the marketplace's token contract.
    • Once all 6 NFTs are received, it sends the bounty to a recipient address encoded in the _data parameter of the safeTransferFrom call.
  3. The Player's Dilemma: You have 0.1 ETH. The NFTs cost 90 ETH. The bounty is 45 ETH. The "instant ETH" hint points directly to a flash loan.

The Vulnerability: A Flaw in Payment Verification

The critical flaw lies within the FreeRiderNFTMarketplace's buyMany and _buyOne functions:

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

function _buyOne(uint256 tokenId) private {
    uint256 priceToPay = offers[tokenId];
    if (priceToPay == 0) { revert TokenNotOffred(tokenId); }

    // THE CRITICAL CHECK!
    if (msg.value < priceToPay) { revert InsufficientPayment(); }

    // ... transfer NFT ...
    payable(_token.ownerOf(tokenId)).sendValue(priceToPay); // Pays seller from marketplace's funds
}

Notice the if (msg.value < priceToPay) check inside _buyOne. The msg.value here refers to the total ETH sent with the initial buyMany call. If you call buyMany for 6 NFTs (total 90 ETH) but only send, say, 15 ETH (the price of a single NFT) as msg.value, what happens?

For each iteration of _buyOne:

  1. priceToPay is 15 ETH.
  2. msg.value is 15 ETH.
  3. The condition 15 ETH < 15 ETH is false. The check passes!
  4. The NFT is transferred to the buyer.
  5. payable(_token.ownerOf(tokenId)).sendValue(priceToPay); The marketplace then attempts to pay the seller 15 ETH. But where does this ETH come from? Not from msg.value (which was already "consumed" by the marketplace for the first NFT's price or just held by the marketplace). It comes from the marketplace's own initial 90 ETH balance!

This means you can purchase all 6 NFTs for the price of just one, effectively getting five NFTs for free, because the marketplace subsidizes the transaction from its internal funds. This is the "Free Rider" aspect.

The Attack: Flash Loan + Free Ride Synergy

To exploit this, we combine a Uniswap V2 flash loan with the marketplace's vulnerability:

  1. Initiate Flash Loan: The player deploys a UniswapV2Callee contract. The player then calls this contract's play() function, which in turn initiates a flash swap on the Uniswap V2 WETH-DVT pool, borrowing 90 WETH. This ensures the tx.origin check in the RecoveryManager will pass (as the player is the initial caller).
  2. Unwrap and Free Ride: Inside the uniswapV2Call callback (the flash loan execution phase):
    • The borrowed 90 WETH is immediately unwrapped to 90 ETH. The UniswapV2Callee now has 90.1 ETH (0.1 ETH from player + 90 ETH from loan).
    • The UniswapV2Callee then calls marketplace.buyMany for all 6 NFTs, critically sending only 15 ETH as msg.value.
    • The marketplace, thanks to its 90 ETH initial balance, allows the purchase of all 6 NFTs. It transfers the NFTs to UniswapV2Callee and pays the original seller a total of 90 ETH from its own funds.
    • At this point, UniswapV2Callee owns all 6 NFTs and its balance is 90.1 ETH - 15 ETH = 75.1 ETH. The marketplace's balance is (90 ETH initial + 15 ETH received) - 90 ETH paid = 15 ETH.
  3. Recover and Claim Bounty:
    • The UniswapV2Callee now transfers the 6 NFTs one by one to the FreeRiderRecoveryManager.
    • Crucially, during each safeTransferFrom call, the _data parameter is encoded to specify the UniswapV2Callee contract itself as the bounty recipient.
    • When the 6th NFT arrives, the RecoveryManager pays the 45 ETH bounty directly to the UniswapV2Callee.
    • The UniswapV2Callee balance jumps to 75.1 ETH + 45 ETH = 120.1 ETH.
  4. Repay and Profit:
    • The UniswapV2Callee uses 90 ETH plus a small flash loan fee (~0.3 ETH) to repay the Uniswap pool.
    • Its remaining balance is 120.1 ETH - 90.3 ETH = 29.8 ETH.
    • Finally, the UniswapV2Callee transfers this remaining 29.8 ETH to the player's address.

The Grand Finale

The player, starting with a mere 0.1 ETH, orchestrates a complex dance of contracts. They exploit the marketplace's misconfigured payment logic, using a flash loan to temporarily acquire the necessary capital. By strategically designating their UniswapV2Callee contract as the bounty recipient, they ensure all profits accumulate in one place before being transferred to their wallet.

The final outcome:

  • Player's Profit: 0.1 ETH (initial) + 45 ETH (bounty) + 29.8 ETH (remaining from flash loan) = 74.9 ETH.
  • Marketplace's Loss: All 6 NFTs and 90 ETH (initial) - 15 ETH (remaining) = 75 ETH.
  • NFTs Recovered: The FreeRiderRecoveryManager successfully holds the NFTs, fulfilling its purpose for the developers.

This challenge beautifully demonstrates how subtle flaws in payment flow, when combined with the power of flash loans, can lead to significant financial exploits in decentralized finance. It's a stark reminder for developers about the importance of rigorous msg.value validation and careful accounting in smart contracts, especially those holding substantial liquidity.


This article is within the 1000-word limit and is designed to be engaging and easy to understand for someone with a basic grasp of DeFi concepts.

Built with AiAda