NFT Dealers

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

Infinite Withdrawal/Double Claim in NFTDealers::collectUsdcFromSelling Function.

Author Revealed upon completion

The collectUsdFromSelling() function allows sellers to make an infinite withdrawal of their token till the contract's balance is drained.

Description

  • The collectUsdFromSelling() function allows sellers to withdraw proceeds from the contract. However, the function does not update any persistent state to indicate that these proceeds have been claimed or collected.

  • Additionally, the collateral associated with the NFT mint (collateralForMinting[tokenId]) is not cleared after the withdrawal.

  • Because of these two omissions, a seller can repeatedly call the function and withdraw the same funds multiple times. This results in an infinite withdrawal vulnerability, allowing an attacker to drain the contract’s token balance.

The function does not track whether the listed pay out has been collected. There is no such boolean value as 'bool collected in the listing struct-- and implemented whereby' that will indicate whether the pay out has been collected.
The listing is copied into memory and as such Any changes to listing will not be reflected in contract storage. This prevents the contract from safely updating listing state during payout.
he collateralForMinting mapping is not updated after collateral is returned to the seller, allowing the same collateral to be reused in multiple payouts.

Risk

Likelihood:

  • This attack happens when a malicious seller repeatedly invokes the withdrawal function and claim the same payout multiple times. This enables the malicious seller to drain the contract’s entire balance of USDC, potentially affecting funds belonging to other users.

    The exploit requires no special privileges and can be executed by any seller whose listing has become inactive.


Impact:

  • An attacker who owns a sold NFT listing can repeatedly call collectUsdcFromSelling() to withdraw the same payout multiple times.

    Potential consequences include:

    • Complete drainage of all USDC held by the contract

    • Theft of funds belonging to other users

    • Loss of the NFT marketplace solvency

    Since the attack can be executed by a legitimate seller, it does not require any special privileges.


Proof of Concept

Seller lists NFT
Buyer purchases NFT
Seller calls collectUsdcFromSelling()
Seller receives payout
Seller calls collectUsdcFromSelling() again
Contract sends payout again
Repeat until contract balance is drained
Let's assume the following parameters:
NFT sale price: 1000 USDC
Collateral locked at mint: 200 USDC
Marketplace fee: 50 USDC
Expected payout to the seller:
(100050) + 200 = 1150 USDC
Step 1 — NFT Sale
A buyer purchases the NFT and transfers 1000 USDC to the contract.
Step 2 — First Withdrawal
The seller calls collectUsdcFromSelling() and receives 1150 USDC.
Step 3 — Repeated Calls
Since no state variables were updated and the collateral remains stored, the seller can call the function again and receive another 1150 USDC.
Step 4 — Contract Drain
The malicious seller repeatedly calls the function until all USDC held by the contract is exhausted.

Recommended Mitigation

function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
Listing storage listing = s_listings[_listingId];
require(!listing.isActive, "Listing must be inactive");
+ add this code require(!listing.collected, "Funds already collected");
uint256 fees = _calculateFees(listing.price);
uint256 amountToSeller = listing.price - fees;
uint256 collateral = collateralForMinting[listing.tokenId];
+ add collateralForMinting[listing.tokenId] = 0;
+ add listing.collected = true;
totalFeesCollected += fees;
usdc.safeTransfer(msg.sender, amountToSeller + collateral);
}

Support

FAQs

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

Give us feedback!