pragma solidity ^0.8.30;
import { Test } from "forge-std/Test.sol";
import { NFTDealers } from "src/NFTDealers.sol";
import { MockUSDC } from "src/MockUSDC.sol";
contract C01_PoC is Test {
NFTDealers public nftDealers;
MockUSDC public usdc;
address owner = makeAddr("owner");
address seller = makeAddr("seller");
address buyer = makeAddr("buyer");
function setUp() public {
usdc = new MockUSDC();
nftDealers = new NFTDealers(
owner,
address(usdc),
"NFT Dealers",
"NFTD",
"ipfs://image",
20 * 1e6
);
usdc.mint(seller, 100_000 * 1e6);
usdc.mint(buyer, 100_000 * 1e6);
vm.deal(seller, 100 ether);
vm.deal(buyer, 100 ether);
}
function test_FeesNeverCollected() public {
vm.startPrank(owner);
nftDealers.revealCollection();
nftDealers.whitelistWallet(seller);
vm.stopPrank();
vm.startPrank(seller);
usdc.approve(address(nftDealers), 20 * 1e6);
nftDealers.mintNft{value: 20 * 1e6}();
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.list(1, 1000 * 1e6);
vm.stopPrank();
vm.startPrank(buyer);
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.buy(1);
vm.stopPrank();
vm.startPrank(seller);
nftDealers.collectUsdcFromSelling(1);
vm.stopPrank();
uint256 reportedFees = nftDealers.totalFeesCollected();
uint256 actualBalance = usdc.balanceOf(address(nftDealers));
console.log("Reported Fees:", reportedFees);
console.log("Actual Balance:", actualBalance);
vm.startPrank(owner);
nftDealers.withdrawFees();
uint256 ownerReceived = usdc.balanceOf(owner);
vm.stopPrank();
console.log("Owner Received:", ownerReceived);
assertGt(reportedFees, 0, "Fees should be tracked");
}
}
pragma solidity ^0.8.30;
* ============================================================
* POC-C01: Fees Are Never Collected - Protocol Loses All Revenue
* Protocol cannot collect any fees from secondary sales
* Severity : CRITICAL
* Contract : NFTDealers.sol
* Function : buy(), collectUsdcFromSelling()
* Author: Sudan249 AKA 0xAljzoli
* ============================================================
*
* VULNERABLE CODE:
*
*
* usdc.transferFrom(msg.sender, address(this), listing.price);
*
*
*
* usdc.safeTransfer(address(this), fees);
* usdc.safeTransfer(msg.sender, amountToSeller);
*
*
* IMPACT:
* - Protocol cannot collect any fees from secondary sales
* - totalFeesCollected variable tracks fees but funds are never isolated
* - Owner cannot withdraw fees because they're mixed with seller funds
* - If multiple sales occur, fee accounting becomes completely broken
* - Protocol revenue model is completely non-functional
*
* FIX:
* - Deduct fees at time of buy() and transfer to fee reserve
* - OR properly track and isolate fees in collectUsdcFromSelling()
* - Remove the nonsensical transfer to self in collectUsdcFromSelling()
*/
import { Test } from "forge-std/Test.sol";
import { console } from "forge-std/console.sol";
import "./AuditBase.sol";
contract POC_C01_FeesNeverCollected is AuditBase {
function test_C01_A_singleSale_feesNotWithdrawn() public {
vm.startPrank(owner);
nftDealers.revealCollection();
nftDealers.whitelistWallet(seller);
vm.stopPrank();
vm.startPrank(seller);
usdc.approve(address(nftDealers), lockAmount);
nftDealers.mintNft{value: lockAmount}();
uint256 tokenId = 1;
vm.stopPrank();
vm.startPrank(seller);
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.list(tokenId, 1000 * 1e6);
vm.stopPrank();
uint256 initialFees = nftDealers.totalFeesCollected();
uint256 initialContractBalance = usdc.balanceOf(address(nftDealers));
console.log("Initial totalFeesCollected:", initialFees);
console.log("Initial Contract USDC Balance:", initialContractBalance);
vm.startPrank(buyer);
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.buy(1);
vm.stopPrank();
uint256 contractBalanceAfterBuy = usdc.balanceOf(address(nftDealers));
uint256 feesAfterBuy = nftDealers.totalFeesCollected();
console.log("Contract USDC Balance After Buy:", contractBalanceAfterBuy);
console.log("totalFeesCollected After Buy:", feesAfterBuy);
vm.startPrank(seller);
nftDealers.collectUsdcFromSelling(1);
vm.stopPrank();
uint256 feesAvailableForWithdrawal = usdc.balanceOf(address(nftDealers));
uint256 reportedFees = nftDealers.totalFeesCollected();
console.log("Reported Fees (totalFeesCollected):", reportedFees);
console.log("Actual USDC Available in Contract:", feesAvailableForWithdrawal);
assertGt(reportedFees, 0, "Fees should have been collected");
vm.startPrank(owner);
uint256 ownerBalanceBefore = usdc.balanceOf(owner);
try nftDealers.withdrawFees() {
uint256 ownerBalanceAfter = usdc.balanceOf(owner);
console.log("Owner USDC Balance After Withdrawal:", ownerBalanceAfter);
if (ownerBalanceAfter - ownerBalanceBefore < reportedFees) {
console.log("VULNERABILITY CONFIRMED: Owner received less than reported fees");
}
} catch {
console.log("VULNERABILITY CONFIRMED: Withdrawal failed - fees not actually collected");
}
vm.stopPrank();
}
function test_C01_B_multipleSales_feeAccountingBroken() public {
vm.startPrank(owner);
nftDealers.revealCollection();
nftDealers.whitelistWallet(seller);
nftDealers.whitelistWallet(seller2);
vm.stopPrank();
vm.startPrank(seller);
usdc.approve(address(nftDealers), lockAmount);
nftDealers.mintNft{value: lockAmount}();
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.list(1, 1000 * 1e6);
vm.stopPrank();
vm.startPrank(seller2);
usdc.approve(address(nftDealers), lockAmount);
nftDealers.mintNft{value: lockAmount}();
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.list(2, 1000 * 1e6);
vm.stopPrank();
console.log("=== Two NFTs listed at 1000 USDC each ===");
console.log("Expected total fees: 20 USDC (10 + 10)");
vm.startPrank(buyer);
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.buy(1);
vm.stopPrank();
vm.startPrank(buyer2);
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.buy(2);
vm.stopPrank();
vm.startPrank(seller);
nftDealers.collectUsdcFromSelling(1);
vm.stopPrank();
vm.startPrank(seller2);
nftDealers.collectUsdcFromSelling(2);
vm.stopPrank();
uint256 reportedFees = nftDealers.totalFeesCollected();
uint256 actualBalance = usdc.balanceOf(address(nftDealers));
console.log("Reported Total Fees:", reportedFees);
console.log("Actual Contract Balance:", actualBalance);
if (reportedFees > actualBalance) {
console.log("VULNERABILITY CONFIRMED: Reported fees exceed actual balance");
console.log(" - Owner cannot withdraw full reported fees");
console.log(" - Fee accounting is completely broken");
}
assertEq(reportedFees, 20 * 1e6, "Expected 20 USDC in fees");
}
function test_C01_C_feeWithdrawalDrainsSellerFunds() public {
vm.startPrank(owner);
nftDealers.revealCollection();
nftDealers.whitelistWallet(seller);
vm.stopPrank();
vm.startPrank(seller);
usdc.approve(address(nftDealers), lockAmount);
nftDealers.mintNft{value: lockAmount}();
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.list(1, 1000 * 1e6);
vm.stopPrank();
vm.startPrank(buyer);
usdc.approve(address(nftDealers), 1000 * 1e6);
nftDealers.buy(1);
vm.stopPrank();
uint256 sellerBalanceBefore = usdc.balanceOf(seller);
uint256 expectedSellerProceeds = 990 * 1e6;
vm.startPrank(owner);
uint256 ownerBalanceBefore = usdc.balanceOf(owner);
try nftDealers.withdrawFees() {
console.log("Owner withdrew fees before seller collected");
} catch {
console.log("Owner withdrawal failed - insufficient isolated fees");
}
vm.stopPrank();
vm.startPrank(seller);
try nftDealers.collectUsdcFromSelling(1) {
uint256 sellerBalanceAfter = usdc.balanceOf(seller);
uint256 actualReceived = sellerBalanceAfter - sellerBalanceBefore;
console.log("Seller Expected:", expectedSellerProceeds);
console.log("Seller Actually Received:", actualReceived);
if (actualReceived < expectedSellerProceeds) {
console.log("VULNERABILITY CONFIRMED: Seller received less than expected");
console.log(" - Owner fee withdrawal drained seller funds");
}
} catch {
console.log("VULNERABILITY CONFIRMED: Seller collection failed after owner withdrawal");
}
vm.stopPrank();
}
}
- function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
- Listing memory listing = s_listings[_listingId];
- if (listing.isActive) revert ListingNotActive(_listingId);
-
- uint256 fees = _calculateFees(listing.price);
- uint256 amountToSeller = listing.price - fees;
- uint256 collateralToReturn = collateralForMinting[listing.tokenId];
-
- totalFeesCollected += fees;
- amountToSeller += collateralToReturn;
-
- IERC20(usdc).safeTransfer(address(this), fees);
- IERC20(usdc).safeTransfer(msg.sender, amountToSeller);
-
- collateralForMinting[listing.tokenId] = 0;
- }
+ function buy(uint256 _listingId) external payable {
+ Listing storage listing = s_listings[_listingId];
+ if (!listing.isActive) revert ListingNotActive(_listingId);
+ if (listing.seller == msg.sender) revert InvalidAddress();
+
+ uint256 fees = _calculateFees(listing.price);
+ uint256 amountToSeller = listing.price - fees;
+
+ totalFeesCollected += fees;
+
+ IERC20(usdc).safeTransferFrom(msg.sender, address(this), fees);
+ IERC20(usdc).safeTransferFrom(msg.sender, listing.seller, amountToSeller);
+
+ listing.isActive = false;
+ activeListingsCounter--;
+ collateralForMinting[listing.tokenId] = 0;
+
+ _safeTransfer(listing.seller, msg.sender, listing.tokenId, "");
+ emit NFT_Dealers_Sold(msg.sender, listing.price);
+ }