NFT Dealers

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

`cancelListing` returns mint collateral, bypassing intended lock-until-sale economics

Author Revealed upon completion

Description

Root + Impact

Normal behavior: mint collateral should remain locked until valid sale settlement and should not be unlocked by canceling a listing.

Issue: cancelListing directly transfers collateralForMinting[tokenId] back to seller, allowing mint/list/cancel loops where users recover all locked funds without selling.

Description Explanation

The protocol's economic invariant is that mint collateral should remain locked until a valid sale lifecycle is completed. Returning collateral during cancellation breaks that invariant by turning lockup into a reversible step controlled entirely by seller action.

This means users can mint NFTs, list to appear compliant with flow, cancel instantly, and recover principal without any market execution. The design no longer enforces economic commitment, and collateral accounting loses its protective function.

function cancelListing(uint256 _listingId) external {
Listing memory listing = s_listings[_listingId];
if (!listing.isActive) revert ListingNotActive(_listingId);
require(listing.seller == msg.sender, "Only seller can cancel listing");
s_listings[_listingId].isActive = false;
activeListingsCounter--;
// @> Collateral is unlocked by cancellation, not sale completion
usdc.safeTransfer(listing.seller, collateralForMinting[listing.tokenId]);
collateralForMinting[listing.tokenId] = 0;
}

Risk

Likelihood:

  • Seller-controlled call path; no special privileges required.

  • Repeats for each minted token and executes immediately.

Impact:

  • Economic lock mechanism is neutralized.

  • Enables chained abuse with fake settlement withdrawals.

Proof of Concept

  1. Seller balance is recorded before mint/list.

  2. Seller mints and lists, which should lock collateral in contract.

  3. Seller cancels listing.

  4. Post-cancel balance equals pre-mint balance, proving collateral was fully refunded without sale.

  5. collateralForMinting(tokenId) becomes zero, confirming lock was removed by cancellation.

// test/NFTDealersFindingsPoC.t.sol
function testPoC_CancelListingReturnsMintCollateral() public {
uint256 balanceBefore = usdc.balanceOf(sellerA);
_mintAndList(sellerA, 1, PRICE_A);
vm.prank(sellerA);
nftDealers.cancelListing(1);
uint256 balanceAfter = usdc.balanceOf(sellerA);
assertEq(balanceAfter, balanceBefore);
assertEq(nftDealers.collateralForMinting(1), 0);
}

Recommended Mitigation

Cancellation should only deactivate listing availability, not settle economics. Keeping collateral locked during cancel preserves intended game theory and ensures collateral release happens only inside a verified sold-settlement path.

This also reduces exploit composition with payout bugs, because attackers cannot pre-unlock capital before attempting invalid claims.

function cancelListing(uint256 _listingId) external {
...
s_listings[_listingId].isActive = false;
activeListingsCounter--;
-
- usdc.safeTransfer(listing.seller, collateralForMinting[listing.tokenId]);
- collateralForMinting[listing.tokenId] = 0;
+
+ // Keep collateral locked on cancellation; only release in sold settlement flow.
}

Support

FAQs

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

Give us feedback!