Root + Impact
Description
The NFT stores metadata on-chain and returns Base64-encoded JSON from tokenURI(). NFT clients generally expect tokenURI() to return a URI such as ipfs://..., https://..., or an inline data URI.
The function returns only the raw Base64 string because _baseURI() is not overridden and therefore defaults to an empty string. Without the data:application/json;base64, prefix, wallets and marketplaces can treat the returned string as an invalid or unknown URI instead of decoding it as JSON metadata.
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (ownerOf(tokenId) == address(0)) {
revert ERC721Metadata__URI_QueryFor_NonExistentToken();
}
string memory profileName = _profiles[tokenId].name;
uint256 profileAge = _profiles[tokenId].age;
string memory imageURI = _profiles[tokenId].profileImage;
return string(
abi.encodePacked(
_baseURI(),
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"',
profileName,
'", ',
'"description":"A soulbound dating profile NFT.", ',
'"attributes": [{"trait_type": "Age", "value": ',
Strings.toString(profileAge),
"}], ",
'"image":"',
imageURI,
'"}'
)
)
)
)
);
}
Risk
Likelihood:
Impact:
-
Profile NFTs may fail to render metadata in wallets, marketplaces, and indexers.
-
The dating profile NFT can lose its intended utility as a visible verified profile.
Proof of Concept
function testTokenURIMissingDataPrefix() public {
SoulboundProfileNFT nft = new SoulboundProfileNFT();
address alice = address(0xA11CE);
vm.prank(alice);
nft.mintProfile("Alice", 25, "ipfs://alice");
string memory uri = nft.tokenURI(1);
bytes memory uriBytes = bytes(uri);
bytes memory expectedPrefix = bytes("data:application/json;base64,");
for (uint256 i = 0; i < expectedPrefix.length; i++) {
assertTrue(i >= uriBytes.length || uriBytes[i] != expectedPrefix[i]);
break;
}
}
Recommended Mitigation
Prepend the standard inline JSON metadata prefix before the Base64 payload.
return string(
abi.encodePacked(
- _baseURI(),
+ "data:application/json;base64,",
Base64.encode(
- bytes(
- abi.encodePacked(
- '{"name":"',
- profileName,
- '", ',
- '"description":"A soulbound dating profile NFT.", ',
- '"attributes": [{"trait_type": "Age", "value": ',
- Strings.toString(profileAge),
- "}], ",
- '"image":"',
- imageURI,
- '"}'
- )
+ abi.encodePacked(
+ '{"name":"',
+ profileName,
+ '", ',
+ '"description":"A soulbound dating profile NFT.", ',
+ '"attributes": [{"trait_type": "Age", "value": ',
+ Strings.toString(profileAge),
+ "}], ",
+ '"image":"',
+ imageURI,
+ '"}'
)
)
)
);