ZRC | Title | Status | Type | Author | Created (yyyy-mm-dd) | Updated (yyyy-mm-dd) |
---|---|---|---|---|---|---|
6 | Non-Fungible Token Standard | Implemented | Standard | Neuti Yoo [email protected] Jun Hao Tan [email protected] |
2021-10-01 | 2022-03-09 |
- I. What are NFTs and NFT royalties?
- II. Abstract
- III. Motivation
- IV. Specification
- V. Implementations
- VI. References
- VII. Copyright
An NFT, or Non-Fungible Token is a digital asset. Unlike fungible tokens, each token is unique and non-interchangeable.
NFT royalties give an NFT creator or rights holder a percentage of the sale price each time the NFT is sold or re-sold.
ZRC-6 defines a new minimum interface of an NFT smart contract while improving upon ZRC-1.
The main advantages of this standard are:
-
ZRC-6 standardizes royalty information retrieval with a percentage-based royalty fee model and unit-less royalty payments to a single address. Funds will be paid for secondary sales only if a marketplace chooses to implement royalty payments. Marketplace contracts should transfer the actual funds.
-
ZRC-6 includes base URI to support token URI with the concatenation of base URI and token ID. A token URI is an IPFS, HTTP, or data URL. This URL must return a JSON blob of data with the metadata for the NFT when queried. ZRC-7 covers metadata and token URI in detail.
-
ZRC-6 is designed for remote state read (
x <- & c.f
) such that logic to get data from a ZRC-6 contract is straightforward. ZRC-6 exposes immutable parameters via mutable fields and includes only transitions that mutate the state of the contract. -
ZRC-6 is designed for failure. Therefore minting, burning, and token transfers are pausable.
-
ZRC-6 features batch operations for minting, burning and token transfers such that only a single transaction is required.
-
ZRC-6 features a single transition for token transfer with destination validation. The transition can be called by a token owner, a spender, or an operator. ZRC-6 prevents transferring tokens to the zero address or the address of a ZRC-6 contract.
-
ZRC-6 is compatible with ZRC-X since every callback name is prefixed with
ZRC6_
.
-
Many of the largest NFT marketplaces have implemented incompatible royalty payment solutions.
-
The concatenated token URI can reduce gas cost and is more optimal for contract state.
-
Using callbacks to get data can complicate the logic easily. Unlike immutable parameters, mutable fields are available for remote state read.
-
Without an emergency stop mechanism, it's hard to respond to bugs and vulnerabilities gracefully.
-
Without batch operations, it can be very inefficient to transfer or mint multiple tokens with multiple transactions.
-
ZRC-1 includes
Transfer
andTransferFrom
for the token transfer. The two transitions have the same type signature and the only difference is the access control. This has added unnecessary complexity. ZRC-1 does not validate the destination for token transfer and it is not safe. -
The ZRC-1 and ZRC-2 contracts can share the same callback names. Contracts must have unique names for callback transitions.
Name | Type | Description |
---|---|---|
initial_contract_owner |
ByStr20 |
Address of contract owner. It must not be the zero address i.e., 0x0000000000000000000000000000000000000000 . |
initial_base_uri |
String |
Base URI. e.g. https://creatures-api.zilliqa.com/api/creature/ . |
name |
String |
NFT name. It must not be an empty string. |
symbol |
String |
NFT symbol. It must not be an empty string. |
Name | Type | Description | Required |
---|---|---|---|
is_paused |
Bool |
True if the contract is paused. Otherwise, False . is_paused defaults to False . |
|
contract_owner |
ByStr20 |
Address of the contract owner. contract_owner defaults to initial_contract_owner . |
✓ |
royalty_recipient |
ByStr20 |
Address to send royalties to. royalty_recipient defaults to initial_contract_owner . |
|
royalty_fee_bps |
Uint128 |
Royalty fee BPS. For example, 1 = 0.01%, 10000 = 100%. royalty_fee_bps ranges from 1 to 10000 and defaults to 1000 . When calculating the royalty amount, you must only use division to avoid integer overflow. royalty amount = sale price ÷ ( 10000 ÷ royalty fee bps ) For example, if royalty_fee_bps is 1000 (10%) and sale price is 999 , royalty amount is 99 . |
|
base_uri |
String |
Base URI. For example, if base_uri is https://creatures-api.zilliqa.com/api/creature/ and token_id is 1 , token URI is https://creatures-api.zilliqa.com/api/creature/1 . If there is no specific token URI for a token in token_uris , the concatenated token URI is the token URI. If every token has its own token URI, base_uri can be an empty string. base_uri defaults to initial_base_uri . This field must not be mutated unless there is a strong reason. |
✓ |
token_uris |
Map Uint256 String |
Mapping from token ID to its specific token URI. When it is required to set a specific token URI per token, use this field. If there is a specific token URI for a token in token_uris , the URI is the token URI. If every token does not have its own token URI, token_uris can be an empty map. |
✓ |
minters |
Map ByStr20 Bool |
Set of minters. | ✓ |
token_owners |
Map Uint256 ByStr20 |
Mapping from token ID to its owner. | ✓ |
spenders |
Map Uint256 ByStr20 |
Mapping from token ID to a spender. | ✓ |
operators |
Map ByStr20 (Map ByStr20 Bool) |
Mapping from token owner to set of operators. | ✓ |
token_id_count |
Uint256 |
The total number of tokens minted. Defaults to 0 . |
✓ |
balances |
Map ByStr20 Uint256 |
Mapping from token owner to the number of existing tokens. | ✓ |
total_supply |
Uint256 |
The total number of existing tokens. Defaults to 0 . |
✓ |
token_name |
String |
Token name. Defaults to name . This field is for remote state read. This field must not be mutated. |
✓ |
token_symbol |
String |
Token symbol. Defaults to symbol . This field is for remote state read. This field must not be mutated. |
✓ |
contract_ownership_recipient |
ByStr20 |
Address of the contract ownership recipient. Defaults to zero address. |
Name | Description | Required |
---|---|---|
contract_owner |
The contract owner can:
|
✓ |
royalty_recipient |
The royalty recipient gets a royalty amount each time the NFT is sold or re-sold. Initially, the royalty recipient is the contract owner. | |
minter |
A minter can mint tokens. Initially, the contract owner is a minter. | ✓ |
token_owner |
Each token has an token owner. A token owner can:
|
✓ |
spender |
On behalf of the token owner, a spender can transfer a token. There can only be one spender per token at any given time. | ✓ |
operator |
On behalf of the token owner, an operator can:
|
✓ |
contract_ownership_recipient |
If the contract owner wants to transfer contract ownership to someone, the contract owner can set the person as the contract ownership recipient. The contract ownership recipient can accept the contract ownership and become the new contract owner. |
Transition | contract_owner |
minter |
token_owner |
spender |
operator |
contract_ownership_recipient |
---|---|---|---|---|---|---|
Pause |
✓ | |||||
Unpause |
✓ | |||||
SetRoyaltyRecipient |
✓ | |||||
SetRoyaltyFeeBPS |
✓ | |||||
SetBaseURI |
✓ | |||||
Mint |
✓ | |||||
BatchMint |
✓ | |||||
Burn |
✓ | ✓ | ||||
BatchBurn |
✓ | ✓ | ||||
AddMinter |
✓ | |||||
RemoveMinter |
✓ | |||||
SetSpender |
✓ | ✓ | ||||
AddOperator |
✓ | |||||
RemoveOperator |
✓ | |||||
TransferFrom |
✓ | ✓ | ✓ | |||
BatchTransferFrom |
✓ | ✓ | ✓ | |||
SetContractOwnershipRecipient |
✓ | |||||
AcceptContractOwnership |
✓ |
The NFT contract must define the following constants for use as error codes for the Error
exception.
Name | Type | Code | Description | Required |
---|---|---|---|---|
NotPausedError |
Int32 |
-1 |
Emit when the contract is not paused. | |
PausedError |
Int32 |
-2 |
Emit when the contract is paused. | |
SelfError |
Int32 |
-3 |
Emit when the address is self. | ✓ |
NotContractOwnerError |
Int32 |
-4 |
Emit when the address is not a contract owner. | ✓ |
NotTokenOwnerError |
Int32 |
-5 |
Emit when the address is not a token owner. | ✓ |
NotMinterError |
Int32 |
-6 |
Emit when the address is not a minter. | ✓ |
NotOwnerOrOperatorError |
Int32 |
-7 |
Emit when the address is neither a token owner nor a token operator. | ✓ |
MinterNotFoundError |
Int32 |
-8 |
Emit when the minter is not found. | ✓ |
MinterFoundError |
Int32 |
-9 |
Emit when the minter is found. | ✓ |
SpenderFoundError |
Int32 |
-10 |
Emit when the spender is found. | ✓ |
OperatorNotFoundError |
Int32 |
-11 |
Emit when the operator is not found. | ✓ |
OperatorFoundError |
Int32 |
-12 |
Emit when the operator is found. | ✓ |
NotAllowedToTransferError |
Int32 |
-13 |
Emit when _sender is not allowed to transfer the token. |
✓ |
TokenNotFoundError |
Int32 |
-14 |
Emit when the token is not found. | ✓ |
InvalidFeeBPSError |
Int32 |
-15 |
Emit when the fee bps does not range from 1 to 10000 . |
|
ZeroAddressDestinationError |
Int32 |
-16 |
Emit when the destination is the zero address. | ✓ |
ThisAddressDestinationError |
Int32 |
-17 |
Emit when the destination is _this_address . |
✓ |
NotContractOwnershipRecipientError |
Int32 |
-18 |
Emit when the address is not a contract ownership recipient. |
Pauses the contract. Use this only if things are going wrong ('circuit breaker').
Requirements:
- The contract must not be paused. Otherwise, it must throw
PausedError
. _sender
must be the contract owner. Otherwise, it must throwNotContractOwnerError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_PauseCallback |
Provide the sender a boolean for whether the contract is paused or not. | is_paused : Bool True if paused, otherwise False |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
Pause |
The contract has been paused. |
|
Unpauses the contract.
Requirements:
- The contract must be paused. Otherwise, it must throw
NotPausedError
. _sender
must be the contract owner. Otherwise, it must throwNotContractOwnerError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_UnpauseCallback |
Provide the sender a boolean for whether the contract is paused or not. | is_paused : Bool True if paused, otherwise False |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
Unpause |
The contract has been unpaused. |
|
Sets to
as the royalty recipient.
Arguments:
Name | Type | Description |
---|---|---|
to |
ByStr20 |
Address that royalties are sent to. |
Requirements:
_sender
must be the contract owner. Otherwise, it must throwNotContractOwnerError
.to
must not be the zero address. Otherwise, it must throwZeroAddressDestinationError
.to
must not be_this_address
. Otherwise, it must throwThisAddressDestinationError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_SetRoyaltyRecipientCallback |
Provide the sender the address of royalty recipient. | to : ByStr20 Address of the royalty recipient |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
SetRoyaltyRecipient |
Royalty recipient has been updated. |
|
Sets fee_bps
as royalty fee bps.
Arguments:
Name | Type | Description |
---|---|---|
fee_bps |
Uint128 |
Royality fee BPS. e.g. 1 = 0.01%, 10000 = 100%. |
Requirements:
_sender
must be the contract owner. Otherwise, it must throwNotContractOwnerError
.fee_bps
must range from1
to10000
. Otherwise, it must throwInvalidFeeBPSError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_SetRoyaltyFeeBPSCallback |
Provide the sender the royalty fee BPS. | royalty_fee_bps : Uint128 Royalty Fee BPS |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
SetRoyaltyFeeBPS |
Royalty fee BPS has been updated. |
|
Sets uri
as the base URI. Use this only if there is a strong reason to change the base_uri
.
Arguments:
Name | Type | Description |
---|---|---|
uri |
String |
Base URI. |
Requirements:
_sender
must be the contract owner. Otherwise, it must throwNotContractOwnerError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_SetBaseURICallback |
Provide the sender the base URI. | base_uri : String Base URI |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
SetBaseURI |
Base URI has been updated. |
|
Mints a token with a specific token_uri
and transfers it to to
.
Pass empty string to token_uri
to use the concatenated token URI. i.e. <base_uri><token_id>
.
Arguments:
Name | Type | Description |
---|---|---|
to |
ByStr20 |
Address of the recipient of the token to be minted. |
token_uri |
String |
URI of a token. |
Requirements:
- The contract must not be paused. Otherwise, it must throw
PausedError
. to
must not be the zero address. Otherwise, it must throwZeroAddressDestinationError
to
must not be_this_address
. Otherwise, it must throwThisAddressDestinationError
_sender
must be a minter. Otherwise, it must throwNotMinterError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_RecipientAcceptMint |
Dummy callback to prevent invalid recipient contract. | |
_tag |
ZRC6_MintCallback |
Provide the sender the address of token recipient and token ID. |
|
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
Mint |
Token has been minted with a specific URI. |
|
Mints multiple tokens with token_uri
s and transfers them to multiple to
s.
Pass empty string to token_uri
to use the concatenated token URI. i.e. <base_uri><token_id>
.
Arguments:
Name | Type | Description |
---|---|---|
to_token_uri_pair_list |
List (Pair ByStr20 String) |
List of Pair (to , token_uri )
|
Requirements:
- The contract must not be paused. Otherwise, it must throw
PausedError
. to_list
must not include the zero address. Otherwise, it must throwZeroAddressDestinationError
to_list
must not include_this_address
. Otherwise, it must throwThisAddressDestinationError
_sender
must be a minter. Otherwise, it must throwNotMinterError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_BatchMintCallback |
An empty callback. |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
BatchMint |
Multiple tokens have been minted with token URIs. |
|
Destroys token_id
.
Arguments:
Name | Type | Description |
---|---|---|
token_id |
Uint256 |
Unique ID of an existing token to be destroyed. |
Requirements:
- The contract must not be paused. Otherwise, it must throw
PausedError
. token_id
must exist. Otherwise, it must throwTokenNotFoundError
._sender
must be a token owner or an operator. Otherwise, it must throwNotOwnerOrOperatorError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_BurnCallback |
Provide the sender the burn address and token ID. |
|
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
Burn |
Token has been burned. |
|
Destroys token_id_list
.
Arguments:
Name | Type | Description |
---|---|---|
token_id_list |
List Uint256 |
List of unique IDs of the NFT to be destroyed. |
Requirements:
- The contract must not be paused. Otherwise, it must throw
PausedError
. token_id
must exist. Otherwise, it must throwTokenNotFoundError
._sender
must be a token owner or an operator. Otherwise, it must throwNotOwnerOrOperatorError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_BatchBurnCallback |
An empty callback. |
Events:
Equivalent to multiple Burn
events.
Adds minter
.
Arguments:
Name | Type | Description |
---|---|---|
minter |
ByStr20 |
Address to be added as minter. |
Requirements:
_sender
must be the contract owner. Otherwise, it must throwNotContractOwnerError
.minter
must not be already a minter. Otherwise, it must throwMinterFoundError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_AddMinterCallback |
Provide the sender the address of the minter. |
|
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
AddMinter |
Minter has been added. |
|
Removes minter
.
Arguments:
Name | Type | Description |
---|---|---|
minter |
ByStr20 |
Address to be removed from minter. |
Requirements:
_sender
must be the contract owner. Otherwise, it must throwNotContractOwnerError
.minter
must be already a minter. Otherwise, it must throwMinterNotFoundError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_RemoveMinterCallback |
Provide the sender the address that has been removed. |
|
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
RemoveMinter |
Minter has been removed. |
|
Sets spender
for token_id
. To remove spender
for a token, use zero_address
i.e., 0x0000000000000000000000000000000000000000
Arguments:
Name | Type | Description |
---|---|---|
spender |
ByStr20 |
Address to be set as a spender for a given token. |
token_id |
Uint256 |
Unique ID of an existing token. |
Requirements:
token_id
must exist. Otherwise, it must throwTokenNotFoundError
._sender
must be a token owner or an operator. Otherwise, it must throwNotOwnerOrOperatorError
._sender
must not bespender
. Otherwise, it must throwSelfError
.spender
must not be already a spender. Otherwise, it must throwSpenderFoundError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_SetSpenderCallback |
Provide the sender the address of the spender and token ID. |
|
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
SetSpender |
Spender has been updated. |
|
Adds operator
for _sender
.
Arguments:
Name | Type | Description |
---|---|---|
operator |
ByStr20 |
Address to be added as operator. |
Requirements:
_sender
must be the token owner. Otherwise, it must throwNotTokenOwnerError
._sender
must not beoperator
. Otherwise, it must throwSelfError
.operator
must not be already an operator. Otherwise, it must throwOperatorFoundError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_AddOperatorCallback |
Provide the sender the address of the operator. | operator : ByStr20 Address that has been added |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
AddOperator |
Operator has been added. |
|
Removes operator
for _sender
.
Arguments:
Name | Type | Description |
---|---|---|
operator |
ByStr20 |
Address to be removed from operator. |
Requirements:
operator
must be already an operator. Otherwise, it must throwOperatorNotFoundError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_RemoveOperatorCallback |
Provide the sender the address that has been removed. | operator : ByStr20 Address that has been removed |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
RemoveOperator |
Operator has been removed. |
|
Transfers token_id
from the token owner to to
.
Arguments:
Name | Type | Description |
---|---|---|
to |
ByStr20 |
Recipient address of the token. |
token_id |
Uint256 |
Unique ID of the token to be transferred. |
Requirements:
- The contract must not be paused. Otherwise, it must throw
PausedError
. to
must not be the zero address. Otherwise, it must throwZeroAddressDestinationError
.to
must not be_this_address
. Otherwise, it must throwThisAddressDestinationError
.token_id
must exist. Otherwise, it must throwTokenNotFoundError
._sender
must be a token owner, spender, or operator. Otherwise, it must throwNotAllowedToTransferError
._sender
must not beto
. Otherwise, it must throwSelfError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_RecipientAcceptTransferFrom |
Dummy callback to prevent invalid recipient contract. |
|
_tag |
ZRC6_TransferFromCallback |
Provide the sender the result of the token transfer. |
|
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
TransferFrom |
Token has been transferred. |
|
Transfers multiple token_id
to multiple to
.
Arguments:
Name | Type | Description |
---|---|---|
to_token_id_pair_list |
List (Pair ByStr20 Uint256) |
List of Pair (to , token_id )
|
Requirements:
- The contract must not be paused. Otherwise, it must throw
PausedError
. to
must not be the zero address. Otherwise, it must throwZeroAddressDestinationError
.to
must not be_this_address
. Otherwise, it must throwThisAddressDestinationError
.token_id
must exist. Otherwise, it must throwTokenNotFoundError
._sender
must be a token owner, spender, or operator. Otherwise, it must throwNotAllowedToTransferError
._sender
must not beto
. Otherwise, it must throwSelfError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_BatchTransferFromCallback |
An empty callback. |
Events:
Equivalent to multiple TransferFrom
events.
Sets to
as the contract ownership recipient. To reset contract_ownership_recipient
, use zero_address
i.e., 0x0000000000000000000000000000000000000000
.
Arguments:
Name | Type | Description |
---|---|---|
to |
ByStr20 |
Address to be set as the contract ownership recipient. |
Requirements:
_sender
must be the contract owner. Otherwise, it must throwNotContractOwnerError
._sender
must not beto
. Otherwise, it must throwSelfError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_SetContractOwnershipRecipientCallback |
Provide the sender the address of the contract ownership recipient. | to : ByStr20 Address of the contract ownership recipient |
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
SetContractOwnershipRecipient |
The contract ownership recipient has been updated. |
|
Sets contract_ownership_recipient
as the contract owner.
Requirements:
_sender
must be the contract ownership recipient. Otherwise, it must throwNotContractOwnershipRecipientError
.
Messages:
Name | Description | Callback Parameters | |
---|---|---|---|
_tag |
ZRC6_AcceptContractOwnershipCallback |
Provide the sender the result of the contract ownership transfer. |
|
Events:
Name | Description | Event Parameters | |
---|---|---|---|
_eventname |
AcceptContractOwnership |
Contract ownership has been transferred. |
|
zrc6.scilla - a reference implementation
- includes test cases written using Zilliqa Isolated Server and Jest
- MIT licensed
- Ethereum Smart Contract Best Practices
- EIP-721: Non-Fungible Token Standard
- EIP-2981: NFT Royalty Standard
- EIP-1155: Multi Token Standard
- OpenZeppelin ERC721.sol Implementation
- OpenZeppelin IERC2981.sol Implementation
- OpenZeppelin ERC1155.sol Implementation
- OpenZeppelin Pausable.sol Implementation
- OpenSea - Metadata Standards
- ZRC-1: Standard for Non Fungible Tokens
- ZRC issue #88 - ZRC contracts must have unique names for callback transitions
- SWC Registry - Smart Contract Weakness Classification and Test Cases
- RFC 1738 - Uniform Resource Locators (URL)
- RFC 2397 - The "data" URL scheme
- ZRC-7: NFT Metadata Standard
Copyright and related rights waived via CC0.