NFT Dealers

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

Mishandling of Eth - Due to existence of a couple of payable functions and lack of a withdraw function capable of taking Eth out of the contract, any paid Eth will be trapped forever.

Author Revealed upon completion

Root + Impact

Description

  • The mint and buy functions are payable which means users can send Eth when calling them, however, it will not be calculated against the price of NFTs.

  • The cost of any NFT will be deducted from the minter or buyer's USDC balance. Therefore, the paid Eth will be wasted from the user's point of view.

@> function mintNft() external payable onlyWhenRevealed onlyWhitelisted {
if (msg.sender == address(0)) revert InvalidAddress();
require(tokenIdCounter < MAX_SUPPLY, "Max supply reached");
require(msg.sender != owner, "Owner can't mint NFTs");
require(
usdc.transferFrom(msg.sender, address(this), lockAmount),
"USDC transfer failed"
);
tokenIdCounter++;
collateralForMinting[tokenIdCounter] = lockAmount;
_safeMint(msg.sender, tokenIdCounter);
}
@> 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;
emit NFT_Dealers_Sold(msg.sender, listing.price);
}

Risk

Likelihood: Low

  • Even though it is very unlikely the user call those functions while sending some Eth, it is not completely impossible.

Impact: High

  • Not only the users lose their Eth tokens, but also the contract cannot recover or withdraw them either.


Proof of Concept

Please copy the following function to the test file and run it using test forge --mt testSendingEthWithMintOrBuyCanCauseTrappedEth.

function testSendingEthWithMintOrBuyCanCauseTrappedEth()
public
whitelisted
revealed
{
deal(userWithCash, 10 ether);
deal(userWithEvenMoreCash, 10 ether);
uint256 initialContractBalance = address(nftDealers).balance;
vm.startBroadcast(userWithCash);
usdc.approve(address(nftDealers), 20e6);
nftDealers.mintNft{value: 1 ether}();
nftDealers.list(1, 21e6);
vm.stopBroadcast();
vm.startBroadcast(userWithEvenMoreCash);
usdc.approve(address(nftDealers), 21e6);
nftDealers.buy{value: 1 ether}(1);
vm.stopBroadcast();
uint256 finalContractBalance = address(nftDealers).balance;
assertGt(
finalContractBalance,
initialContractBalance,
"Sending ETH with mint or buy should be possible"
);
}

Recommended Mitigation

Is solution is quite simple, just make the functions non-payabale as follows.

- function mintNft() external payable onlyWhenRevealed onlyWhitelisted {
+ function mintNft() external onlyWhenRevealed onlyWhitelisted {
if (msg.sender == address(0)) revert InvalidAddress();
require(tokenIdCounter < MAX_SUPPLY, "Max supply reached");
require(msg.sender != owner, "Owner can't mint NFTs");
require(
usdc.transferFrom(msg.sender, address(this), lockAmount),
"USDC transfer failed"
);
tokenIdCounter++;
collateralForMinting[tokenIdCounter] = lockAmount;
_safeMint(msg.sender, tokenIdCounter);
}
- function buy(uint256 _listingId) external payable {
+ function buy(uint256 _listingId) external {
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;
emit NFT_Dealers_Sold(msg.sender, listing.price);
}

Support

FAQs

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

Give us feedback!