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,
uint256
)
external
pure
override
returns (string memory uri)
{
string memory svg = '<svg width="500" height="500" .../>';
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,
'"}'
);
uri = string.concat(
"data:application/json;base64,",
Base64.encode(bytes(json))
);
}
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 {
string private constant ENCODED_SVG = "PHN2ZyB3...";
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)
{
string memory json = string.concat(
JSON_PREFIX,
ENCODED_SVG,
JSON_SUFFIX
);
return string.concat(
"data:application/json;base64,",
Base64.encode(bytes(json))
);
}
}
Alternative Optimization:
contract FlowNFTDescriptor is IFlowNFTDescriptor {
bytes32 private constant ENCODED_PREFIX = 0x...;
bytes32 private constant ENCODED_SUFFIX = 0x...;
function tokenURI(
IERC721Metadata sablierFlow,
uint256 streamId
)
external
view
override
returns (string memory)
{
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) {
return string.concat(
ENCODED_PREFIX,
Base64.encode(bytes(dynamicData)),
ENCODED_SUFFIX
);
}