Fee Self-Transfer Renders Protocol Fee Collection Insolvent
Description
-
The expected behavior is that protocol fees are retained in the contract when a seller settles a completed sale, tracked via totalFeesCollected, and later withdrawn by the owner through withdrawFees().
-
The issue is that collectUsdcFromSelling() executes usdc.safeTransfer(address(this), fees) — a transfer from the contract to itself — which is a no-op. totalFeesCollected is incremented as though fees were segregated, but no USDC is actually set aside. When the owner calls withdrawFees(), the payout draws from other users' locked collateral and uncollected sale proceeds, making the contract progressively insolvent.
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:
Proof of Concept
function testFeeInsolvency() public revealed {
vm.prank(owner);
nftDealers.whitelistWallet(userWithEvenMoreCash);
vm.startPrank(userWithCash);
usdc.approve(address(nftDealers), 20e6);
nftDealers.mintNft();
nftDealers.list(1, 1000e6);
vm.stopPrank();
vm.startPrank(userWithEvenMoreCash);
usdc.approve(address(nftDealers), type(uint256).max);
nftDealers.mintNft();
nftDealers.buy(1);
vm.stopPrank();
vm.prank(userWithCash);
nftDealers.collectUsdcFromSelling(1);
vm.prank(owner);
nftDealers.withdrawFees();
assertEq(usdc.balanceOf(address(nftDealers)), 20e6);
}
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];
totalFeesCollected += fees;
amountToSeller += collateralToReturn;
+ collateralForMinting[listing.tokenId] = 0;
+ delete s_listings[_listingId];
- usdc.safeTransfer(address(this), fees);
usdc.safeTransfer(msg.sender, amountToSeller);
}