The PoC (test/PoC_PR-09.t.sol) runs three normal raffle rounds, choosing callers so the contract's own rarity formula mints one common, one rare and one legendary puppy. For each token it base64-decodes tokenURI and asserts that the JSON contains the bare "value": <word>}, is byte-identical to the contract's payload rebuilt without quotes (2 bytes shorter than the quoted version), and is rejected by Foundry's strict vm.parseJson; .name and .image also cannot be extracted. A baseline test shows the same document with the rarity value quoted parses and returns the expected name, image and "common". Run it with forge test --match-path test/PoC_PR-09.t.sol -vv.
pragma solidity ^0.7.6;
pragma experimental ABIEncoderV2;
import {Test, console} from "forge-std/Test.sol";
import {PuppyRaffle} from "../src/PuppyRaffle.sol";
import {Base64} from "lib/base64/base64.sol";
contract PoC_PR09 is Test {
PuppyRaffle raffle;
uint256 constant FEE = 1e18;
uint256 constant DURATION = 1 days;
string constant PREFIX = "data:application/json;base64,";
string constant COMMON_URI = "ipfs://QmSsYRx3LpDAb1GZQm7zZ1AuHZjfbPkD6J7s9r41xu1mf8";
string constant RARE_URI = "ipfs://QmUPjADFGEKmfohdTaNcWhp7VGk26h5jXDA7v3VtTnTLcW";
string constant LEGENDARY_URI = "ipfs://QmYx6GsYAKnNzZ9A6NvEKV9nf1VaDzJrqDR23Y8YSkebLU";
uint160 nextAddr = 0x100000;
function setUp() public {
raffle = new PuppyRaffle(FEE, address(0xFEE), DURATION);
vm.deal(address(this), 1000 ether);
}
function _mintVia(address caller) internal returns (uint256 tokenId) {
address[] memory p = new address[](4);
for (uint256 i = 0; i < 4; i++) {
p[i] = address(nextAddr++);
}
raffle.enterRaffle{value: FEE * 4}(p);
vm.warp(block.timestamp + DURATION + 1);
tokenId = raffle.totalSupply();
vm.prank(caller);
raffle.selectWinner();
assertEq(raffle.totalSupply(), tokenId + 1, "selectWinner must mint exactly one puppy");
}
function _callerFor(uint256 wanted) internal view returns (address) {
for (uint160 s = 0xC0FFEE; s < 0xC0FFEE + 5000; s++) {
uint256 r = uint256(keccak256(abi.encodePacked(address(s), block.difficulty))) % 100;
uint256 got;
if (r <= 70) got = 70;
else if (r <= 95) got = 25;
else got = 5;
if (got == wanted) return address(s);
}
revert("no caller found");
}
function _decodedMetadata(uint256 tokenId) internal view returns (string memory) {
bytes memory uri = bytes(raffle.tokenURI(tokenId));
bytes memory pre = bytes(PREFIX);
require(uri.length > pre.length, "uri too short");
for (uint256 i = 0; i < pre.length; i++) {
require(uri[i] == pre[i], "unexpected uri prefix");
}
bytes memory b64 = new bytes(uri.length - pre.length);
for (uint256 i = 0; i < b64.length; i++) {
b64[i] = uri[i + pre.length];
}
return string(Base64.decode(string(b64)));
}
function _contains(string memory hay, string memory needle) internal pure returns (bool) {
bytes memory h = bytes(hay);
bytes memory n = bytes(needle);
if (n.length > h.length) return false;
for (uint256 i = 0; i + n.length <= h.length; i++) {
bool ok = true;
for (uint256 j = 0; j < n.length; j++) {
if (h[i + j] != n[j]) {
ok = false;
break;
}
}
if (ok) return true;
}
return false;
}
function _payload(string memory rareName, string memory image, bool quoted) internal pure returns (string memory) {
string memory q = quoted ? '"' : "";
return string(
abi.encodePacked(
'{"name":"',
"Puppy Raffle",
'", "description":"An adorable puppy!", ',
'"attributes": [{"trait_type": "rarity", "value": ',
q,
rareName,
q,
'}], "image":"',
image,
'"}'
)
);
}
function _parses(string memory json) internal view returns (bool) {
try vm.parseJson(json) returns (bytes memory) {
return true;
} catch {
return false;
}
}
function _imageReadable(string memory json) internal view returns (bool) {
try vm.parseJsonString(json, ".image") returns (string memory) {
return true;
} catch {
return false;
}
}
function _nameReadable(string memory json) internal view returns (bool) {
try vm.parseJsonString(json, ".name") returns (string memory) {
return true;
} catch {
return false;
}
}
function test_baseline_quotedMetadataParses() public {
string memory fixedJson = _payload("common", COMMON_URI, true);
console.log("fixed :", fixedJson);
assertTrue(_parses(fixedJson), "quoted metadata should parse");
assertEq(vm.parseJsonString(fixedJson, ".name"), "Puppy Raffle");
assertEq(vm.parseJsonString(fixedJson, ".image"), COMMON_URI);
assertEq(vm.parseJsonString(fixedJson, ".attributes[0].value"), "common");
}
function test_tokenURI_isInvalidJson_forEveryRarity() public {
uint256[3] memory rarities = [uint256(70), uint256(25), uint256(5)];
string[3] memory names = ["common", "rare", "legendary"];
string[3] memory images = [COMMON_URI, RARE_URI, LEGENDARY_URI];
for (uint256 k = 0; k < 3; k++) {
uint256 id = _mintVia(_callerFor(rarities[k]));
assertEq(raffle.tokenIdToRarity(id), rarities[k], "rarity drawn as targeted");
string memory json = _decodedMetadata(id);
console.log("token id", id);
console.log("decoded:", json);
string memory bare = string(abi.encodePacked('"value": ', names[k], "}"));
string memory quotedV = string(abi.encodePacked('"value": "', names[k], '"}'));
assertTrue(_contains(json, bare), "decoded metadata contains bare rarity token");
assertFalse(_contains(json, quotedV), "rarity value is not quoted");
assertEq(json, _payload(names[k], images[k], false), "tokenURI == bare payload");
assertEq(bytes(_payload(names[k], images[k], true)).length, bytes(json).length + 2);
assertTrue(_parses(_payload(names[k], images[k], true)), "fixed variant parses");
assertFalse(_parses(json), "tokenURI metadata must fail to parse");
assertFalse(_nameReadable(json), ".name unreadable");
assertFalse(_imageReadable(json), ".image unreadable");
}
assertEq(raffle.totalSupply(), 3, "three puppies minted, all with broken metadata");
}
}