Normal behavior dictates that users must lock a 20 USDC collateral within the protocol to mint an NFT.
The specific issue is that the cancelListing function refunds this collateral to the seller but does not require the NFT to be burned or transferred back to the protocol, allowing users to keep both the NFT and their initial USDC.
Solidity
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");
// @> Collateral is refunded, but the NFT remains in the seller's wallet
usdc.safeTransfer(listing.seller, collateralForMinting[listing.tokenId]);
collateralForMinting[listing.tokenId] = 0;
Whitelisted users who mint an NFT can immediately list it and call cancelListing in the same or subsequent transaction.
No complex preconditions or specific states are required to execute this action.
The protocol suffers a complete loss of its intended economic design, as the 20 USDC collateral per NFT is drained.
A malicious actor can loop this process to mint the entire MAX_SUPPLY of the collection for absolutely zero net cost, stealing the protocol's backing collateral.
Solidity
function test_PoC_FreeNFTAndCollateralTheft() public {
uint256 initialUsdc = usdc.balanceOf(attacker);
Canceling a secondary market listing should only change the listing status to inactive. It should not refund the minting collateral, since the user still retains ownership of the NFT.
Diff
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");
}
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.