Dynamic Traits

OpenSea developer guide

ERC-7496: NFT Dynamic Traits introduces an innovative approach to incorporating NFT metadata directly onchain. This advancement empowers contract developers to create richer and more novel NFT applications and games. Onchain user actions can prompt immediate updates to the traits displayed on OpenSea and other platforms. Dynamic Traits do not replace offchain token URIs, but rather augment them by giving a few key traits the ability to change over time. Furthermore, these traits can be read and updated onchain in a standardized manner, enabling a wide variety of interesting applications.

An example of using Dynamic Traits in a redeemables context can be seen in ERC-7498: NFT Redeemables where a "redemption" trait can be updated onchain and therefore allows metadata to be updated at the time of the action. It also allows for onchain order protection during fulfillment based on the trait values to prevent frontrunning.

📘

See Shipyard for standard contracts that implement ERC-7496 and Redeemables for ERC-7498.

Here is a simplified example of an NFT contract that allows token owners to update their token's "name" property, emitting an event that notifies indexers to process the update.

contract NameYourNFT is ERC721 {
  mapping(uint256 tokenId => bytes32 name) tokenNames;
  ...
  function setName(uint256 tokenId, string memory newName) external {
    // Only allow the token owner to update its name
    if (ownerOf(tokenId) != msg.sender) revert NotOwner();
    
    // To keep things simple, the name should fit in bytes32
    if (bytes(newName).length > 32) revert NameTooLong();
    bytes32 newNameAsBytes32 = bytes32(newName);
    
    // Update the token name in storage
    tokenNames[tokenId] = newNameAsBytes32;
    
    // Emit an event with the update (according to ERC-7496)
    emit TraitUpdated(bytes32("name"), tokenId, newNameAsBytes32);
  }
  
  // getTraitValue() defined as part of ERC-7496 Specification
  function getTraitValue(uint256 tokenId, bytes32 traitKey) external view returns (bytes32) {
     if (!_exists(tokenId)) revert TokenDoesNotExist();
     if (traitKey == bytes32("name")) {
       return tokenNames[tokenId];
     }
     revert UnknownTraitKey();
   }
}

// or, using ERC721DynamicTraits from Shipyard:
// https://github.com/ProjectOpenSea/shipyard-core/blob/main/src/dynamic-traits/ERC721DynamicTraits.sol

contract NameYourNFT is ERC721DynamicTraits {
  bytes32 constant NAME_KEY = bytes32("name");
  
  function setName(uint256 tokenId, string memory newName) external {
    // Only allow the token owner to update its name
    if (ownerOf(tokenId) != msg.sender) revert NotOwner();
    
    // To keep things simple, the name should fit in bytes32
    if (bytes(newName).length > 32) revert NameTooLong();
    bytes32 newNameAsBytes32 = bytes32(newName);

    setTrait(tokenId, NAME_KEY, newNameAsBytes32);
  }
  
  function getName(uint256 tokenId) external returns (string memory name) {
  	name = string(abi.encodePacked(getTraitValue(tokenId, NAME_KEY)));
  }
}

Ingestion Guide

Metadata

For OpenSea to ingest a Dynamic Trait, the metadata URI specified by getTraitMetadataURI() must first be ingested. The event TraitMetadataURIUpdated() should be emitted for our indexers to be notified to fetch the metadata URI, initially and on subsequent updates.

Please strictly follow the metadata schema as defined in the ERC so it is not rejected by indexers. In the future this constraint may be relaxed, but is currently a hard requirement. If you have any issues with having your traits being ingested or displayed, please email [email protected] with details and transaction hashes to try to recreate the issue to fix.

Limits

Our indexers won't update more than 10,000 tokens or traits at once for performance reasons. To overcome this limit, please split updates across multiple transactions.

Order Protection

Order protection during fulfillment will be available in the future, once the development and integration of the Dynamic Traits zone for Seaport is complete.