function mintNft() external payable onlyWhenRevealed onlyWhitelisted {
...
require(usdc.transferFrom(msg.sender, address(this), lockAmount), "USDC transfer failed");
tokenIdCounter++;
collateralForMinting[tokenIdCounter] = lockAmount;
_safeMint(msg.sender, tokenIdCounter);
}
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);
}
function cancelListing(uint256 _listingId) external {
...
usdc.safeTransfer(listing.seller, collateralForMint
function testCollateralForMintingNotResetAfterCollectUsdcFromSelling() public revealed whitelisted {
uint256 tokenId = 1;
uint32 firstSalePrice = 1000e6;
uint32 secondSalePrice = 1000e6;
address secondBuyer = makeAddr("secondBuyer");
usdc.mint(secondBuyer, 200_000e6);
vm.prank(owner);
nftDealers.whitelistWallet(userWithEvenMoreCash);
uint256 feeFirstSale = nftDealers.calculateFees(firstSalePrice);
uint256 feeSecondSale = nftDealers.calculateFees(secondSalePrice);
uint256 expectedFirstSellerPayout = uint256(firstSalePrice) + nftDealers.lockAmount() - feeFirstSale;
uint256 expectedSecondSellerPayoutWithPhantomCollateral =
uint256(secondSalePrice) + nftDealers.lockAmount() - feeSecondSale;
uint256 expectedSecondSellerPayoutWithoutPhantomCollateral = uint256(secondSalePrice) - feeSecondSale;
mintAndListNFTForTesting(tokenId, firstSalePrice);
vm.startPrank(userWithEvenMoreCash);
usdc.approve(address(nftDealers), firstSalePrice);
nftDealers.buy(tokenId);
vm.stopPrank();
vm.prank(userWithCash);
nftDealers.collectUsdcFromSelling(tokenId);
uint256 collateralAfterFirstCollect = nftDealers.collateralForMinting(tokenId);
uint256 firstSellerBalanceAfterCollect = usdc.balanceOf(userWithCash);
uint256 contractBalanceAfterFirstCollect = usdc.balanceOf(address(nftDealers));
console2.log("feeFirstSale:", feeFirstSale);
console2.log("expectedFirstSellerPayout:", expectedFirstSellerPayout);
console2.log("firstSellerBalanceAfterCollect:", firstSellerBalanceAfterCollect);
console2.log("contractBalanceAfterFirstCollect:", contractBalanceAfterFirstCollect);
console2.log("collateralAfterFirstCollect:", collateralAfterFirstCollect);
assertEq(firstSellerBalanceAfterCollect, expectedFirstSellerPayout, "sanity: first seller collected once");
assertEq(
contractBalanceAfterFirstCollect, feeFirstSale, "sanity: only first-sale fee remains after first collect"
);
assertEq(
collateralAfterFirstCollect,
nftDealers.lockAmount(),
"BUG: collateralForMinting[tokenId] was not cleared after successful collect"
);
vm.prank(userWithEvenMoreCash);
nftDealers.list(tokenId, secondSalePrice);
(address relistedSeller, uint32 relistedPrice,, uint256 relistedTokenId, bool relistedIsActive) =
nftDealers.s_listings(tokenId);
console2.log("relistedSeller:", relistedSeller);
console2.log("relistedPrice:", uint256(relistedPrice));
console2.log("relistedTokenId:", relistedTokenId);
console2.log("relistedIsActive:", relistedIsActive ? uint256(1) : uint256(0));
assertEq(relistedSeller, userWithEvenMoreCash, "sanity: buyer is now the relisting seller");
assertEq(relistedPrice, secondSalePrice, "sanity: second listing price stored");
assertTrue(relistedIsActive, "sanity: second listing is active");
vm.startPrank(secondBuyer);
usdc.approve(address(nftDealers), secondSalePrice);
nftDealers.buy(tokenId);
vm.stopPrank();
uint256 secondSellerBalanceBeforeCollect = usdc.balanceOf(userWithEvenMoreCash);
uint256 contractBalanceBeforeSecondCollect = usdc.balanceOf(address(nftDealers));
console2.log("feeSecondSale:", feeSecondSale);
console2.log("secondSellerBalanceBeforeCollect:", secondSellerBalanceBeforeCollect);
console2.log("contractBalanceBeforeSecondCollect:", contractBalanceBeforeSecondCollect);
console2.log(
"expectedSecondSellerPayoutWithPhantomCollateral:", expectedSecondSellerPayoutWithPhantomCollateral
);
console2.log(
"expectedSecondSellerPayoutWithoutPhantomCollateral:", expectedSecondSellerPayoutWithoutPhantomCollateral
);
assertEq(
contractBalanceBeforeSecondCollect,
feeFirstSale + uint256(secondSalePrice),
"sanity: second collect is funded only by pooled balance, not by fresh collateral"
);
vm.prank(userWithEvenMoreCash);
nftDealers.collectUsdcFromSelling(tokenId);
uint256 secondSellerBalanceAfterCollect = usdc.balanceOf(userWithEvenMoreCash);
uint256 contractBalanceAfterSecondCollect = usdc.balanceOf(address(nftDealers));
uint256 collateralAfterSecondCollect = nftDealers.collateralForMinting(tokenId);
console2.log("secondSellerBalanceAfterCollect:", secondSellerBalanceAfterCollect);
console2.log("contractBalanceAfterSecondCollect:", contractBalanceAfterSecondCollect);
console2.log("collateralAfterSecondCollect:", collateralAfterSecondCollect);
assertEq(
secondSellerBalanceAfterCollect,
secondSellerBalanceBeforeCollect + expectedSecondSellerPayoutWithPhantomCollateral,
"BUG: secondary seller received phantom collateral on resale"
);
assertTrue(
secondSellerBalanceAfterCollect
!= secondSellerBalanceBeforeCollect + expectedSecondSellerPayoutWithoutPhantomCollateral,
"BUG: payout should not include old collateral on secondary sale"
);
assertEq(
contractBalanceAfterSecondCollect,
0,
"BUG: phantom collateral payout consumed balances that should not belong to the secondary seller"
);
assertEq(
collateralAfterSecondCollect,
nftDealers.lockAmount(),
"BUG: collateral mapping remains stale even after repeated sale settlements"
);
}
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/NFTDealersTest.t.sol:NFTDealersTest
[PASS] testCollateralForMintingNotResetAfterCollectUsdcFromSelling() (gas: 650153)
Logs:
feeFirstSale: 10000000
expectedFirstSellerPayout: 1010000000
firstSellerBalanceAfterCollect: 1010000000
contractBalanceAfterFirstCollect: 10000000
collateralAfterFirstCollect: 20000000
relistedSeller: 0x533575789af8F38A73C7747E36C17C1835FDF44a
relistedPrice: 1000000000
relistedTokenId: 1
relistedIsActive: 1
feeSecondSale: 10000000
secondSellerBalanceBeforeCollect: 199000000000
contractBalanceBeforeSecondCollect: 1010000000
expectedSecondSellerPayoutWithPhantomCollateral: 1010000000
expectedSecondSellerPayoutWithoutPhantomCollateral: 990000000
secondSellerBalanceAfterCollect: 200010000000
contractBalanceAfterSecondCollect: 0
collateralAfterSecondCollect: 20000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.68ms (1.79ms CPU time)
Ran 1 test suite in 13.88ms (4.68ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)