Skip to main content

ERC721 Preset (Recommended)

The Immutable ERC721 preset is an extension of the Immutable ERC721 Base contract that contains functionality for using one of two efficient methods for minting batches of tokens. This is Immutable's recommended preset contract for game development


Key functionalityRecommended usage
  • Minting - single and batch
  • Minting API compatible
  • Metadata setting and interrogation
  • Royalty fee calculation and on-chain royalty enforcement
  • Restricted role-based functionality
  • Launching your ERC721 collection on Immutable zkEVM

Installation, usage and deployment

Installation

The preset contract is contained within the contracts repository which can be easily added to your project via:

npm install @imtbl/contracts

We recommend using existing frameworks such as Hardhat or Foundry in order to set up your own smart contract development environment from scratch.

Usage

The contracts repository can be used to:

  • develop and deploy your own smart contract using one of Immutable's presets
  • interact with your already deployed Immutable preset contract using the Typescript client

Smart contract development

pragma solidity ^0.8.19;
import '@imtbl/contracts/contracts/token/erc721/preset/ImmutableERC721.sol';

contract MyERC721 is ImmutableERC721 {
constructor(
address owner,
string memory name,
string memory symbol,
string memory baseURI,
string memory contractURI,
address royaltyAllowlist,
address _receiver,
uint96 _feeNumerator
)
ImmutableERC721(
owner,
name,
symbol,
baseURI,
contractURI,
royaltyAllowlist,
_receiver,
_feeNumerator
)
{}
}

Interacting with deployed contracts

Read the full guide for interacting with deployed contracts here.

Deployment

To deploy smart contracts on Immutable zkEVM, see the deploying smart contracts quickstart guide.

Configure Hardhat

Update your project's hardhat.config.js/ts file with the following code. This file is located in your smart contract repository.

solidity: {
version: "0.8.17",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},

Functionality and design

Whilst the contract embeds the recommended functionality, it can also be extended or refactored to fit the developer's needs. With a caveat of extending functionality may introduce vulnerabilities inside your smart contracts, care should be taken.

Functionality

Minting

Minting an ERC721 NFT creates a unique asset on the blockchain specific to the contract. The preset uses one of two methods to mint tokens:

  • When minting by quantity, the contract uses the ERC721Psi method to efficiently mint multiple tokens. This method mints tokens starting at the value returned by bulkMintThreshold, which can be overridden by subclassing the preset.
  • When minting by ID, the contract uses the internal ERC721 _mint function. The IDs specified for tokens minted this way must be less than the value returned by bulkMintTreshold.

By default, bulkMintThreshold returns the value 2 ** 128, or 340282366920938463463374607431768211456.

Both methods are permissioned. The mint function allows users with the minter role to mint an amount of tokens to a specified address (for further information about permissions, refer to the access control section below).

Burning

Burning destroys an ERC721 NFT.

Batch minting and burning

The preset contract provides several methods for efficiently minting and burning batches of tokens:

  • mintBatch: mint a batch of tokens with caller-specified IDs
  • mintBatchByQuantity: mint a batch of tokens with sequential IDs
  • burnBatch: burn a batch of tokens, specifying the ID of each

Safe minting and burning

The ERC721 Preset contract includes safe versions of all minting and burning methods. These methods cost additional gas, but perform additional checks to ensure the safety of the operation:

  • The safe minting methods ensure that the receiver of the transaction is either a wallet or a contract that can receive ERC721 tokens. If the destination address of a safe mint call is a contract, the contract must implement the IERC721Receiver.onERC721Received method for the transaction to complete successfully.
  • The safe burning methods ensure that the owner of the token(s) to be burned matches the owner provided as a method argument. This can be used to prevent certain race conditions that can arise in crafting workflows.

Metadata

Metadata is an integral part of an NFT collection, defining its unique attributes and utility; the collection owner defines this data. Metadata in a collection is referenced by its universal resource identifier (URI). The metadata source may be stored either on-chain or off-chain, supporting different schemas.

Metadata can be a mutable object and is often updated by the collection owners. This may relate to the specific utility or mechanics of the collection and means that services integrating with the collection need to have the latest metadata in their system.

The Immutable preset contract supports metadata at the token and collection level. The base URI and contract URI are set in the preset's constructor, and the preset also exposes functions to update these.

Types

There are three canonical metadata URIs inside an ERC721 collection:

  1. Base URI: single metadata URI that is the common denominator across all token IDs

  2. Token URI : metadata URI specific to a single token ID

  3. Contract URI: single metadata URI that is collection-wide. E.g. this is used by marketplaces to provide store-front information

Storage

Storing metadata off-chain means that the data is stored outside the contract's storage in an off-chain location. A pointer to the off-chain metadata source is stored within the contract's storage.

Storing metadata on-chain means the data is stored inside the contract’s storage. The format of on-chain metadata is often, but not always, in JSON format, which should be handled by integrating systems accordingly.

On-chain metadata is still a pattern that isn’t as widely adopted as off-chain metadata in the ecosystem and thus has less support and tooling (as well as the increased gas cost), lacking the richness that off-chain storage can provide. This makes off-chain metadata storage the recommended approach.

Schemas

Metadata schemas provide a standard that allows applications that integrate with your ERC721 contract to know what the expected returned metadata is and how to interpret it. OpenSea’s metadata schema, which is structured according to the official ERC721 metadata standard is the most adopted schema by the community for defining and understanding metadata within the ecosystem, which is supported for both on and off-chain metadata. The schema can be found here. For this reason, it is the recommended schema when defining metadata (this applies for the schema for both the collection URI and token URI).

The format for used in OpenSea's schema is JSON. An example is shown below:

{
"description": ""
"external_url": "",
"image": "",
"name": "",
"attributes": []
}

Traits

The schema used by OpenSea supports attributes that allow creators to provide additional information about their NFTs. Within these attributes, are traits which allow you to describe the attributes of an NFT as well as format their display. The three different trait types are numeric, date and string. For example, one may use these traits to describe a hero NFT as follows:

{
"attributes": [
{
"trait_type": "Hero",
"value": "Fighter"
},
{
"display_type": "number",
"trait_type": "Strength",
"value": 2
}
]
}

For further information on using attributes and their traits in your metadata, refer to here.

Access control

The preset contract inherits OpenZeppelin's AccessControl module, which allows you to define role-based access modifiers for particular functions, such as minting.

Access control defines the ontology of roles inside a contact. A role is a structure with a list of members and a defined admin role, with each member within this structure having the admin role. For further information on access control, refer to OpenZeppelin's documentation here.

The modifier onlyRole() is used to restrict functionality to admins of that role. For example, only addresses with the MINTER role may execute the function mint. The DEFAULT_ADMIN_ROLE is the other role inside the preset which has restricted functionality. These functions include:

  1. setBaseURI allows the admin to update the base URI.

  2. setContractURI allows the admin to update the contract URI.

  3. grantMinterRole allows the admin to grant the MINTER role to the user.

  4. transferOwnership allows the admin to set a new owner inside the contract.

The ontology of an owner is within the preset contract, for display purposes (e.g. a marketplace storefront will often display metadata about the collection, for example, the collection owner signalled by the contract owner). The owner is set on contract deployment and is the same address set for the DEFAULT_ADMIN_ROLE. Ownership may be transferred to other addresses, as long as they are a member of the DEFAULT_ADMIN_ROLE. Note that this implementation is not OpenZeppelin's Ownable standard. For more information on Ownable, refer to here.

Royalty specification and retrieval

EIP-2981 defines the specification and retrieval of royalties. The present contract defines the royalty specification as a fraction of the sale price. That is, given a tokenID and a salePrice, the royalty amount returned is a fraction of the sale price, specified in basis points. This is what will be returned on royalty retrieval for a tokenId. The EIP can be found here.

The preset's implementation of EIP-2981 does not include any bespoke royalty calculation logic, and returns a fixed percentage of the sale price. The royalty percentage is defined on contract creation, and the preset does not expose a function to modify this afterwards.

Operator allowlist: Royalty and Protocol fee enforcement

All collections must use Immutable's operator allowlist to protect a collection's royalties.

The Operator Allowlist Address value in the table below should be used as the operatorAllowlist parameter.

Chain NameChain IDOperator Allowlist Address
imtbl-zkevm-testneteip155:134730x6b969FD89dE634d8DE3271EbE97734FEFfcd58eE
imtbl-zkevm-mainneteip155:133710x5F5EBa8133f68ea22D712b0926e2803E78D89221

Immutable's preset contracts incorporate an operator allowlist to safeguard game studios' royalties and Immutable's protocol fee during secondary market trading.

This allowlist protects against vampire attacks, preventing unauthorized trading on orderbooks or settlement smart contracts that might bypass content creators' revenues (i.e., royalties) and Immutable's 2% fee.

The enforcement of the operator allowlist is achieved in the following function call within the OperatorAllowlistEnforced.sol file, found in Immutable's contracts repository:

function _setOperatorAllowlistRegistry(address _operatorAllowlist) internal {
if (!IERC165(_operatorAllowlist).supportsInterface(type(IOperatorAllowlist).interfaceId)) {
revert AllowlistDoesNotImplementIOperatorAllowlist();
}

All game studios are required to inherit OperatorAllowlistEnforced.sol to ensure that their royalties and Immutable's fees are properly respected during secondary market trading. For more details on royalty enforcements, refer to the article.

Balance

This preset contract utilizes the ERC721 standard balanceOf() function.

Additionally, it incorporates two batch minting functions outlined here:

  1. mintBatch()
  2. mintBatchByQuantity() - Most gas efficient.

It's important to note that due to the gas efficiencies offered by mintBatchByQuantity(), the balanceOf() function will return incomplete results if more than approximately 80,000 tokens are minted using mintBatchByQuantity() within the collection.

Should you require the simultaneous use of both mintBatchByQuantity() and the balanceOf() function, please reach out to your Immutable representative.

Permits

This present contract supports permits. For more information on permits please see our Permits product guide.

Interface

FunctionDescription
grantMinterRole(user)Grant a user the minter role, restricted to admins
mint(to, tokenId)Allows minter role to mint a token with ID tokenId to to
mintBatch(idMints[])Allows minter role to mint a batch of tokens with specified IDs (idMints.tokenIds) to a batch of specified wallets (idMints.to)
mintByQuantity(to, quantity)Allows minter role to mint a number of tokens to a specified wallet
mintBatchByQuantity(mints[])Allows minter role to mint a batch of tokens (mints.quantity) to a batch of specified wallets (mints.to)
burn(tokenId)Burns the token with ID tokenId
burnBatch(tokenIds[])Burns the tokens with IDs specified in tokenIds
safeMint(to, tokenId)Allows minter role to safely mint a token with ID tokenId to to
safeMintBatch(idMints[])Allows minter role to safely mint a batch of tokens with specified IDs (idMints.tokenIds) to a batch of specified wallets (idMints.to)
safeMintByQuantity(to, quantity)Allows minter role to safely mint a number of tokens to a specified wallet
safeMintBatchByQuantity(mints[])Allows minter role to safely mint a batch of tokens (mints.quantity) to a batch of specified wallets (mints.to)
safeBurn(tokenId)Safely burns the token with ID tokenId
safeBurnBatch(tokenIds[])Safely burns the tokens with IDs specified in tokenIds

Immutable ERC721 Base

FunctionDescription
baseURI()Returns the base URI
contractURI()Returns the contract URI
owner()Returns the current owner of the contract
setBaseURI(baseURI_)Set the base URI, restriced to admins
setContractURI(_contractURI)Set the contract URI, restricted to admins
transferOwnership(address newOwner)Transfer contract ownership, updating contract owner, restricted to current owner
supportsInterface(interfaceId)Returns whether contract supports supplied interface ID

Inheritance

Immutable contracts