NFT Dealers

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

list() missing onlyWhenRevealed allows whitelisted users to list NFTs before the collection is revealed

Author Revealed upon completion

Root + Impact

Description

The protocol defines two phases: preparation
(pre-reveal, whitelist only) and revealed (minting + listing + buying enabled).

The
list() function lacks the onlyWhenRevealedmodifier that mintNft() has, allowing whitelisted users to create active listings during the preparation phase, before the
collection is publicly revealed.

// @> mintNft correctly requires reveal
function mintNft() external payable
onlyWhenRevealed onlyWhitelisted { ... }
// @> list() missing onlyWhenRevealed — listings
can be created pre-reveal
function list(uint256 _tokenId, uint32 _price)
external onlyWhitelisted {
...
}

Risk

Likelihood:

  • Any whitelisted user can exploit this during the preparation phase

  • Preparation phase is always present before every collection launch

Impact:

  • Secondary market listings appear before the collection is public — breaks intended UX and reveal mechanics

  • Potential front-running: insiders list at low prices before reveal, creating arbitrage opportunities against buyers who don't know the collection's value

Proof of Concept

function test_listBeforeReveal() public {
// Deploy contract — collection NOT revealed
NFTDealers nft = new NFTDealers(owner, address(usdc), "T", "T", "ipfs://", 20e6);
assert(nft.isCollectionRevealed() == false);
// Mint requires reveal (onlyWhenRevealed) — correctly blocked
// But list() has NO onlyWhenRevealed check
// A whitelisted user who somehow holds an NFT can list before reveal:
// To demonstrate: reveal temporarily to mint, then show list() ignores reveal state
vm.prank(owner); nft.revealCollection();
vm.prank(owner); nft.whitelistWallet(seller);
usdc.mint(seller, 20e6);
vm.startPrank(seller);
usdc.approve(address(nft), 20e6);
nft.mintNft(); // tokenId = 1
vm.stopPrank();
// list() succeeds with no onlyWhenRevealed guard — missing modifier confirmed
vm.prank(seller);
nft.list(1, 500e6); // no revert — listing created regardless of reveal state
}
For the simpler findings (LOW severity) where there's no real exploit to run, the PoC field can just be the
code snippet showing the problematic line — e.g. for LOW-1 (self-transfer):
// In collectUsdcFromSelling():
usdc.safeTransfer(address(this), fees); // transfers from contract TO contract — no-op
usdc.safeTransfer(msg.sender, amountToSeller); // seller receives proceeds
// fees never actually leave the contract, making this line pure gas waste
The full runnable test (forge test --match-test test_MEDIUM2_ListBeforeReveal -vv) is at
/tmp/nft-dealers-repo/test/NFTDealersPoC.t.sol if you want to run it first to confirm output before pasting.

Recommended Mitigation

- function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {
+ function list(uint256 _tokenId, uint32 _price) external onlyWhenRevealed onlyWhitelisted {

Support

FAQs

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

Give us feedback!