Back to blogs
Written by
Sidharth Kumthekar
Published on
March 22, 2024

EIP 3664 - The full guide to advanced NFT properties

EIP 3664 extends NFT standards to enable customizable, interoperable, and evolvable NFT attributes. You'll learn how to use it, how it works, and its ecosystem impact.

Table of Contents

What if your NFTs could level up, gain skills, and evolve like characters in a video game?

Over recent years, NFTs have evolved immensely, both in terms of utilities and technology.

From digital art and collectibles to making their way into the metaverse, real estate, and web3 gaming, NFTs are ushering in new digital ownership and value paradigms. But with newer use cases come newer functionalities, which in turn demand the development of newer and more robust technical standards.

Published in July 2021, the Ethereum Improvement Proposal (EIP 3664) is one such evolution, which extends the existing NFT standards to enable customizable, interoperable, and evolvable NFT attributes.

In this article, we will go through what EIP 3664 is, why we even need it, how it differs from already existing NFT protocols, how it works, and what impact it brings to the NFT ecosystem.

EIP 3664 - Overview

EIP3664 is an extendable NFT metadata protocol that enables all the existing NFT protocols like ERC 721 and 1155 standards to add descriptive power to their attributes. If you don’t know what an EIP is, check out zero to hero guide to become a smart contract auditor.

EIP3664 is not an NFT creation protocol like ERC 721 OR 1155.

Instead,  it acts as an extensible NFT metadata framework to provide attribute extendibility to NFTs created using ERC 721 or ERC 1155. The specification specifies various ways in which attributes can be tokenized and utilized to customize NFTs.

Each attribute has an attrId associated with it, with the help of which an independent attribute can be tokenized, transferred, and combined to create dynamic artworks, in-game assets, and avatars.

Why EIP 3664?

The goal behind EIP 3664 is to transform static NFTs into variable NFTs. Allowing developers to create modular - component-based NFTs whose attributes can transform, transfer, and transition based on application-specific events.

Users can also combine and trade these attribute components to pro-create better composable NFTs.

Thus EIP 3664, aims to complement the foundational standards, not replace them.

To understand its need, we must take a moment to understand what the foundational ERC 721, and ERC 1155 contracts offer and what they lack.

What are the ERC721 and ERC1155?

ERC-721 key features:

  • Established as the basic standard for NFT creation on Ethereum.
  • Enables creation and transfer of Non-Fungible Tokens, only.
  • Provides a simple mapping between tokens and owners.
  • Includes basic methods to track and transfer tokens from one account to another.
  • An owner having multiple token types to deploy, can deploy only one collection at a time. Hence, for multiple tokens, multiple smart-contracts must be deployed.

ERC-1155 key features:

  • Multi-token standard which supports both fungible (ERC 20) and non-fungible (ERC 721) tokens.
  • Allows configuration for multiple token types, in the same contract, each represented by a unique tokenID.
  • Every tokenId can have its own metadata, supply, and attributes.
  • Introduces batch transfer to allow multiple token transfers in a single transaction.Thereby reducing the gas costs and avoiding redundant API calls to mint each collection.
  • Includes burning and batch-burning methods to permanently remove tokens from the supply.
  • Safe transfer rules allow senders to specify necessary conditions that must be met before the token transfer is initiated.

Comparison between 751 and 1155:

ERC 721 ERC1155
Type of Token Creation Supported Non-Fungible ONLY BOTH, Fungible & Non-Fungible
Number of NFT Collections that one contract can create Only ONE Multiple (Infinite)
Out of the box Support For Batched Transaction No Yes
Token Supply Fixed Mintable and Burnable
Out of the box Transaction Reversal Cannot revert transactions The safe transfer function enables transaction reversal
Support for Attribute Mutability Not supported Not supported

What are NFT properties?

Let us assume that you are playing a web3-powered Pokémon game, where each Pokémon is an NFT.

At the start of your game. Professor ‘X’ gifts you Auditchu - an Eth-type Pokémon - who loves to audit smart contracts, find vulnerabilities, and protect dApps from evil exploits.

Aditchu. Image source: Midjourney

Auditchu’s attributes (characteristics) can either be in the form of his physical traits like talons and wings.

Attributes can also describe its overall features like “strength,” “skills,” “type,” and “level.”

Let us have a look at some of Auditchu’s attributes:

Pokémon Type Value Primary Attribute (never changes)
WingsLevel Level 1 Upgrades on completing in-game quests and missions.
WingsImage image Each wingsLevel has a different wingsImage. Can be attached, detached, or transferred.
Strength 30 / 100 Variable- may increase or decrease based on auditing record
Logic 40 / 100 Variable- may increase or decrease based on auditing record
Vision 35 /100 Variable- may increase or decrease based on auditing record
Aura aesthetic Transferable trait, can trade for other auras

Dynamic Properties in the EIP3664

  • An attribute is a specific property or characteristic that provides additional detail or metadata for an NFT token. They have their own metadata and values that exist independently from the base NFT.
  • They act as composable components that can be attached to and removed from base NFTs - For example "strength", "level", and "generation".
  • Each attribute defined in ERC-3664 has its own unique ID. These IDs are used to manage and manipulate the attributes separately (on-chain).
  • EIP-3664 currently extends its support for upgradable, variable, transferable, evolvable, and text-based attributes. We’ll look into them in detail as we progress.

Each attribute ID (attrId) has a unique URI. A URI is a unique identifier for some data or resource.


function attrURI(uint256 attrId) external view returns (string memory);

The URI can point to a JSON metadata file that describes the attribute.


{
  "name": "WingsImage",
  "description": "Upgrades on completing in-game quests and missions.",
  "type": "transferable",
  "visual": {
    "src": "ipfs://wing.svg",
    "alt": "wing component"
  }
}


As Auditchu grows older (in-game activity time), audits more contracts, and wins security contests at CodeHawks his strength and experience increase gradually.

Increased strength leads to new trait upgrades like larger wings!

Eventually, Auditchu unlocks his evolution seed and successfully transforms into a larger, stronger, and smarter Pokemon - Auditmax!

Image: Auditmax (evolved version of Auditchu)
Pokémon Type Value Primary Attribute (never changes)
WingsLevel Level 3 Upgrades on completing in-game quests and missions.
WingsImage image Each wingsLevel has a different wingsImage. Can be attached, detached, or transferred.
Strength 80 / 100 Variable- may increase or decrease based on auditing record
Logic 90 / 100 Variable- may increase or decrease based on auditing record
Vision 90 / 100 Variable- may increase or decrease based on auditing record
Aura majestic Transferable trait, can trade for other auras. Unlocks new aura on evolution.
Crown (new unlocked trait!) image Trait attached after evolution

It is essential to understand that in our example, the evolved NFT is not getting replaced by another new NFT. The older NFT traits are getting modified to newer traits.

This is how powerful attribute composability is!

Now that we have understood the purpose of EIP 3664 its time to look at the code!

EIP 3664 - Understanding the Contract

The current implementation of EIP 3664 extends the ERC1155 standard. But EIP 3664 is designed to be compatible with any kind of Non-Fungible Token standard, not just ERC 721 or 1155.

The main interface of ERC 3664 defines certain functions and events that are used to attach, manage, and remove attribute


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Required interface of an ERC3664 compliant contract.
 */
interface ERC3664 {
    /**
     * @dev Emitted when new attribute type `attrId` are minted.
     */
    event AttributeCreated(
        uint256 indexed attrId,
        string name,
        string symbol,
        string uri
    );

    /**
     * @dev Emitted when `value` of attribute type `attrId` are attached to "to"
     * or removed from `from` by `operator`.
     */
    event TransferSingle(
        address indexed operator,
        uint256 from,
        uint256 to,
        uint256 indexed attrId,
        uint256 value
    );

    /**
     * @dev Equivalent to multiple {TransferSingle} events.
     */
    event TransferBatch(
        address indexed operator,
        uint256 from,
        uint256 to,
        uint256[] indexed attrIds,
        uint256[] values
    );

    /**
     * @dev Returns primary attribute type of owned by `tokenId`.
     */
    function primaryAttributeOf(uint256 tokenId)
        external
        view
        returns (uint256);

    /**
     * @dev Returns all attribute types of owned by `tokenId`.
     */
    function attributesOf(uint256 tokenId)
        external
        view
        returns (uint256[] memory);

    /**
     * @dev Returns the attribute type `attrId` value owned by `tokenId`.
     */
    function balanceOf(uint256 tokenId, uint256 attrId)
        external
        view
        returns (uint256);

    /**
     * @dev Returns the batch of attribute type `attrIds` values owned by `tokenId`.
     */
    function balanceOfBatch(uint256 tokenId, uint256[] calldata attrIds)
        external
        view
        returns (uint256[] memory);

    /**
     * @dev Set primary attribute type of owned by `tokenId`.
     */
    function setPrimaryAttribute(uint256 tokenId, uint256 attrId) external;

    /**
     * @dev Attaches `amount` value of attribute type `attrId` to `tokenId`.
     */
    function attach(
        uint256 tokenId,
        uint256 attrId,
        uint256 amount
    ) external;

    /**
     * @dev [Batched] version of {attach}.
     */
    function batchAttach(
        uint256 tokenId,
        uint256[] calldata attrIds,
        uint256[] calldata amounts
    ) external;
}

interface ERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

The first step to applying ERC-3664 to an EIP-1155 contract is to define one or multiple attributes that will be attached to the NFTs. These attributes are typically defined during token creation with the (_mint) and (_mintBatch) functions.


_mint(address account, uint256 id, uint256 amount, bytes data)

_mintBatch(address to, uint256[] ids, uint256[] amounts, bytes data)
  • to - The address to which the newly minted tokens are assigned.
  • id - Each ID represents a new configurable token type. Each token type can have its own metadata, supply, and other attributes.
  • amount - Specifies the Number of tokens to be minted.
  • data: - Additional data or signatures for some custom implementation.

The minted attributes are attached to the token/NFT in two ways:

  1. Either during its minting process
  2. OR dynamically applying to Update the already minted NFT.

EIP 3664 Contract Functions

There are three state-changing methods for attaching and defining the attributes.

These functions can only be called by the owner of the token or by someone who is authorized by the owner:

1) attach(): used for single a attribute


/**
 * @dev Attaches `amount` value of attribute type `attrId` to `tokenId`.
 */
function attach(
    uint256 tokenId,
    uint256 attrId,
    uint256 amount
) external;

2) batchAttach(): used to attach multiple composable attributes from a predetermined attribute list, specified via (calldata)


/**
 * @dev [Batched] version of {attach}.
 */
function batchAttach(
    uint256 tokenId,
    uint256[] calldata attrIds,
    uint256[] calldata amounts
) external;

3setPrimaryAttribute(): helps define the “PRIMARY” attribute of an NFT. Using this, an NFT can be categorized under a specific class or category.

Example: Weapon type attribute defines a class of weaponry attributes

EIP3664 Contract Events

Events allow tracking changes to attributes like new mints and value updates.

The EIP 3664 interface emits 3 events. Frontend dApps can listen for these events to automatically update UI/metadata.

AttributeCreatedemits when new attribute types are minted, with details like ID, name, symbol and URI.TransferSingleemits when an attribute value changes or when an attribute is attached or detached from an NFT.TransferBatchis emitted when multiple attribute values are changed (attached or removed).

EIP 3664 View Functions

The view functions enable querying the currently composed attributes of any NFT on-chain.

primaryAttributeOfReturns the primary attribute ID of the tokenattributesOfreturns an array of all attribute IDs owned by a token with a given (tokenId) of an NFT.balanceOfgets the value of a specific attribute type (attrId) owned by a (tokenId).balanceOfBatchSimilar to (balanceOf). But allows caller to check the values of multiple attributes at once.

An array of (attrId)s is provided.  The function returns an array of values corresponding to each id.

EIP 3664 Types of Attributes

EIP3664 primarily supports the following types of attribute extendability (OK):

  1. General Attributes
  2. Variable Attributes
  3. Transferable Attributes
  4. Upgradable Attributes
  5. Evolvable Attributes
  6. Text Attributes

Each of these attribute interfaces inherits some properties from the IERC3664Metadata interface.

Metadata Interface:

  • The IERC3664Metadata interface in the ERC 3664 protocol is an optional interface that allows for the management of metadata for attributes.

/**
 * @dev Interface for the optional metadata functions from the ERC3664 standard.
 */
interface IERC3664Metadata is IERC3664 {
    /**
     * @dev Returns the name of the attribute.
     */
    function name(uint256 attrId) external view returns (string memory);

    /**
     * @dev Returns the symbol of the attribute.
     */
    function symbol(uint256 attrId) external view returns (string memory);

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `attrId` attribute.
     */
    function attrURI(uint256 attrId) external view returns (string memory);
}


Every attribute that we are about to study extends from the IERC3664Metada interface to facilitate it

1) General Attribute

  • General Attributes refer to the attributes which are immutable in nature (attributes whose values cannot be modified).
  • Example: Creator Of an NFT, Pokemon type
  • These attributes are set while minting the NFT tokens.

2) Variable or updatable Attributes

  • Variable attributes are associated with numerical values that can change (increase or decrease) as per the events in the game or storyline.
  • Example: Health, No. of wins, Strength and Logic of Auditmax (from our Pokemon example), or combat power of a weapon.
  • The remove(uint256 tokenId, uint256 attrId): This function removes an attribute (attrId) from a token (tokenId). After this operation, the token will no longer have this attribute. Useful in a game scenario where a character might lose a certain ability or trait.
  • The values of a specific attribute (attrId) of a token (tokenId) can be varied by a certain amount using the increase and decrease functions.

/**
 * @dev Interface for the updatable functions from the ERC3664.
 */
interface IERC3664Updatable is IERC3664Metadata {
    /**
     * @dev Remove attribute type `attrId` from `tokenId`.
     */
    function remove(uint256 tokenId, uint256 attrId) external;

    /**
     * @dev Increases `amount` value of attribute type `attrId` to `tokenId`.
     */
    function increase(
        uint256 tokenId,
        uint256 attrId,
        uint256 amount
    ) external;

    /**
     * @dev Decreases `amount` value of attribute type `attrId` from `tokenId`.
     */
    function decrease(
        uint256 tokenId,
        uint256 attrId,
        uint256 amount
    ) external;
}

3) Transferable Attributes

  • As the name suggests, transferable attributes can be “transferred” between NFTs of the same collection.
  • This creates an opportunity to trade individual attributes in in-game marketplaces and associate scarcity/rarity with them.
  • Once a transfer is complete the NFT ID remains the same but its price and rarity may fluctuate.
  • isApproved(uint256 from, uint256 to, uint256 attrId): isApproved is a boolean return type function that checks if an attribute (attrId) is approved to be transferred from one tokenID (from) to another tokenID (to).
  • Function approve(uint256 from, uint256 to, uint256 attrId): approves an attribute (attrId) to be transferred from one token (from) to another token (to).
    → The function can only be called by the owner of from token.
    → Emits an AttributeApproval event.

/**
 * @dev Interface for the transferable functions from the ERC3664.
 */
interface IERC3664Transferable is IERC3664Metadata {
    /**
     * @dev Emitted when  attribute type `attrId` are approved to "to" from `from` by `operator`.
     */
    event AttributeApproval(
        address indexed operator,
        uint256 from,
        uint256 to,
        uint256 attrId
    );

    /**
     * @dev Returns true if `attrId` is approved to token `to` from token `from`.
     */
    function isApproved(
        uint256 from,
        uint256 to,
        uint256 attrId
    ) external view returns (bool);

    /**
     * @dev Approve attribute type `attrId` of token `from` to token `to` called by `from` holde
     * Emits an {AttributeApproval} event.
     */
    function approve(
        uint256 from,
        uint256 to,
        uint256 attrId
    ) external;

    /**
     * @dev Transfers attribute type `attrId` from token type `from` to `to`
     * Emits a {TransferSingle} event.
     */
    function transferFrom(
        uint256 from,
        uint256 to,
        uint256 attrId
    ) external;
}

4) Upgradable Attribute

  • Upgradable Attributes provide a discrete tier/level progression scale to attributes.
  • Example: Common, Rare, Epic, SuperUltraEpic OR simply, Level1,2 or 3 as in case of our Pokemon example.
  • mintWithLevel(uint256 attrId, string memory name, string memory symbol, string memory uri, uint8 maxLevel) function is used to mint a new attribute with a given attrId, name, symbol, URI and maxLevel of the attribute.
  • upgrade(uint256 tokenId, uint256 attrId, uint8 level) is a function that upgrades a specific attribute (attrId) of a token (tokenId) to a certain level. The function can only be called by the owner of the token.

interface IERC3664Upgradable is IERC3664Metadata {

	/*emitted when an attribute (attrId) of a token (tokenId) is upgraded
 to a certain level.*/
    event AttributeUpgraded(
        uint256 indexed tokenId,
        uint256 indexed attrId,
        uint8 level
    );
	/*returns the current level of a specific attribute (attrId) of a token (tokenId)*/
    function levelOf(uint256 tokenId, uint256 attrId)
        external
        view
        returns (uint8);

    function mintWithLevel(
        uint256 attrId,
        string memory name,
        string memory symbol,
        string memory uri,
        uint8 maxLevel
    ) external;

    function upgrade(
        uint256 tokenId,
        uint256 attrId,
        uint8 level
    ) external;
}


5) Evolvable Attribute:

  • In my opinion, this is the most interesting and game-changing attribute.
  • The evolvable attribute allows the NFT to have a chance of “evolving” into improved - powerful - rarer versions over time.
  • So, you might be having this question - by evolution, do you mean that our Auditchu Pokemon NFT will evolve into a bigger Auditmax Pokemon NFT?
  • The answer to it is, not exactly!
  • ERC-3664 provides evolution capabilities to the properties/attributes of an NFT (and not change the entire NFT as a whole). The protocol describes how NFTs can strengthen their attributes via evolution without transforming the root NFT into a completely different NFT.
    Example: the crown attribute (that got unlocked during evolution).
  • Function period(uint256 tokenId, uint256 attrId) - returns the period of time after which an attribute (attrId) of a token (tokenId) can evolve or attempt to evolve.
  • evolutive(uint256 tokenId, uint256 attrId) : On reaching the evolution eligibility the holder can call the (evolutive) function to attempt the evolution.
    The success of the evolution might not be certain and could depend on various factors defined by the NFT creators.
    Some ways could be to:
    → Trigeering the eligibility on reaching some threshold value (Ex: Level 10, magic skills reach - gold tier)
    → Hardcode evolution success chance.(60%)
    → Random number generation.
    → Increase the chance of success after each failure to guarantee eventual evolution.
    → Or some time-based unlocking technique, like block number.
    repair(uint256 tokenId, uint256 attrId)
    : Attributes that fail to evolve, need to repair themselves in order to be operable. The (repair) function is used to repair an attribute (attrId) of a token (tokenId), allowing it to evolve again after a failed evolution

/**
 * @dev Interface for the Evolvable functions from the ERC3664.
 */
interface IERC3664Evolvable is IERC3664Metadata {

/*emitted when an attribute (attrId) of a token (tokenId) becomes evolvable (status is true)
 or stops being evolvable (status is false).
*/
    event AttributeEvolvable(
        address indexed operator,
        uint256 tokenId,
        uint256 attrId,
        bool status
    );

/*emitted when an attribute (attrId) of a token (tokenId) is repaired*/
    event AttributeRepaired(
        address indexed operator,
        uint256 tokenId,
        uint256 attrId
    );

    function period(uint256 tokenId, uint256 attrId)
        external
        view
        returns (uint256);
	/*can only be called by the owner of the token or someone authorized by the owner*/
    function evolutive(uint256 tokenId, uint256 attrId) external;

/*can only be called by the owner of the token or someone authorized by the owner*/
    function repair(uint256 tokenId, uint256 attrId) external;
}


6) Text Attribute

  • Text-based attributes allow associating arbitrary text metadata with other attribute types. This enables NFTs to maintain unstructured text-based properties as well. Somewhat similar to the Loot Project NFTs.
  • The function attachWithText(uint256 tokenId, uint256 attrId, uint256 amount, bytes memory text) : attaches a text (text) to an attribute (attrId) of a token (tokenId). The amount parameter represents the value of the attribute.

interface IERC3664TextBased is IERC3664Metadata {

/*returns the text associated with a specific attribute (attrId) of a token (tokenId).*/

    function textOf(uint256 tokenId, uint256 attrId)
        external
        view
        returns (bytes memory);

    function attachWithText(
        uint256 tokenId,
        uint256 attrId,
        uint256 amount,
        bytes memory text
    ) external;

    function batchAttachWithTexts(
        uint256 tokenId,
        uint256[] calldata attrIds,
        uint256[] calldata amounts,
        bytes[] calldata texts
    ) external;
}

Finally, If we consider our Pokemon example, Auditmax’s attributes could be classified in the following way.

Attribute Value Type
WingsLevel Level 3 Upgradable
WingsImage image Transferable
Strength 80 / 100 Variable
Logic 85 / 100 Variable
Vision 90 /100 Variable
Aura - Transferable
Crown (new trait unlocked!) Level 1 Evolvable

Extending existing NFT collections with EIP 3664

  • Extending existing NFT collections to 3664 does not require any modifications in their core implementation.
  • ERC-3664 can simply be added on top of existing NFTs to augment them with additional attributes.
  • Specifically for ERC-721 and ERC-1155 NFTs, ERC-3664 attributes can be added without needing to change the base contracts.
  • The base ERC-721 and ERC-1155 implementations remain completely intact.

Thus, ERC-3664 just enables defining customizable - supplemental attributes through its own contract.

Conclusion

Congrats on making it to the finish line! 🏁🏁🏁 Now you successfully know about a whole new NFT standard.

We started off by understanding what is EUIP 3664 all about, Why should we study it, How does it compares with other NFT standards. Then, we imbibed the concept of composible Attributes with the help of our Pokemon example. Auditmax is a chad!⚡

Finally, we deep-dived into the contract, understood its flow, and studied the nuances of various attribute types EIP 3664 offers for creating the ultimate, customizable, evolving non-fungible asset.

If you’d like to learn more about smart contract development, make sure to check out Cyfrin Updraft - the most comprehensive learning platform out there for smart contract engineers.

Happy BUILDING 💪

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.