NFT Dealers

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

Canceled listings are collectible as sold listings, enabling payout without buyer payment

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: canceling a listing should deactivate the listing and only return the seller's own locked collateral.

  • Specific issue: buy and cancelListing both set isActive = false, while collectUsdcFromSelling only checks inactivity. A canceled listing is therefore treated as collectible sale proceeds, allowing payout with no buyer payment.

// Root cause in the codebase with @> marks to highlight the relevant section
function cancelListing(uint256 _listingId) external {
...
@> s_listings[_listingId].isActive = false;
...
}
function buy(uint256 _listingId) external payable {
...
@> s_listings[_listingId].isActive = false;
}
function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
Listing memory listing = s_listings[_listingId];
@> require(!listing.isActive, "Listing must be inactive to collect USDC");
uint256 amountToSeller = listing.price - fees;
@> usdc.safeTransfer(msg.sender, amountToSeller);
}

Risk

Likelihood:

  • Any whitelisted seller can execute list -> cancelListing -> collectUsdcFromSelling using their own listing lifecycle.

  • This behavior occurs whenever pooled USDC exists in the contract from mints or other market activity.

Impact:

  • Sellers can extract price - fee from shared protocol funds without any corresponding buyer payment.

  • totalFeesCollected can increase based on fake sales, corrupting accounting and owner fee withdrawals.

Proof of Concept

// Validated by test: testFinding02_collectAfterCancelWithoutBuyerPayment
nft.list(2, uint32(MIN_PRICE));
nft.cancelListing(2);
uint256 beforeCollect = usdc.balanceOf(seller);
nft.collectUsdcFromSelling(2);
uint256 afterCollect = usdc.balanceOf(seller);
assertEq(afterCollect - beforeCollect, 990_000); // no buyer paid this listing

Recommended Mitigation

+enum ListingStatus { NONE, ACTIVE, SOLD, CANCELED }
struct Listing {
...
- bool isActive;
+ ListingStatus status;
}
function buy(uint256 _listingId) external {
...
- s_listings[_listingId].isActive = false;
+ s_listings[_listingId].status = ListingStatus.SOLD;
}
function cancelListing(uint256 _listingId) external {
...
- s_listings[_listingId].isActive = false;
+ s_listings[_listingId].status = ListingStatus.CANCELED;
}
function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
- require(!listing.isActive, "Listing must be inactive to collect USDC");
+ require(listing.status == ListingStatus.SOLD, "Listing not sold");
}

Support

FAQs

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

Give us feedback!