DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

`tokenURI` returns a raw Base64 blob — missing `data:application/json;base64,` prefix

Root + Impact

Description

`tokenURI` constructs on-chain JSON metadata, Base64-encodes it, and returns the result. The outer `abi.encodePacked` call prepends `_baseURI()`, which is inherited from OpenZeppelin's `ERC721` and returns an empty string `""` by default because the contract never overrides it.
```solidity
return string(
abi.encodePacked(
_baseURI(), // always returns "" — no override exists
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"', profileName, '", ',
'"description":"A soulbound dating profile NFT.", ',
'"attributes": [{"trait_type": "Age", "value": ',
Strings.toString(profileAge),
'}], ',
'"image":"', imageURI, '"}'
)
)
)
)
);
```
The ERC-721 metadata standard (and EIP-721 as widely implemented) requires `tokenURI` to return one of:
- An HTTPS/IPFS URL pointing to a JSON file, **or**
- A data URI of the form `data:application/json;base64,<encoded>` for fully on-chain metadata.
Because `_baseURI()` returns `""`, the function returns a bare Base64 string such as:
```
eyJuYW1lIjoiQWxpY2UiLCAiZGVzY3JpcHRpb24iOiJBIHNvdWxib3VuZC...
```
No marketplace, wallet, or browser can resolve this string as a URI. The JSON data is present but permanently inaccessible to any off-chain consumer.
```solidity
return string(
abi.encodePacked(
_baseURI(), // always returns "" — no override exists
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:

No marketplace, wallet, or browser can resolve this string as a URI. The JSON data is present but permanently inaccessible to any off-chain consumer.

Impact:

Every NFT minted by this contract will show broken metadata on all platforms. Users who mint a profile will have no name, no age attribute, and no profile image displayed anywhere the token is viewed. Since profile identity is the core purpose of this NFT, this failure invalidates the primary use-case of the contract for every user. The issue affects all past and future mints and cannot be corrected retroactively for already-issued tokens without redeployment.

Proof of Concept

Test: `testPoC_TokenURIMissingDataURIPrefix` in `test/testSoulboundProfileNFT.t.sol`.
```solidity
// PoC: tokenURI returns a raw Base64 blob with no data URI scheme prefix.
// Marketplaces call tokenURI and treat the result as a URL or data URI.
// Without "data:application/json;base64," the string is unresolvable and
// the NFT metadata will appear broken on every standards-compliant viewer.
function testPoC_TokenURIMissingDataURIPrefix() public {
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 tokenId = soulboundNFT.profileToToken(user);
string memory uri = soulboundNFT.tokenURI(tokenId);
// Log the raw output so the failure message shows what was actually returned
emit log_named_string("tokenURI returned", uri);
// A valid on-chain data URI MUST start with this prefix
bytes memory expectedPrefix = bytes("data:application/json;base64,");
bytes memory actualBytes = bytes(uri);
// Assert the URI is long enough to contain the prefix
assertTrue(
actualBytes.length >= expectedPrefix.length,
"URI too short to contain data URI prefix"
);
// Assert each prefix byte matches
for (uint256 i = 0; i < expectedPrefix.length; i++) {
assertEq(
actualBytes[i],
expectedPrefix[i],
"tokenURI is missing 'data:application/json;base64,' prefix"
);
}
}
```
```
Logs:
tokenURI returned: eyJuYW1lIjoiQWxpY2UiLCAiZGVzY3JpcHRpb24iOiJBIHNv...
[FAIL: tokenURI is missing 'data:application/json;base64,' prefix:
actual[0] = 0x65 ('e')
expected[0] = 0x64 ('d')
]
```
Decoded actual output: `{"name":"Alice", "description":"A soulbound dating profile NFT.", "attributes": [{"trait_type": "Age", "value": 25}], "image":"ipfs://profileImage"}` — the JSON is correct but the data URI wrapper is absent.
Run with: `forge test --match-test testPoC_TokenURIMissingDataURIPrefix -vvv`

Recommended Mitigation

Remove the `_baseURI()` call and prepend the data URI scheme prefix directly:
```solidity
return string(
abi.encodePacked(
"data:application/json;base64,", // add this prefix
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"', profileName, '", ',
'"description":"A soulbound dating profile NFT.", ',
'"attributes": [{"trait_type": "Age", "value": ',
Strings.toString(profileAge),
'}], ',
'"image":"', imageURI, '"}'
)
)
)
)
);
```
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 8 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!