5/5
## Understanding NFT Metadata with IPFS At the core of any Non-Fungible Token (NFT) is its metadata, which describes the token's properties, such as its name, description, and image. This metadata is typically stored in a JSON file. For instance, the metadata for a token with ID `0` might look like this: ```json { "name": "First Place Panagram Game NFT", "description": "You were the first to solve the panagram game! Congratulations!", "image": "ipfs://bafybeieh7jzzpzgfq54hf7viwxpixue08djzpbemfsaakxwzjffs6um/0.png" } ``` This JSON file itself can be hosted on the InterPlanetary File System (IPFS). A common way to access IPFS content is through a URI, such as `bafybeicqfc4ipkle34tgqv3gh7gccwhmr22qdg7p6k6oxon255mnwb6csi.ipfs.localhost:8080/0.json`. If you have an IPFS browser extension installed and the IPFS desktop application running, your browser can directly resolve these IPFS URIs. Notice the `"image"` field in the JSON metadata. It also points to an IPFS URI: `ipfs://bafybeieh7jzzpzgfq54hf7viwxpixue08djzpbemfsaakxwzjffs6um/0.png`. Opening this specific URI in a browser (again, with IPFS tools configured) would display the actual image associated with the NFT – in this case, a trophy. ## ERC1155 Token URI Mechanics When working with the ERC1155 token standard in Solidity, handling metadata URIs is a key aspect. Let's consider a contract `Panagram.sol` that inherits from OpenZeppelin's `ERC1155` implementation. The `ERC1155` parent contract is often initialized with a base URI string directly in the child contract's constructor. This base URI typically includes a placeholder, `{id}`, which will be replaced with the specific token ID. ```solidity // Panagram.sol import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; // ... other imports and interfaces like IVerifier contract Panagram is ERC1155 { // ... other state variables IVerifier public verifier; constructor(IVerifier _verifier) ERC1155("ipfs://bafybeicqfc4ipkle34tgqv3gh7gccwhmr22qdg7p6k6oxon255mnwb6csi/{id}.json") { verifier = _verifier; // ... other constructor logic } // ... } ``` The ERC1155 standard defines how clients (like wallets or marketplaces) retrieve metadata URIs for tokens. This is primarily done through the `uri(uint256 id)` function. The OpenZeppelin `ERC1155.sol` contract provides a base implementation: ```solidity // ERC1155.sol (OpenZeppelin snippet) /** * @dev See {IERC1155MetadataURI-uri}. * * This implementation returns the same URI for *all* token types. It relies * on the token type ID substitution mechanism * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the ERC]. * * Clients calling this function must replace the `\{id\}` substring with * actual token type ID. */ function uri(uint256 /* id */) public view virtual returns (string memory) { return _uri; // _uri is the string set in the constructor } ``` **Token Type ID Substitution Mechanism:** The ERC1155 standard relies on a client-side substitution mechanism. When a client application (e.g., OpenSea, MetaMask, or a custom frontend) needs to display metadata for a specific token ID: 1. It calls the contract's `uri(tokenID)` function. 2. The contract returns the base URI string (e.g., `"ipfs://bafybeicqfc4ipkle34tgqv3gh7gccwhmr22qdg7p6k6oxon255mnwb6csi/{id}.json"`). 3. The client then programmatically replaces the `"{id}"` substring with the actual `tokenID` (usually in its hexadecimal representation, though decimal representation like `0` leading to `0.json` is also common depending on server setup). For token ID `0`, this would result in `"ipfs://.../0.json"`. 4. This fully formed URI is then fetched to retrieve the JSON metadata. 5. Finally, the `image` URI (or other asset URIs) within the JSON metadata is fetched to display the visual content. In the OpenZeppelin `ERC1155.sol` implementation, the `id` parameter in the `uri` function (`uint256 /* id */`) is commented out in the parameter list. This is because this base implementation returns a single URI string (`_uri`) intended for all token types, and the crucial substitution of `{id}` is expected to be handled by the client application off-chain. ## Optional: Collection-Level Metadata with `contractURI` Beyond individual token metadata, some platforms and marketplaces like OpenSea use a `contractURI` function to fetch collection-level metadata. This function, if implemented, would return a URI pointing to a JSON file describing the entire NFT contract or collection (e.g., collection name, description, logo). This is generally considered an optional feature but can enhance how your collection is displayed on various platforms. ## Further Learning: ERC1155 Deep Dive For a more comprehensive understanding of the intricacies of the ERC1155 standard, including its batch transfer capabilities and other advanced features, the NFT section of the "Advanced Foundry" course by Patrick Collins is a highly recommended resource. ## Core Functionality for the Panagram Game Contract The `Panagram.sol` contract, designed for a word game, will require several key functions to operate: 1. A function to initiate a new game round. 2. A function allowing users to submit their guesses for the panagram. 3. An internal mechanism (likely involving a separate Verifier contract) to check if a guess is correct. 4. View functions to allow users and frontends to query the current state of the game. 5. A crucial administrative function to set or update the address of the Verifier contract. 6. A view function to retrieve the current Verifier contract address. ## Implementing an Updatable and Owner-Restricted Verifier In our `Panagram.sol` contract, we need to store the address of a `Verifier` contract, which will be responsible for validating game solutions. Initially, this might be set in the constructor: ```solidity // Panagram.sol interface IVerifier { // Define functions of the Verifier contract if needed for interaction // For this example, we only need its address. } contract Panagram is ERC1155 { IVerifier public verifier; // Initially immutable if 'immutable' keyword was used constructor(IVerifier _initialVerifier) ERC1155("ipfs://bafybeicqfc4ipkle34tgqv3gh7gccwhmr22qdg7p6k6oxon255mnwb6csi/{id}.json") // Ownable will be initialized here later { verifier = _initialVerifier; } // ... } ``` If the `verifier` state variable was initially declared with the `immutable` keyword, it could only be set once in the constructor. To allow for future updates (e.g., if the Verifier contract logic needs to be upgraded or changed), we must remove the `immutable` keyword, making it a regular state variable: ```solidity // Panagram.sol (snippet) IVerifier public verifier; // Changed to be mutable ``` **Adding the `setVerifier` Function and Event** To update the verifier address, we introduce a `setVerifier` function. It's good practice to emit an event when such a critical address is changed: ```solidity // Panagram.sol (snippet) event Panagram_VerifierUpdated(IVerifier verifier); function setVerifier(IVerifier _newVerifier) external { // We'll add access control here verifier = _newVerifier; emit Panagram_VerifierUpdated(_newVerifier); } ``` When defining the `Panagram_VerifierUpdated` event, the `verifier` parameter could be marked as `indexed`. However, since one wouldn't typically filter or search for all past verifier addresses on-chain, making it `indexed` is not strictly necessary for this particular use case and would incur slightly higher gas costs for emitting the event. **Securing `setVerifier` with `Ownable`** Allowing anyone to call `setVerifier` would be a significant security risk. We need to restrict this function so that only the contract owner can call it. OpenZeppelin's `Ownable` contract is perfect for this. 1. **Import `Ownable`:** ```solidity // Panagram.sol import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; ``` 2. **Inherit from `Ownable`:** Modify the contract declaration to inherit from `Ownable` in addition to `ERC1155`. ```solidity // Panagram.sol contract Panagram is ERC1155, Ownable { // ... } ``` 3. **Initialize `Ownable` in the Constructor:** The `Ownable` contract's constructor needs to be called. Modern versions of OpenZeppelin's `Ownable` (e.g., v5.x) require an `initialOwner` argument in their constructor. We'll set the deployer of the `Panagram` contract (`msg.sender`) as the initial owner. ```solidity // Panagram.sol // ... interface IVerifier { /* ... */ } contract Panagram is ERC1155, Ownable { IVerifier public verifier; event Panagram_VerifierUpdated(IVerifier verifier); constructor(IVerifier _initialVerifier) ERC1155("ipfs://bafybeicqfc4ipkle34tgqv3gh7gccwhmr22qdg7p6k6oxon255mnwb6csi/{id}.json") Ownable(msg.sender) // Initialize Ownable with the deployer as owner { verifier = _initialVerifier; } // ... } ``` 4. **Add `onlyOwner` Modifier and Input Validation:** Apply the `onlyOwner` modifier (provided by `Ownable`) to the `setVerifier` function. It's also good practice to add a `require` statement to ensure the new verifier address is not the zero address. ```solidity // Panagram.sol (within the contract body) function setVerifier(IVerifier _newVerifier) external onlyOwner { require(address(_newVerifier) != address(0), "Panagram: Invalid verifier address"); verifier = _newVerifier; emit Panagram_VerifierUpdated(_newVerifier); } ``` With this setup, the `setVerifier` function can only be successfully called by the address that deployed the `Panagram` contract (or an address to which ownership has been subsequently transferred). This provides a secure administrative mechanism to update the verifier contract address if needed, crucial for the long-term maintenance and adaptability of the Panagram game.
A foundational examination of Understanding NFT Metadata with IPFS - Uncover the structure of NFT metadata using JSON, its storage on IPFS, and the mechanics of ERC1155 token URIs. This lesson further details implementing an admin-controlled, updatable verifier address in Solidity with `Ownable`.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025
Duration: 6min
Duration: 1h 11min
Duration: 2h 12min
Duration: 3h 19min
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025