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.
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
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);
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");
}