NFT Dealers

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

Repeated Seller Settlement Enables Multiple USDC Payouts

Author Revealed upon completion

Root + Impact

Description

  • The expected behavior is that a seller can settle a completed listing once, receiving salePrice - fee + lockedCollateral, while protocol fees remain in the contract for owner withdrawal.

  • The actual behavior allows repeated settlement for the same listing because there is no one-time settlement guard and collateral is not cleared in the settlement path. This enables multiple payouts from the same sale.

function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
Listing memory listing = s_listings[_listingId];
require(!listing.isActive, "Listing must be inactive to collect USDC");
uint256 fees = _calculateFees(listing.price);
uint256 amountToSeller = listing.price - fees;
uint256 collateralToReturn = collateralForMinting[listing.tokenId];
totalFeesCollected += fees;
amountToSeller += collateralToReturn;
usdc.safeTransfer(address(this), fees); // @> self-transfer does not isolate/lock fees
usdc.safeTransfer(msg.sender, amountToSeller); // @> seller can call repeatedly
// @> collateralForMinting[listing.tokenId] is not reset here
// @> no proceedsCollected state variable/check
}

Risk

Likelihood:

  • A sold listing naturally transitions to inactive, and seller-driven settlement is part of normal usage after each successful buy.

  • Repeated calls pass current validation because the function only checks inactive status and seller identity, both unchanged after first settlement.

Impact:

  • Seller can withdraw sale proceeds multiple times for one listing.

  • Contract USDC pool can be drained, breaking collateral/proceeds availability for honest users.

Proof of Concept

// Pseudocode / Foundry-style flow:
// 1) seller mints + lists tokenId=1 at 1000e6
// 2) buyer approves USDC and buys listing 1
// 3) seller calls collectUsdcFromSelling(1) -> succeeds (expected)
// 4) seller calls collectUsdcFromSelling(1) -> succeeds again (unexpected)
// 5) seller USDC balance increases on both calls
// Assertion idea:
// assertGt(balanceAfterSecondCollect - balanceAfterFirstCollect, 0);

Recommended Mitigation

function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
totalFeesCollected += fees;
amountToSeller += collateralToReturn;
- usdc.safeTransfer(address(this), fees);
usdc.safeTransfer(msg.sender, amountToSeller);
}

Support

FAQs

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

Give us feedback!