Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

All Tokens Share Same Image URI in `Snowman.sol::tokenURI` function

[High] All Tokens Share Same Image URI in Snowman.sol::tokenURI function


Description

The tokenURI function in Snowman.sol returns metadata using a single static URI stored in the state variable s_SnowmanSvgUri. This means every Snowman NFT minted by the contract shares the same metadata and image, regardless of its tokenId.


Risk

This issue impacts the uniqueness, integrity, and marketplace compatibility of the NFTs:

  1. No Visual or Metadata Uniqueness : All tokens are indistinguishable from one another, undermining the core principle of NFTs being “non-fungible.”

  2. Poor Marketplace Presentation : Platforms like OpenSea may display these NFTs as duplicates or fail to categorize them properly, degrading discoverability and appeal.

  3. Limited Extensibility : The design does not support future features such as traits, rarity, or metadata upgrades. It’s incompatible with evolving NFT standards that expect per-token metadata.


Proof of Concept

The following function demonstrates that all NFTs use a shared static URI:

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; // Same for all tokens
return string(
abi.encodePacked(
_baseURI(),
Base64.encode(
abi.encodePacked(
'{"name":"',
name(),
'", "description":"Snowman for everyone!!!", ',
'"attributes": [{"trait_type": "freezing", "value": 100}], "image":"',
imageURI,
'"}'
)
)
)
);
}

This logic does not include any differentiation based on the tokenId.


Recommended Mitigation

Introduce per-token metadata, either by dynamically generating it or by storing it at mint time.

Proposed Fix in Snowman.sol:

+ mapping(uint256 => string) private s_tokenURIs;
function mintSnowman(address receiver, uint256 amount) external {
for (uint256 i = 0; i < amount; i++) {
+ uint256 tokenId = s_TokenCounter;
_safeMint(receiver, tokenId);
+ s_tokenURIs[tokenId] = generateUniqueTokenURI(tokenId);
emit SnowmanMinted(receiver, tokenId);
s_TokenCounter++;
}
}
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (!_exists(tokenId)) revert ERC721Metadata__URI_QueryFor_NonExistentToken();
+ return s_tokenURIs[tokenId];
}
+ function generateUniqueTokenURI(uint256 tokenId) internal pure returns (string memory) {
+ return string(abi.encodePacked("ipfs://your-base-uri/", Strings.toString(tokenId), ".json"));
+ }
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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