AEX: 141
Title: Non-Fungible Token Standard
Author: Arjan van Eersel <arjan@toqns.com> (@zkvonsnarkenstein), Marco Walz (@marc0olo), Philipp (@thepiwo), Rogerio (@jyeshe)
License: ISC
Discussions-To: https://forum.aeternity.com/t/aeternity-nft-token-standard/9781
Status: Review
Type: Interface
Created: 2021-09-11
A standard implementation of non-fungible tokens for the æternity ecosystem. Initially, the design goal of the primary interface was to be as compatible with ERC-721 as possible, so that anyone who can work with ERC-721 can work with this interface. However, specifically when it comes to dealing with metadata a decision was taken to provide contract developers as much flexibility as possible.
Core differences to the well-known ERC-721 standard:
- Unsafe transactions are not supported. Therefore, all transactions are ought to be safe
- Usage of zero-address for minting or burning is avoided. Thus, explicit events for minting and burning have been defined
- Token transfers do not require the (owner) address to be passed. Only the recipient address needs to be provided to the entrypoint
- High flexibility when it comes to dealing with metadata is provided
The following standard describes standard interfaces for non-fungible tokens. The proposal contains a primary interface and secondary interfaces (extensions) for optional functionality that not everyone might need.
contract interface IAEX141 =
datatype metadata_type = URL | OBJECT_ID | MAP
datatype metadata = MetadataIdentifier(string) | MetadataMap(map(string, string))
record meta_info =
{ name: string
, symbol: string
, base_url: option(string)
, metadata_type : metadata_type }
datatype event
= Transfer(address, address, int)
| Approval(address, address, int, string)
| ApprovalForAll(address, address, string)
entrypoint aex141_extensions : () => list(string)
entrypoint meta_info : () => meta_info
entrypoint metadata : (int) => option(metadata)
entrypoint total_supply : () => int
entrypoint balance : (address) => option(int)
entrypoint owner : (int) => option(address)
stateful entrypoint transfer : (address, int, option(string)) => unit
stateful entrypoint transfer_to_contract : (int) => unit
stateful entrypoint approve : (address, int, bool) => unit
stateful entrypoint approve_all : (address, bool) => unit
entrypoint get_approved : (int) => option(address)
entrypoint is_approved : (int, address) => bool
entrypoint is_approved_for_all : (address, address) => bool
Returns a hardcoded list of all implemented extensions on the deployed contract.
entrypoint aex141_extensions() : list(string)
Returns meta information associated with the contract.
Note:
- The
base_urlis optional and is only intended to be used if themetadata_typeisURL. As known from ERC-721 this can be used to resolve metadata for a specific NFT which can be fetched from an URL based on the token id - The
metadata_typeMUST be defined on contract level and MUST NOT be mixed across various NFTs in a contract
entrypoint meta_info() : meta_info
| return | type |
|---|---|
| meta_info | meta_info |
Returns metadata associated with an NFT.
Note:
- The
metadatacan be set in the constructor, as well as by implementing extensions like e.g.mintable - The
metadatato use depends on themetadata_typedefined on contract level and provides certain flexibility:- for
URLandOBJECT_IDuseMetadataIdentifierURLcan represent any URL and typically the NFT id is used to resolve the metadata using that URL, e.g.ipfs://serving asbase_urland pointing to a folder stored on IPFS where immutable metadata is stored (recommended)https://serving asbase_urland pointing to a traditional website where metadata is stored- ...
OBJECT_IDcan be used to refer to any kind of item which typically already exists (e.g. the VIN of a car)
- for
MAPuseMetadataMapMAPprovides almost unlimited flexibility and allows any kind of metadata to be represented in a map
- for
entrypoint metadata(token_id: int) : option(metadata)
| parameter | type |
|---|---|
| token_id | int |
| return | type |
|---|---|
| data | option(metadata) |
Returns the total amount of NFTs in circulation.
entrypoint total_supply() : ínt
| return | type |
|---|---|
| total_supply | int |
Returns the number of NFTs owned by the account with address owner in the contract. If the owner address is unknown to the contract, None will be returned. Using option type as a return value allows us to determine if the account owns 0, more than 0, or the account has never owned a balance and is still unknown to the contract.
entrypoint balance(owner: address) : option(int)
| parameter | type |
|---|---|
| owner | address |
| return | type |
|---|---|
| balance | option(int) |
Returns the owner's address for the provided token_id if the NFT is minted. If the NFT isn't minted, None will be returned.
entrypoint owner(token_id: int) : option(address)
| parameter | type |
|---|---|
| token_id | int |
| return | type |
|---|---|
| owner | option(address) |
Transfers NFT with ID token_id from the current owner to the to address. Will invoke IAEX141Receiver.on_aex141_received if the to address belongs to a contract. If provided, data will be submitted with the invocation of IAEX141Receiver.on_aex141_received. Emits the Transfer event.
Note: For security reasons reentrancy is not possible. Therefore contracts cannot use this entrypoint to transfer NFTs to itself. Use transfer_to_contract instead to cover this scenario.
Throws if:
Call.calleris NOT the current owner or NOT approved to transfer on behalf of the owner;token_idis NOT a valid token;- the invocation of
IAEX141Receiver.on_aex141_receivedfails.
stateful entrypoint transfer(to: address, token_id: int, data: option(string)) : unit
| parameter | type |
|---|---|
| token_id | int |
| data | option(string) |
Transfers NFT with ID token_id from the current owner to the contract calling this entrypoint. As reentrancy is not possible for security reasons, this entrypoint MUST be used if a contract (e.g. NFT marketplace) wants to transfer the NFT to itself on behalf of the owner. Emits the Transfer event.
Throws if:
Call.calleris NOT a contract or NOT approved to transfer on behalf of the owner;token_idis NOT a valid token;
stateful entrypoint transfer(to: address, token_id: int, data: option(string)) : unit
| parameter | type |
|---|---|
| to | address |
| token_id | int |
| data | option(string) |
Sets the approved address to interact on behalf of an owner for the NFT with ID token_id. If enabled is true the operator address is approved, if false the approval is revoked. Throws unless caller is the current NFT owner, or an authorized operator of the current owner. Emits the Approval event.
stateful entrypoint approve(approved: address, token_id: int, enabled: bool) : unit
| parameter | type |
|---|---|
| approved | address |
| token_id | int |
| enabled | bool |
Enables or disables approval for an operator address to manage all of the caller's NFTs. If enabled is true, the operator address is approved, if false, the approval is revoked. Emits the ApprovalForAll event.
stateful entrypoint approve_all(operator: address, enabled: bool) : unit
| parameter | type |
|---|---|
| operator | address |
| enabled | bool |
Returns the address approved to interact with the NFT with ID token_id or returns None if no approval has been set. Throws if NFT with ID token_id does not exist.
entrypoint get_approved(token_id: int) : option(address)
| parameter | type |
|---|---|
| token_id | int |
| return | type |
|---|---|
| approved | option(address) |
Returns true if approved address is approved to transact for NFT with ID token_id.
entrypoint is_approved(token_id: int, approved: address) : bool
| parameter | type |
|---|---|
| token_id | int |
| approved | address |
| return | type |
|---|---|
| approved | bool |
Returns true if operator is approved to commit transactions on behalf of owner.
Indicates whether an address is an authorized operator for another address.
entrypoint is_approved_for_all(owner: address, operator: address) : bool
| parameter | type |
|---|---|
| owner | address |
| approved | address |
| return | type |
|---|---|
| approved | bool |
datatype event
= Transfer(address, address, int)
| Approval(address, address, int, string)
| ApprovalForAll(address, address, string)
This event MUST be triggered and emitted when tokens are transferred.
The event arguments should be as follows: (from, to, token_id)
Transfer(address, address, int)
| parameter | type |
|---|---|
| from | address |
| to | address |
| token_id | int |
This event MUST be triggered and emitted upon approval, including revocation of approval.
The event arguments should be as follows: (owner, approved, token_id, enabled).
Enabled is of type string, because of a limit of 3 on indexed values. Since address, int and bool are automatically indexed, having enabled as bool would cause an error, as it would be the 4th indexed item. Use "true" or "false" as return values instead.
Approval(address, address, int, string)
| parameter | type |
|---|---|
| owner | address |
| approved | address |
| token_id | int |
| enabled | string |
This event MUST be triggered and emitted upon a change of operator status, including revocation of approval for all NFTs in the contract.
The event arguments should be as follows: (owner, operator, approved)
For idiomatic reasons approved is, just as with Approval, of type string
ApprovalForAll(address, address, string)
| parameter | type |
|---|---|
| owner | address |
| operator | address |
| approved | string |
The standard only allows safe transfers of tokens. On transfer a check MUST be performed which checks if the recipient is a contract and if so the transfer MAY ONLY happen if on_aex141_received returns true.
contract interface IAEX141Receiver =
entrypoint on_aex141_received : (option(address), int, option(string)) => bool
Deals with receiving NFTs on behalf of a contract. Contracts MUST implement this interface to be able to receive NFTs. Mint and transfer transactions will invoke the on_aex141_received function.
Returns true or false to signal whether processing the received NFT was successful or not.
entrypoint on_aex141_received(from: option(address), token_id: int, data: option(string)) : bool
| parameter | type |
|---|---|
| from | option(address) |
| token_id | int |
| data | option(string) |
This section covers the extendability of the basic token - e.g. mintable, burnable.
When an NFT contract implements an extension its name should be included in the aex141_extensions array, in order for third party software or contracts to know the interface.
Any extensions should be implementable without permission. Developers of extensions MUST choose a name for aex141_extensions that is not yet used. Developers CAN make a pull request to the reference implementation for general purpose extensions and maintainers choose to eventually include them.
The mintable extension SHOULD be used for generic NFT minting without specific requirements in regards to minting.
Note: If you aim to reuse the same metadata across many NFTs you might prefer the mintable_templates extension.
contract interface IAEX141Mintable =
datatype event = Mint(address, int)
stateful entrypoint mint : (address, option(metadata), option(string)) => int
Issues a new token to the provided address. If the owner is a contract, IAEX141Receiver.on_aex141_received will be called with data if provided.
Emits the Mint event.
Throws if the call to IAEX141Receiver.on_aex141_received implementation failed (safe transfer)
stateful entrypoint mint(owner: address, metadata: option(metadata), data: option(string)) : int
| parameter | type |
|---|---|
| owner | address |
| metadata | option(metadata) |
| data | option(string) |
| return | type |
|---|---|
| token_id | int |
The mintable_limit extension SHOULD be used if the amount of NFTs to mint should be limited/capped. It MAY ONLY be used in combination with the mintable extension.
The initially defined token limit MUST be greater than or equal to 1.
Emits the TokenLimit event on contract creation.
contract interface IAEX141MintableLimit =
datatype event
= TokenLimit(int)
| TokenLimitDecrease(int, int)
entrypoint token_limit : () => int
stateful entrypoint decrease_token_limit : (int) => unit
Returns the limit / max amount of NFTs that can be minted.
entrypoint token_limit() : ínt
| return | type |
|---|---|
| token_limit | int |
Decreases the NFT limit/cap defined in the collection. An increase of the limit is forbidden.
Emits the TokenLimitDecrease event.
Throws if:
new_limitequals the currenttoken_limitnew_limitis lower than the currenttotal_supply
stateful entrypoint decrease_token_limit(new_limit: int) : unit
| parameter | type |
|---|---|
| new_limit | int |
The burnable extension SHOULD be used if NFTs within a contract are intended to be burnable.
contract interface IAEX141Burnable =
datatype event = Burn(address, int)
stateful entrypoint burn : (int) => unit
Burns the NFT with the provided token_id.
Emits the Burn event.
Throws if Call.caller is NOT the current owner or NOT approved to transfer on behalf of the owner.
stateful entrypoint burn(token_id: int) : unit
| parameter | type |
|---|---|
| token_id | int |
The mintable_templates extension SHOULD be used if multiple NFTs within a contract should share the same metadata. The extension provides the possbility to create templates. Optionally an edition limit for each template can be defined on template creation. If no edition limit is provided, it's considered to be unlimited. The edition limit can be decreased at any point of time as long as the new value does not exceed the current edition supply. Also, templates can be deleted if no NFT has been created using the template.
Note:
- On contract level the
MAPMUST be used asmetadata_type- the map stores the
template_idandedition_serialof each NFT - if
mutable_attributesextension is used, it also stores the mutable attributes
- the map stores the
- The
template_metadata_typecan either be T_URL (string, e.g.ipfs://pointing to a JSON stored on IPFS) or T_MAP (map(string, string))- it is defined during contract creation
- it is applied to ALL templates created with the contract
contract interface IAEX141MintableTemplates =
datatype template_metadata_type = T_URL | T_MAP
datatype event
= TemplateCreation(int)
| TemplateDeletion(int)
| TemplateMint(address, int, int, string)
| EditionLimit(int, int)
| EditionLimitDecrease(int, int, int)
record template =
{ immutable_metadata: metadata
, edition_limit: option(int)
, edition_supply: int }
entrypoint template_metadata_type : () => template_metadata_type
entrypoint template : (int) => option(template)
entrypoint template_supply : () => int
stateful entrypoint create_template : (metadata, option(int)) => int
stateful entrypoint delete_template : (int) => unit
stateful entrypoint template_mint : (address, int, option(string)) => int
stateful entrypoint decrease_edition_limit : (int, int) => unit
Returns the metadata type which is used for templates.
entrypoint template_metadata_type() : template_metadata_type
| return | type |
|---|---|
| template_metadata_type | template_metadata_type |
Returns the template for the provided template_id in case it exists, otherwise None.
entrypoint template(template_id: int) : option(template)
| return | type |
|---|---|
| template | option(template) |
Returns the total amount of templates that currently exist.
entrypoint template_supply() : ínt
| return | type |
|---|---|
| template_supply | int |
Creates a new template for specific immutable metadata. The immutable_metadata needs to be MetadataIdentifier in case template_metadata_type is T_URL and MetadataMap for T_MAP. The edition limit defines how many NFTs can be minted based on a specific template. If no edition limit is provided, an unlimited amount of NFTs can be created using the template. The edition limit can be decreased at any time.
Emits the TemplateCreation event.
Note: It's recommended to perform a check if immutable_metadata is considered valid according to individual requirements.
Throws in case the wrong variant for metadata is provided:
- variant MUST be
MetadataIdentifieriftemplate_metadata_typeisT_URL - variant MUST be
MetadataMapiftemplate_metadata_typeisT_MAP
stateful entrypoint create_template(immutable_metadata: metadata, edition_limit: option(int)) : int
| parameter | type |
|---|---|
| immutable_metadata | metadata |
| edition_limit | option(int) |
| return | type |
|---|---|
| template_id | int |
Deletes the template with the provided id.
Emits the TemplateDeletion event.
Throws if:
- the provided
template_iddoes not exist - an NFT based on this template has already been created
stateful entrypoint delete_template(template_id: int) : unit
| parameter | type |
|---|---|
| template_id | int |
Mints a new NFT for the provided template id. If to is a contract, IAEX141Receiver.on_aex141_received will be called with data if provided.
Emits the TemplateMint event.
Throws if:
- the provided
template_iddoes not exist - the mint would exceed the current
edition_limitof the template - the call to
IAEX141Receiver.on_aex141_receivedimplementation failed (safe transfer)
stateful entrypoint template_mint(to: address, template_id: int, data: option(string)) : int
| parameter | type |
|---|---|
| to | address |
| template_id | int |
| data | option(string) |
| return | type |
|---|---|
| token_id | int |
Decreases the edition limit of a specific template. An increase of the edition limit of a template is forbidden.
Emits the EditionLimitDecrease event.
Throws if:
- the provided
template_iddoes not exist - the
new_limitis equal to or lower than 0 - the
new_limitis equal to or higher than the current edition limit - the
new_limitis lower than the current edition supply
stateful entrypoint decrease_edition_limit(template_id: int, new_limit: int) : unit
| parameter | type |
|---|---|
| template_id | int |
| new_limit | int |
The mintable_templates_limit extension SHOULD be used to introduce a limit/cap for the amount of templates that may be in existence. It MAY ONLY be used in combination with the mintable_templates extension.
contract interface IAEX141MintableTemplatesLimit =
datatype event
= TemplateLimit(int)
| TemplateLimitDecrease(int, int)
entrypoint template_limit : () => int
stateful entrypoint decrease_template_limit : (int) => unit
Returns the limit / max amount of templates that can be created.
entrypoint template_limit() : ínt
| return | type |
|---|---|
| template_limit | int |
Decreases the template limit/cap defined in the contract. An increase of the limit is forbidden.
Emits the TemplateLimitDecrease event.
Throws if:
new_limitequals to or is lower than 0new_limitequals the currenttoken_limitnew_limitis lower than the currenttotal_supply
stateful entrypoint decrease_template_limit(new_limit: int) : unit
| parameter | type |
|---|---|
| new_limit | int |
The mutable_attributes extension SHOULD be used if NFTs in the contract should store attributes which can change over time. This can e.g. be beneficial for game assets where users could level up their characters. The mutable attributes are expected to be provided in a JSON string.
contract interface IAEX141MutableAttributes =
datatype event = MutableAttributesUpdate(int, string)
stateful entrypoint update_mutable_attributes : (int, string) => unit
Updates the JSON string that contains mutable attributes of the NFT.
Emits the MutableAttributesUpdate event.
Throws if the provided token_id does not exist.
stateful entrypoint update_mutable_attributes(token_id: int, mutable_attributes: string) : unit
| parameter | type |
|---|---|
| token_id | int |
| mutable_attributes | string |
Mint
This event is defined by the mintable extension and MUST be triggered whenever a new token is minted.
The event arguments should be as follows: (to, token_id)
Mint(address, int)
| parameter | type |
|---|---|
| to | address |
| token_id | int |
TokenLimit
This event is defined by the mintable_limit extension and MUST be triggered in the init entrypoint during contract creation.
The event arguments should be as follows: (limit)
TokenLimit(int)
| parameter | type |
|---|---|
| limit | int |
TokenLimitDecrease
This event is defined by the mintable_limit extension and MUST be triggered whenever the decrease_token_limit entrypoint is called.
The event arguments should be as follows: (old_limit, new_limit)
TokenLimitDecrease(int, int)
| parameter | type |
|---|---|
| old_limit | int |
| new_limit | int |
Burn
This event is defined by the burn extension and MUST be triggered whenever an NFT is burned.
The event arguments should be as follows: (owner, token_id)
Burn(address, int)
| parameter | type |
|---|---|
| owner | address |
| token_id | int |
TemplateCreation
This event is defined by the mintable_templates extension and MUST be triggered whenever a new template is created.
The event arguments should be as follows: (template_id)
TemplateCreation(int)
| parameter | type |
|---|---|
| template_id | int |
TemplateDeletion
This event is defined by the mintable_templates extension and MUST be triggered whenever a template is deleted.
The event arguments should be as follows: (template_id)
| parameter | type |
|---|---|
| template_id | int |
TemplateMint
This event is defined by the mintable_templates extension and MUST be triggered whenever a new NFT is minted.
The event arguments should be as follows: (to, template_id, token_id, edition_serial). Sophia does not allow 4 "indexed" values as of writing the standard. For this reason edition_serial defined as type string.
The event arguments should be as follows: (to, template_id, token_id, edition_serial)
TemplateMint(address, int, int, string)
| parameter | type |
|---|---|
| to | int |
| template_id | int |
| token_id | int |
| edition_serial | string |
EditionLimit
This event is defined by the mintable_templates extension and MUST be triggered whenever a new template is created which specifies an edition limit.
The event arguments should be as follows: (template_id, edition_limit)
EditionLimit(int, int)
| parameter | type |
|---|---|
| template_id | int |
| edition_limit | int |
EditionLimitDecrease
This event is defined by the mintable_templates extension and MUST be triggered whenever the decrease_edition_limit entrypoint is called.
The event arguments should be as follows: (template_id, old_limit, new_limit)
EditionLimitDecrease(int, int, int)
| parameter | type |
|---|---|
| template_id | int |
| old_limit | int |
| new_limit | int |
TemplateLimit
This event is defined by the mintable_templates_limit extension and MUST be triggered in the init entrypoint during contract creation.
The event arguments should be as follows: (limit)
TemplateLimit(int)
| parameter | type |
|---|---|
| limit | int |
TemplateLimitDecrease
This event is defined by the mintable_templates_limit extension and MUST be triggered whenever the decrease_template_limit entrypoint is called.
The event arguments should be as follows: (old_limit, new_limit)
TemplateLimitDecrease(int, int)
| parameter | type |
|---|---|
| old_limit | int |
| new_limit | int |
MutableAttributesUpdate
This event is defined by the mutable_attributes extension and MUST be triggered whenever the mutable attributes of an NFT are updated.
The event arguments should be as follows: (token_id, mutable_attributes)
MutableAttributesUpdate(token_id, string)
| parameter | type |
|---|---|
| token_id | int |
| mutable_attributes | string |
There are currently following reference implementations available which follows defined standard:
- aex141-nft-collection-example
- CollectionUniqueNFTs.aes implements the following extensions:
mintablemintable_limitburnable
- CollectionTemplateEditionNFTs.aes implements the following extensions:
mintable_templatesmintable_templates_limitmutable_attributesburnable
- CollectionUniqueNFTs.aes implements the following extensions:
Additionally there exists following showcase for a simple NFT marketplace using AEX-141: