NFT Dealers

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

Incorrectly Apply onlyWhitelisted, Preventing Non-Whitelisted Users from Exercising Legitimate Protocol Rights Description

Author Revealed upon completion

Root + Impact

Description

  • The protocol's documented actor model specifies that the onlyWhitelisted restriction applies exclusively to mintNft. All other user-facing actions — buying, listing, updating price, cancelling a listing, and collecting sale proceeds — are explicitly available to non-whitelisted users as well.

  • The implementation contradicts this specification. list functions carry onlyWhitelisted modifiers (https://github.com/CodeHawks-Contests/2026-03-NFT-dealers/blob/f34038b7b948d0902ef5ae99e9f1a4964bd3cdb5/src/NFTDealers.sol#L127)that the design does not sanction, locking non-whitelisted users out of operations they are entitled to perform. Most critically, a non-whitelisted user who legitimately purchases an NFT via buy() immediately finds themselves unable to re-list, manage, or monetise that asset through any protocol function.

// ✅ Correct — mintNft should be whitelisted-only per spec
function mintNft() external onlyWhitelisted onlyRevealed { ... }
// ❌ Incorrect — spec grants these to non-whitelisted users as well
@> function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted { ... }

Risk

Likelihood:

  • buy() is open to all users, so any non-whitelisted address that purchases an NFT will immediately encounter this inconsistency upon attempting any follow-on action. This is a normal, expected usage path, not an edge case.

  • No malicious intent is required — any regular user who buys before being whitelisted triggers the issue.

Impact:

  • A non-whitelisted buyer pays full market price for an NFT they cannot re-list, reprice, or collect proceeds from. The asset is economically stranded within the protocol.

  • The protocol violates its own documented specification, creating a trust and correctness gap that affects any integration or audit relying on the actor model documentation.

  • If a seller's whitelist status is revoked after listing but before calling collectUsdcFromSelling, their proceeds are permanently locked despite the sale having already occurred legitimately.

Proof of Concept

Add this to 2026-03-NFT-dealers/test/NFTDealersTest.t.sol,run forge test --match-test testPoC_M04_NonWhitelistedCannotListAfterBuying -vvvv

function testPoC_M04_NonWhitelistedCannotListAfterBuying() public revealed {
uint256 tokenId = 1;
uint32 nftPrice = 1000e6;
uint256 lockAmt = nftDealers.lockAmount();
// Whitelisted seller mints and lists
vm.prank(owner);
nftDealers.whitelistWallet(userWithCash);
vm.startPrank(userWithCash);
usdc.approve(address(nftDealers), lockAmt);
nftDealers.mintNft();
nftDealers.list(tokenId, nftPrice);
vm.stopPrank();
// Non-whitelisted buyer purchases NFT — correctly permitted by spec
address nonWhitelistedBuyer = makeAddr("nonWhitelistedBuyer");
usdc.mint(nonWhitelistedBuyer, nftPrice + lockAmt);
assertFalse(nftDealers.isWhitelisted(nonWhitelistedBuyer));
vm.startPrank(nonWhitelistedBuyer);
usdc.approve(address(nftDealers), nftPrice);
nftDealers.buy(tokenId); // ✅ succeeds as expected
assertEq(nftDealers.ownerOf(tokenId), nonWhitelistedBuyer);
// Attempt to re-list — should succeed per spec, but reverts
vm.expectRevert();
nftDealers.list(tokenId, 1500e6);
// Attempt to collect proceeds (if they later sell) — should succeed per spec, but reverts
vm.expectRevert();
nftDealers.collectUsdcFromSelling(tokenId);
vm.stopPrank();
}

Recommended Mitigation

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

Support

FAQs

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

Give us feedback!