NFT Dealers

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

Protocol Fees Are Not Accrued at Purchase Time (Deferred Fee Accounting by Seller Action)

Author Revealed upon completion

Description:
In buy, full price is transferred to the contract, but protocol fees are not accrued there. Fees are only reflected when seller later calls collectUsdcFromSelling.

Impact:
Medium. Fee realization is delayed and dependent on seller behavior, weakening protocol revenue accounting and operational visibility.

Proof of Concept:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.34;
import {Test, console} from "forge-std/Test.sol";
import {NFTDealers} from "../../src/NFTDealers.sol";
import {MockUSDC} from "../../src/MockUSDC.sol";
contract ExploitFeesNotCollectedTest is Test {
NFTDealers public nftDealers;
MockUSDC public usdc;
string internal constant BASE_IMAGE = "https://images.unsplash.com/photo-1541781774459-bb2af2f05b55";
address public owner = makeAddr("owner");
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
uint256 constant LOCK_AMOUNT = 20e6;
uint256 constant NFT_PRICE = 1000e6;
function setUp() public {
usdc = new MockUSDC();
nftDealers = new NFTDealers(owner, address(usdc), "NFTDealers", "NFTD", BASE_IMAGE, LOCK_AMOUNT);
usdc.mint(alice, 5000e6);
usdc.mint(bob, 5000e6);
vm.prank(owner);
nftDealers.revealCollection();
vm.prank(owner);
nftDealers.whitelistWallet(alice);
vm.prank(owner);
nftDealers.whitelistWallet(bob);
}
function _mintNft(address user) internal {
vm.startBroadcast(user);
usdc.approve(address(nftDealers), LOCK_AMOUNT);
nftDealers.mintNft();
vm.stopBroadcast();
}
function _listNft(address user, uint256 tokenId, uint256 price) internal {
vm.startBroadcast(user);
nftDealers.list(tokenId, uint32(price));
vm.stopBroadcast();
}
function testExploit_FeesNotCollectedOnPurchase() public {
_mintNft(alice);
_listNft(alice, 1, NFT_PRICE);
vm.startBroadcast(bob);
usdc.approve(address(nftDealers), NFT_PRICE);
nftDealers.buy(1);
vm.stopBroadcast();
assertEq(nftDealers.totalFeesCollected(), 0, "fees are not accrued on buy");
vm.prank(alice);
nftDealers.collectUsdcFromSelling(1);
assertGt(nftDealers.totalFeesCollected(), 0, "fees are accrued only on seller collection");
}
}

Recommended Mitigation:
Accrue protocol fees at sale finalization (buy) or implement explicit payable balances where protocol fees are accounted independently from seller-triggered collection.

Support

FAQs

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

Give us feedback!