Appearance
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
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
buyManyfunction allows users to purchase multiple NFTs in a single transaction.
The Recovery Manager (
FreeRiderRecoveryManager.sol):- This contract holds the 45 ETH bounty and is designed to receive the 6 NFTs.
- Its
onERC721Receivedcallback is triggered when an NFT is transferred to it. It checks thattx.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
_dataparameter of thesafeTransferFromcall.
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:
priceToPayis 15 ETH.msg.valueis 15 ETH.- The condition
15 ETH < 15 ETHis false. The check passes! - The NFT is transferred to the buyer.
payable(_token.ownerOf(tokenId)).sendValue(priceToPay);The marketplace then attempts to pay the seller 15 ETH. But where does this ETH come from? Not frommsg.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:
- Initiate Flash Loan: The player deploys a
UniswapV2Calleecontract. The player then calls this contract'splay()function, which in turn initiates a flash swap on the Uniswap V2 WETH-DVT pool, borrowing 90 WETH. This ensures thetx.origincheck in theRecoveryManagerwill pass (as the player is the initial caller). - Unwrap and Free Ride: Inside the
uniswapV2Callcallback (the flash loan execution phase):- The borrowed 90 WETH is immediately unwrapped to 90 ETH. The
UniswapV2Calleenow has 90.1 ETH (0.1 ETH from player + 90 ETH from loan). - The
UniswapV2Calleethen callsmarketplace.buyManyfor all 6 NFTs, critically sending only 15 ETH asmsg.value. - The marketplace, thanks to its
90 ETHinitial balance, allows the purchase of all 6 NFTs. It transfers the NFTs toUniswapV2Calleeand pays the original seller a total of 90 ETH from its own funds. - At this point,
UniswapV2Calleeowns all 6 NFTs and its balance is90.1 ETH - 15 ETH = 75.1 ETH. The marketplace's balance is(90 ETH initial + 15 ETH received) - 90 ETH paid = 15 ETH.
- The borrowed 90 WETH is immediately unwrapped to 90 ETH. The
- Recover and Claim Bounty:
- The
UniswapV2Calleenow transfers the 6 NFTs one by one to theFreeRiderRecoveryManager. - Crucially, during each
safeTransferFromcall, the_dataparameter is encoded to specify theUniswapV2Calleecontract itself as the bounty recipient. - When the 6th NFT arrives, the
RecoveryManagerpays the 45 ETH bounty directly to theUniswapV2Callee. - The
UniswapV2Calleebalance jumps to75.1 ETH + 45 ETH = 120.1 ETH.
- The
- Repay and Profit:
- The
UniswapV2Calleeuses 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
UniswapV2Calleetransfers this remaining29.8 ETHto the player's address.
- The
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
FreeRiderRecoveryManagersuccessfully 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.