Snowman Merkle Airdrop

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

[L-03] `tokenURI()` has unreachable custom error — `ownerOf()` already reverts for non-existent tokens

Description

Snowman.tokenURI() checks if (ownerOf(tokenId) == address(0)) and reverts with a custom error ERC721Metadata__URI_QueryFor_NonExistentToken. But OpenZeppelin's ERC721 ownerOf() already reverts with ERC721NonexistentToken for tokens that don't exist, so the custom check is never reached. The custom error is dead code.

Vulnerability Details

// src/Snowman.sol, lines 47-49
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (ownerOf(tokenId) == address(0)) { // @> ownerOf already reverts before returning address(0)
revert ERC721Metadata__URI_QueryFor_NonExistentToken(); // @> UNREACHABLE
}
// ...
}

OpenZeppelin ERC721 (v5.x) ownerOf() implementation:

function ownerOf(uint256 tokenId) public view returns (address) {
return _requireOwned(tokenId); // reverts with ERC721NonexistentToken if not minted
}

The _requireOwned function reverts before returning address(0), so the custom check in tokenURI never evaluates to true. The ERC721Metadata__URI_QueryFor_NonExistentToken error is declared but can never be thrown.

Risk

Likelihood:

  • The dead code exists in every deployment.

Impact:

  • No security impact. The function still reverts for non-existent tokens — just with OpenZeppelin's error instead of the custom one. External integrations that catch ERC721Metadata__URI_QueryFor_NonExistentToken will never see it; they need to catch ERC721NonexistentToken instead.

Proof of Concept

function testExploit_TokenUriDeadCode() public {
Snowman snowman = new Snowman("https://example.com/snowman.svg");
// Query tokenURI for non-existent token
// Reverts with OZ's ERC721NonexistentToken, NOT the custom error
vm.expectRevert(
abi.encodeWithSignature("ERC721NonexistentToken(uint256)", 999)
);
snowman.tokenURI(999);
// The custom error ERC721Metadata__URI_QueryFor_NonExistentToken is never thrown
}

Recommendations

Remove the unreachable check or replace ownerOf with a direct storage lookup:

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
- if (ownerOf(tokenId) == address(0)) {
- revert ERC721Metadata__URI_QueryFor_NonExistentToken();
- }
+ _requireOwned(tokenId); // reverts if token doesn't exist
// ...
}
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!