Weather Witness

First Flight #40
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

Owner-Controlled Image URIs in `tokenURI` Can Lead to Gas Exhaustion or Costly Metadata Retrieval

Root + Impact

Description

The tokenURI function constructs NFT metadata JSON on-chain. This JSON includes an image field, whose value is sourced from s_weatherToTokenURI. This mapping is populated in the constructor using weatherURIs provided by the contract deployer (owner). There are no checks on the length of these URIs.
If the contract owner, during deployment, provides excessively long strings for weatherURIs, the image string in the generated JSON can become extremely large. The subsequent Base64.encode(jsonData) operation scales in gas cost with the size of jsonData. Returning the final, very long metadata string also incurs gas costs proportional to its length.
This means that if an image URI is excessively long, any attempt to call tokenURI for an NFT currently displaying that image could consume a large amount of gas. This might lead to transactions running out of gas or becoming prohibitively expensive, effectively making the NFT's metadata inaccessible on-chain for wallets, marketplaces, or other dApps.

// Root cause in the codebase with @> marks to highlight the relevant section
// File: src/WeatherNft.sol
constructor(
Weather[] memory weathers,
// @> string[] memory weatherURIs, // Owner provides these URIs. No on-chain length validation.
address functionsRouter,
FunctionsConfig memory _config,
uint256 _currentMintPrice,
uint256 _stepIncreasePerMint,
address _link,
address _keeperRegistry,
address _keeperRegistrar,
uint32 _upkeepGaslimit
)
ERC721("Weather NFT", "W-NFT")
FunctionsClient(functionsRouter)
ConfirmedOwner(msg.sender)
{
require(
weathers.length == weatherURIs.length,
WeatherNft__IncorrectLength()
);
for (uint256 i; i < weathers.length; ++i) {
// @> s_weatherToTokenURI[weathers[i]] = weatherURIs[i]; // Storing potentially very long strings
}
// ... other constructor logic
}
function tokenURI(
uint256 tokenId
) public view override returns (string memory) {
_requireOwned(tokenId); // Checks ownership
// @> string memory image = s_weatherToTokenURI[s_tokenIdToWeather[tokenId]]; // Retrieves the potentially long URI
bytes memory jsonData = abi.encodePacked(
'{"name": "Weathear NFT", "user": "',
Strings.toHexString(_ownerOf(tokenId)), // Fixed length (42 chars)
'", "image": "',
// @> image, // If 'image' is excessively long, 'jsonData' becomes very large
'"}'
);
// @> string memory base64TransformedData = Base64.encode(jsonData); // Encoding very long 'jsonData' costs significant gas
// @> return string.concat(_baseURI(), base64TransformedData); // Returning a very long string also costs gas
}

Risk

Likelihood: Low

  • This vulnerability depends on the contract owner deploying with extremely long image URIs. Typically, owners want metadata to be accessible. It might occur due to error, misunderstanding gas implications, or in a test/malicious scenario by the owner (though self-sabotaging).

Impact: Low

  • Metadata Inaccessibility: Calls to tokenURI for affected NFTs might fail due to out-of-gas errors or become too expensive for practical use by dApps like marketplaces or wallets.

  • Reduced NFT Interoperability: If metadata cannot be reliably retrieved, the NFTs might not display or function correctly on third-party platforms.

Proof of Concept

  1. The contract owner deploys WeatherNft. During deployment, for one of the weather states (e.g., Weather.SUNNY), they provide an extremely long string as its URI in the weatherURIs array (e.g., 50,000 characters, perhaps a data URI with embedded content).

  2. A user mints an NFT, and its current weather state is Weather.SUNNY.

  3. A marketplace attempts to display this NFT by calling tokenURI(tokenId).

  4. Inside tokenURI:

    • image becomes the 50,000-character string.

    • jsonData becomes a string of slightly more than 50,000 bytes.

    • Base64.encode(jsonData) operates on this large byte array. This operation's gas cost is proportional to the input size. For very large inputs, this can be substantial.

    • Returning the resulting base64 string (approx. 66,500 characters) also consumes considerable gas.

  5. The tokenURI call might hit the block gas limit or the gas limit provided by the caller, causing the transaction to revert and the metadata to be unretrievable.

Recommended Mitigation

The primary mitigation is for the contract deployer to be aware of these gas implications and use reasonably sized URIs.

  1. Documentation: Clearly document for the deployer the potential gas issues with very long image URIs and recommend keeping them concise (e.g., pointing to off-chain resources like IPFS/Arweave if the image data itself is large, rather than embedding large data URIs directly).

  2. Deployment Scripts: If providing deployment scripts, add comments or console warnings about URI length.

A strict on-chain code change to limit URI length in the constructor is possible but might be overly restrictive for legitimate use cases of moderately long URIs. This is often a policy decision for the project.

If an on-chain length restriction is desired as a hard safeguard:

// File: src/WeatherNft.sol
+ // Define a maximum reasonable length for image URIs stored on-chain.
+ // This value should be chosen based on typical URI lengths and gas considerations.
+ // E.g., IPFS hash + gateway prefix is usually < 100 chars. Data URIs can be longer.
+ uint256 private constant MAX_IMAGE_URI_LENGTH = 512; // Example: 512 characters.
constructor(
Weather[] memory weathers,
string[] memory weatherURIs,
address functionsRouter,
FunctionsConfig memory _config,
uint256 _currentMintPrice,
uint256 _stepIncreasePerMint,
address _link,
address _keeperRegistry,
address _keeperRegistrar,
uint32 _upkeepGaslimit
)
ERC721("Weather NFT", "W-NFT")
FunctionsClient(functionsRouter)
ConfirmedOwner(msg.sender)
{
require(
weathers.length == weatherURIs.length,
WeatherNft__IncorrectLength()
);
for (uint256 i; i < weathers.length; ++i) {
+ require(bytes(weatherURIs[i]).length <= MAX_IMAGE_URI_LENGTH, "WeatherNft__ImageUriTooLong");
s_weatherToTokenURI[weathers[i]] = weatherURIs[i];
}
s_functionsConfig = _config;
s_currentMintPrice = _currentMintPrice;
s_stepIncreasePerMint = _stepIncreasePerMint;
s_link = _link;
s_keeperRegistry = _keeperRegistry;
s_keeperRegistrar = _keeperRegistrar;
s_upkeepGaslimit = _upkeepGaslimit;
s_tokenCounter = 1;
}
// No change to tokenURI function itself needed if constructor enforces length.

This mitigation focuses on preventing the storage of overly long URIs at deployment time. The choice of MAX_IMAGE_URI_LENGTH would be crucial and depends on expected URI types (URLs vs. small data URIs).

Updates

Appeal created

bube Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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