Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

Unreachable Guard in tokenURI — Dead Code

Root + Impact

Description

tokenURI() opens with a guard intended to reject queries for non-existent tokens, reverting with
ERC721Metadata__URI_QueryFor_NonExistentToken. However, the condition used — ownerOf(tokenId) == address(0) — can
never be true in OpenZeppelin's ERC721 implementation.

OZ's ownerOf() calls _requireOwned(tokenId) internally, which reverts with ERC721NonexistentToken before
returning if the token does not exist. ownerOf() never returns address(0) — it either returns a valid owner
address or reverts. The custom guard therefore never fires, the custom error is dead code, and a caller querying
a non-existent token receives OZ's generic revert rather than the protocol's descriptive error.

The intent is clear and correct — the developer wanted to guard non-existent token queries — but the
implementation relies on a return value that OZ guarantees will never be address(0).

// Snowman.sol:47-50
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
@> if (ownerOf(tokenId) == address(0)) {
@> revert ERC721Metadata__URI_QueryFor_NonExistentToken(); // never reached
@> } // ownerOf() reverts first
string memory imageURI = s_SnowmanSvgUri;
...
}

Risk

Likelihood:

  • When any caller (NFT marketplace, indexer, wallet) queries tokenURI() for a token ID that has not been minted

Impact:

  • Callers receive OZ's ERC721NonexistentToken(tokenId) instead of the protocol's
    ERC721Metadata__URI_QueryFor_NonExistentToken — a developer experience and debuggability degradation

  • The custom error is permanently unreachable dead code, misleading future maintainers into believing it provides
    an active guard

  • No security impact — the function still reverts correctly for non-existent tokens, just with the wrong error


Proof Of Concept


Statically verifiable. OZ ERC721.ownerOf() source:

// OZ ERC721.sol
function ownerOf(uint256 tokenId) public view virtual returns (address) {
return _requireOwned(tokenId); // reverts with ERC721NonexistentToken if unminted
}
function _requireOwned(uint256 tokenId) internal view returns (address) {
address owner = _ownerOf(tokenId);
if (owner == address(0)) {
revert ERC721NonexistentToken(tokenId); // <-- reverts here, never returns address(0)
}
return owner;
}

Calling tokenURI(999) on an unminted token ID reverts with ERC721NonexistentToken(999), not
ERC721Metadata__URI_QueryFor_NonExistentToken. The custom error branch is never executed.

Recommended Mitigation

Replace the dead ownerOf() check with OZ's _ownerOf() internal, which returns address(0) without reverting for
non-existent tokens, making the guard reachable:

This preserves the protocol's custom error while correctly intercepting non-existent token queries before the
metadata assembly executes.

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
- if (ownerOf(tokenId) == address(0)) {
+ if (_ownerOf(tokenId) == address(0)) {
revert ERC721Metadata__URI_QueryFor_NonExistentToken();
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!