Reentrancy possible in buy() leading to a loss of NFT for the seller without payment.
https://github.com/CodeHawks-Contests/2026-03-NFT-dealers/blob/main/src/NFTDealers.sol#L141-L155
When the buyer is a contract, _safeTransfer() from ERC721 will trigger a callback function to check if the contract is able to handle NFTs, which is : onERC721Received().
-> This onERC721Received() can trigger a reentrancy on buy() to ask for a second (or multiple) NFTs. Therefore, only the last one will set s_listings[_listingId].isActive = false;. Leaving the previous NFTs at an active state. Yet, activeListingsCounter will still be decremented.
More details about the Reentrancy in the buy() function :
1st : Leading to a mismatch between activeListingsCounter and the actual NFTs being still active.
2nd : There could be 1 or mutliple NFTs actually sold, but still being registered as active in the listing. Which results in :
Impact --> The seller will not be able to collect usdc from the selling, they will lose their NFT and never get paid.
Plus they will lose the collateral. The seller could call cancelListing() afterward and only get back the collateral, but not the NFT. The problem is why would they do that if they are not even aware that their NFT has been sold ?!
If this user checks their wallet they could notice but if they only rely on the Logs they would miss it because there is a flaw in the Log events.
Problem concerning the Logs:
emit NFT_Dealers_Sold(msg.sender, listing.price); will only show the Log for the last NFT sold in the loop.
So tracking real sellings from outside of the blockchain using Logs would be compromised.
Side note:
https://github.com/CodeHawks-Contests/2026-03-NFT-dealers/blob/main/src/NFTDealers.sol#L181
-> usdc.safeTransfer(address(this), fees); this could be avoided to save gas.
Likelihood: Medium
The reentrancy is easy to do, it only needs a smart contract with a callback function onERC721Received() that triggers buy() multiple times when it receives a NFT.
Impact: High
Loss of funds for the seller (they will never get the funds from the selling)
Loss of their NFT
Add a nonReentrant modifier for the buy() function.
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.