NFT Dealers

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

mintNft() and buy() are payable but have no ETH handling, permanently locking sent ETH

Author Revealed upon completion

Root + Impact

Description

  • mintNft() and buy() accept payment exclusively in USDC via transferFrom. No ETH is required or used in either function.

  • Both functions are marked payable, so the EVM accepts ETH sent alongside calls without reverting. Since the contract has no receive(), fallback(), or ETH withdrawal function, any ETH sent to these functions is permanently locked.

@> function mintNft() external payable onlyWhenRevealed onlyWhitelisted {
...
require(usdc.transferFrom(msg.sender, address(this), lockAmount), "USDC transfer failed");
// msg.value is never used or returned
}
@> function buy(uint256 _listingId) external payable {
...
bool success = usdc.transferFrom(msg.sender, address(this), listing.price);
// msg.value is never used or returned
}

Risk

Likelihood:

  • Users familiar with ETH-native NFT marketplaces may send ETH expecting it to cover the mint or purchase price.

  • Wallet or frontend bugs can cause unintended ETH to be attached to contract calls.

Impact:

  • Any ETH sent to mintNft() or buy() is permanently lost with no recovery path.

  • No admin function or selfdestruct exists to rescue locked ETH.

Proof of Concept

  1. Alice calls mintNft{value: 0.1 ether}() — USDC transfer succeeds, NFT is minted, 0.1 ETH is locked in contract forever.

  2. Bob calls buy{value: 0.5 ether}(1) — USDC transfer succeeds, NFT transfers to Bob, 0.5 ETH is locked in contract forever.

function test_ethLockedInMintNft() public {
vm.prank(alice);
nftDealers.mintNft{value: 0.1 ether}();
assertEq(address(nftDealers).balance, 0.1 ether); // ETH stuck, no recovery path
}
function test_ethLockedInBuy() public {
vm.prank(alice); nftDealers.mintNft();
vm.prank(alice); nftDealers.list(1, 100e6);
vm.prank(bob);
nftDealers.buy{value: 0.5 ether}(1);
assertEq(address(nftDealers).balance, 0.5 ether); // ETH stuck, no recovery path
}

Recommended Mitigation

Remove payable from both functions. Since all payments are handled in USDC, neither function has any reason to accept ETH. This causes the EVM to revert any call that includes a non-zero msg.value, protecting users from accidental ETH loss.

- function mintNft() external payable onlyWhenRevealed onlyWhitelisted {
+ function mintNft() external onlyWhenRevealed onlyWhitelisted {
- function buy(uint256 _listingId) external payable {
+ function buy(uint256 _listingId) external {

Support

FAQs

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

Give us feedback!