Flow

Sablier
FoundryDeFi
20,000 USDC
View results
Submission Details
Severity: medium
Invalid

Double Base64 Encoding in NFT Metadata Generation Causes Excessive Gas Costs for Token URI Operations

Summary

The FlowNFTDescriptor.tokenURI function performs two separate Base64 encoding operations: one for the SVG and another for the entire JSON metadata. Each encoding operation is gas-intensive, making this implementation inefficient.

Code

https://github.com/Cyfrin/2024-10-sablier/blob/main/src/FlowNFTDescriptor.sol#L13

function tokenURI(
IERC721Metadata, /* sablierFlow */
uint256 /* streamId */
)
external
pure
override
returns (string memory uri)
{
string memory svg = '<svg width="500" height="500" .../>'; // SVG data
// First Base64 encoding operation for SVG
string memory encodedSvg = Base64.encode(bytes(svg));
string memory json = string.concat(
'{"description": "This NFT represents a payment stream in Sablier Flow",',
'"external_url": "https://sablier.com",',
'"name": "Sablier Flow",',
'"image": "data:image/svg+xml;base64,',
encodedSvg, // First encoded data
'"}'
);
// Second Base64 encoding operation for entire JSON
uri = string.concat(
"data:application/json;base64,",
Base64.encode(bytes(json)) // Second encoding operation
);
}

Impact

The double Base64 encoding creates unnecessary gas costs that compound with each tokenURI call. This becomes particularly expensive in scenarios like:

  • Batch minting operations where multiple NFTs are created

  • Marketplace listings where URIs are fetched for multiple tokens

  • Integration scenarios requiring frequent metadata access

  • Protocol operations that need to process multiple streams

Each Base64 encoding operation involves multiple memory operations and string manipulations. When this happens twice per call, the gas costs become prohibitive, especially for protocol users dealing with multiple streams.

Fix

contract FlowNFTDescriptor is IFlowNFTDescriptor {
// Pre-encoded SVG stored as constant
string private constant ENCODED_SVG = "PHN2ZyB3..."; // Pre-encoded SVG data
// Metadata prefix and suffix as constants
string private constant JSON_PREFIX = '{"description":"This NFT represents a payment stream in Sablier Flow","external_url":"https://sablier.com","name":"Sablier Flow","image":"data:image/svg+xml;base64,';
string private constant JSON_SUFFIX = '"}';
function tokenURI(
IERC721Metadata sablierFlow,
uint256 streamId
)
external
view
override
returns (string memory)
{
// Single concatenation operation
string memory json = string.concat(
JSON_PREFIX,
ENCODED_SVG,
JSON_SUFFIX
);
// Single Base64 encoding operation
return string.concat(
"data:application/json;base64,",
Base64.encode(bytes(json))
);
}
}

Alternative Optimization:

contract FlowNFTDescriptor is IFlowNFTDescriptor {
// Pre-encode both SVG and static parts of JSON
bytes32 private constant ENCODED_PREFIX = 0x...; // Pre-encoded JSON start + SVG
bytes32 private constant ENCODED_SUFFIX = 0x...; // Pre-encoded JSON end
function tokenURI(
IERC721Metadata sablierFlow,
uint256 streamId
)
external
view
override
returns (string memory)
{
// Single abi.encodePacked operation
return string(
abi.encodePacked(
"data:application/json;base64,",
ENCODED_PREFIX,
ENCODED_SUFFIX
)
);
}
}

For dynamic data additions in future updates:

function _encodeMetadata(string memory dynamicData) internal pure returns (string memory) {
// Use pre-encoded static parts with dynamic data
return string.concat(
ENCODED_PREFIX,
Base64.encode(bytes(dynamicData)), // Single encode for dynamic part only
ENCODED_SUFFIX
);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 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.