NFT Dealers

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

Re-Listing Overwrites Previous Seller's Uncollected Sale Proceeds : Permanent Fund Loss

Author Revealed upon completion

Root + Impact

Re-Listing Overwrites Previous Seller's Uncollected Sale Proceeds : Permanent Fund Loss

Description

  • After a sale, the original seller's address, price, and collateral reference are stored in s_listings[tokenId]. The seller is expected to call collectUsdcFromSelling to claim their proceeds.

list() uses _tokenId as the storage key and unconditionally overwrites all fields in s_listings[_tokenId]. When the new owner re-lists the same token before the previous seller collects, the original seller's data is destroyed and the onlySeller modifier blocks them permanently.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • This occurs during normal marketplace usage any time a buyer re-lists a purchased NFT before the previous seller collects, no malicious intent is required

On-chain transaction ordering means even a few blocks of delay between sale and collection creates a window for the overwrite

Impact:

  • Previous seller permanently loses their entire sale proceeds (price - fees) and minting collateral

The onlySeller modifier reverts for the original seller since the stored seller address now belongs to the new lister

  • No recovery mechanism exists — the funds remain trapped in the contract with no way to route them to the rightful owner

Proof of Concept

function test_NM003_RelistOverwritesUncollectedProceeds() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
// Alice mints tokenId=1, lists at 1000 USDC
_mintAndDepositCollateral(alice, 1);
vm.prank(alice);
nftDealers.list(1, 1000e6);
// Bob buys Alice's NFT
deal(address(usdc), bob, 1000e6);
vm.startPrank(bob);
usdc.approve(address(nftDealers), 1000e6);
nftDealers.buy(1);
// Bob re-lists BEFORE Alice collects — overwrites her sale data
nft.approve(address(nftDealers), 1);
nftDealers.list(1, 500e6);
vm.stopPrank();
// Alice tries to collect her ~1010 USDC — reverts
vm.prank(alice);
vm.expectRevert("Only seller can call this function");
nftDealers.collectUsdcFromSelling(1);
// Alice's funds are permanently locked
}

Recommended Mitigation

function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {
+ require(collateralForMinting[_tokenId] == 0, "Previous sale uncollected");
+ listingsCounter++;
- s_listings[_tokenId] = Listing({
+ s_listings[listingsCounter] = Listing({
seller: msg.sender,
price: _price,
nft: address(nft),
tokenId: _tokenId,
isActive: true
});
- listingsCounter++;
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!