NFT Dealers

First Flight #58
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: low
Valid

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

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 {
Updates

Lead Judging Commences

rubik0n Lead Judge 16 days ago
Submission Judgement Published
Validated
Assigned finding tags:

accidental-eth-locking

Support

FAQs

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

Give us feedback!