Puppy Raffle

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

tokenURI() emits syntactically invalid JSON - rarity value is not wrapped in double quotes

Root + Impact

Description

  • tokenURI() builds the token's JSON metadata with abi.encodePacked, but embeds the rarity name ("common", "rare", or "legendary") directly as an unquoted identifier instead of a properly quoted JSON string.

  • The resulting attributes field looks like "value": common instead of the syntactically valid "value": "common". Every single token minted by the contract has this same malformed metadata - it is deterministic, not a rare edge case.

  • Wallets, marketplaces, and indexers that parse this JSON with a standard JSON parser will fail to parse the attributes array (or the whole document, depending on the parser's strictness), degrading or breaking how the NFT displays.

return string(
abi.encodePacked(
_baseURI(),
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"',
name(),
'", "description":"An adorable puppy!", ',
'"attributes": [{"trait_type": "rarity", "value": ',
@> rareName,
'}], "image":"',
imageURI,
'"}'
)
)
)
)
);

Risk

Likelihood:

  • Reason 1 // This triggers on every single mint with no special conditions - it is a deterministic output-formatting bug, not something that depends on an attacker or unusual input.

Impact:

  • Impact 1 // The NFT's on-chain metadata is not valid JSON, which can cause wallets, marketplaces, and indexers to fail to display the rarity trait correctly (or reject the metadata document entirely), directly undermining the "collectible NFT" value proposition of the raffle prize - though it does not put funds at risk.

Proof of Concept

Ran with forge test --match-path "test/PoC_9.t.sol" -vv: [PASS] test_TokenURI_ProducesInvalidJSON_UnquotedRarityValue(). The test decodes the actual on-chain tokenURI(0) output and inspects the raw bytes the contract concatenates for the attributes field. Logs confirm the byte immediately following "value": is 0x63 (the ASCII character c, the start of common), not 0x22 (a double quote) - i.e. the value is emitted unquoted, exactly as claimed.

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
pragma experimental ABIEncoderV2;
import {Test, console} from "forge-std/Test.sol";
import {PuppyRaffle} from "../src/PuppyRaffle.sol";
contract PoC_9_TokenURIInvalidJSON is Test {
PuppyRaffle raffle;
address[] players;
function setUp() public {
raffle = new PuppyRaffle(1 ether, address(999), 1 days);
players.push(address(1));
players.push(address(2));
players.push(address(3));
players.push(address(4));
vm.deal(address(1), 10 ether);
vm.deal(address(2), 10 ether);
vm.deal(address(3), 10 ether);
vm.deal(address(4), 10 ether);
}
function test_TokenURI_ProducesInvalidJSON_UnquotedRarityValue() public {
for (uint256 i = 0; i < players.length; i++) {
address[] memory one = new address[](1);
one[0] = players[i];
vm.prank(players[i]);
raffle.enterRaffle{value: 1 ether}(one);
}
vm.warp(block.timestamp + 1 days + 1);
raffle.selectWinner();
string memory uri = raffle.tokenURI(0);
console.log("tokenURI(0) =", uri);
uint256 rarity = raffle.tokenIdToRarity(0);
string memory rareName = raffle.rarityToName(rarity);
bytes memory jsonFragment = abi.encodePacked(
'"attributes": [{"trait_type": "rarity", "value": ',
rareName,
'}]'
);
bytes memory prefix = bytes('"attributes": [{"trait_type": "rarity", "value": ');
uint256 valueStartIndex = prefix.length;
bytes1 charAfterColonSpace = jsonFragment[valueStartIndex];
console.log("byte right after 'value: ' is (as uint8):", uint8(charAfterColonSpace));
assertTrue(
charAfterColonSpace != bytes1(0x22),
"rarity `value` in tokenURI() JSON is not wrapped in double quotes - invalid JSON"
);
}
}

Recommended Mitigation

'"attributes": [{"trait_type": "rarity", "value": ',
- rareName,
+ '"',
+ rareName,
+ '"',
'}], "image":"',

Wrap rareName in escaped double quotes so the emitted JSON is syntactically valid: "value":"common" instead of "value": common.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour 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!