Snowman Merkle Airdrop

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

Broken tokenURI Returns Same Metadata For All NFTs

Root + Impact

Description

  • The tokenURI() function should return unique metadata for each tokenId, allowing marketplaces and wallets to display different properties for each NFT.


However, the current implementation ignores the tokenId parameter entirely and returns the exact same SVG image and metadata for every single Snowman NFT, making all NFTs indistinguishable.

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (ownerOf(tokenId) == address(0)) {
revert ERC721Metadata__URI_QueryFor_NonExistentToken();
}
// @> tokenId parameter is never used after this point
string memory imageURI = s_SnowmanSvgUri;
return string(
abi.encodePacked(
_baseURI(),
Base64.encode(
abi.encodePacked(
'{"name":"',
name(), // @> Returns "Snowman Airdrop" for ALL tokens
'", "description":"Snowman for everyone!!!", ',
// @> Same attributes for ALL tokens
'"attributes": [{"trait_type": "freezing", "value": 100}], "image":"',
imageURI, // @> Same image for ALL tokens
'"}'
)
)
)
);
}

Risk

Likelihood:

  • Every call to tokenURI() for any tokenId returns identical data


The issue is present in the code and manifests immediately upon deployment

  • NFT marketplaces and wallets will display all Snowman NFTs as identical clones

Impact:

  • All Snowman NFTs appear identical on marketplaces (OpenSea, Blur, etc.), destroying any uniqueness or collectibility


Users cannot distinguish between their NFTs or determine which one they're trading

  • Violates ERC721 metadata standards where each token should have unique identifying information

  • Severely reduces market value since buyers cannot identify specific NFTs they want to purchase

  • May violate marketplace listing requirements that expect unique metadata per token

Proof of Concept

Copy this test in test folder

function testAllTokensHaveSameMetadata() public {
// Mint 3 different Snowman NFTs
snowman.mintSnowman(user1, 1); // tokenId 0
snowman.mintSnowman(user2, 1); // tokenId 1
snowman.mintSnowman(user3, 1); // tokenId 2
string memory uri0 = snowman.tokenURI(0);
string memory uri1 = snowman.tokenURI(1);
string memory uri2 = snowman.tokenURI(2);
// All three return EXACTLY the same string
assertEq(uri0, uri1);
assertEq(uri1, uri2);
// Decoded metadata shows:
// Token 0: {"name":"Snowman Airdrop", "description":"Snowman for everyone!!!", "attributes": [{"trait_type": "freezing", "value": 100}], "image":"<same_svg>"}
// Token 1: {"name":"Snowman Airdrop", "description":"Snowman for everyone!!!", "attributes": [{"trait_type": "freezing", "value": 100}], "image":"<same_svg>"}
// Token 2: {"name":"Snowman Airdrop", "description":"Snowman for everyone!!!", "attributes": [{"trait_type": "freezing", "value": 100}], "image":"<same_svg>"}
}

Recommended Mitigation

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (ownerOf(tokenId) == address(0)) {
revert ERC721Metadata__URI_QueryFor_NonExistentToken();
}
string memory imageURI = s_SnowmanSvgUri;
return string(
abi.encodePacked(
_baseURI(),
Base64.encode(
abi.encodePacked(
'{"name":"',
- name(),
+ name(), ' #', Strings.toString(tokenId),
'", "description":"Snowman for everyone!!!", ',
- '"attributes": [{"trait_type": "freezing", "value": 100}], "image":"',
+ '"attributes": [{"trait_type": "freezing", "value": 100}, {"trait_type": "tokenId", "value": ', Strings.toString(tokenId), '}], "image":"',
imageURI,
'"}'
)
)
)
);
}
+ // Import Strings library
+ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 13 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!