NFT Dealers

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

mintNft and buy are payable while protocol operates in USDC only — native ETH permanently locked with no recovery path

Author Revealed upon completion

Root + Impact

Description

mintNft() and buy() are marked payable but the protocol settles exclusively in USDC. Any ETH sent with these calls is accepted by the contract and permanently locked — there is no withdraw, rescue, or receive/fallback recovery function for native ETH. The contract inherits from OpenZeppelin ERC721, which includes no native token recovery.

Risk

Likelihood:

  • Occurs when a user calls mintNft() or buy() with non-zero msg.value — possible through user error, frontend bugs, or wallets that default to non-zero value

  • Both functions are core user-facing operations called in normal marketplace usage

Impact:

  • Any native ETH sent is irrecoverably locked — no contract function can move it, not even the owner

  • Cumulative losses grow over the contract's lifetime with no recovery path

Proof of Concept

forge test --match-test test_M01_EthLockedForever -vvv
function test_M01_EthLockedForever() public {
vm.deal(alice, 1 ether);
vm.startPrank(alice);
usdc.approve(address(nftDealers), type(uint256).max);
nftDealers.mintNft{value: 0.5 ether}();
vm.stopPrank();
assertEq(address(nftDealers).balance, 0.5 ether);
// No withdraw function for ETH — permanently locked
}

Output: Contract holds 0.5 ETH with no function to extract it.

Recommended Mitigation

Root cause — payable modifier on USDC-only functions:

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

Fix — remove payable:

-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!