Skip to content

Latest commit

 

History

History
378 lines (287 loc) · 15.8 KB

frc-0053.md

File metadata and controls

378 lines (287 loc) · 15.8 KB
fip title author discussions-to status type created
0053
Non-Fungible token standard
Alex Su (@alexytsu), Andrew Bright (@abright), Alex North (@anorth)
Draft
FRC
2022-14-11

FRC-0053: Non-Fungible Token Standard

Simple Summary

A standard interface for native actor non-fungible tokens (NFTs).

Abstract

This proposal provides a standard API for the implementation of non-fungible tokens (NFTs) as FVM native actors. The proposal learns from NFT standards developed for other blockchain ecosystems, being heavily inspired by ERC-721. However, as a design goal, this proposal aims to complement the existing fungible token interface described in FRC-0046. As such it brings along equivalent specifications for:

  • Standard token/name/symbol/supply/balance queries
  • Standard allowance protocol for delegated control
  • Mandatory universal receiver hooks for incoming tokens

The interface has been designed with gas-optimisations in mind and hence methods support batch-operations where practical.

Change Motivation

The concept of a non-fungible token is widely established in other blockchains. As on other blockchains, a complementary NFT standard to FRC-0046 allows the ownership of uniquely identifiable assets to be tracked on-chain. The Filecoin ecosystem will benefit from a standard API implemented by these actors. A standard permits easy building of UIs, wallets, tools, and higher level applications on top of a variety of tokens representing different assets.

Specification

Methods and types are described with a Rust-like pseudocode. All parameters and return types are IPLD types in a CBOR tuple encoding.

Methods are to be dispatched according to the calling convention described in FRC-0042.

Interface

An actor implementing a FRC-00XX token must provide the following methods.

type TokenID = u64;

/// Multiple token or actor IDs are represented as a BitField encoded with RLE+, 
/// the index of each set bit corresponds to a token or actor ID.
type TokenSet = BitField;
type ActorIDSet = BitField;

/// A descriptive name for the collection of NFTs in this actor.
fn Name() -> String

/// An abbreviated (ticker) name for the NFT collection.
fn Symbol() -> String

/// Returns the total number of tokens in this collection.
/// Must be non-negative.
/// Must equal the balance of all adresses.
/// Should equal the sum of all minted tokens less tokens burnt.
fn TotalSupply() -> u64

/// Returns a URI that resolves to the metadata for a particular token.
fn MetadataID(tokenID: TokenID) -> String

/// Returns the balance of an address which is the number of unique NFTs held.
/// Must be non-negative.
fn Balance(owner: Address) -> u64

/// Returns a parallel list of actor IDs that own a list of tokens.
fn Owner(tokenIDs: TokenSet) -> Vec<ActorID>

/// Transfers tokens from caller to the specified address.
/// Each token specified must exist and be owned by the caller.
/// The entire batch of transfers aborts if any of the specified tokens does not meet the criteria.
/// Transferring to the caller must be treated as a normal transfer.
/// Transferring a token also revokes any operators from the token.
/// The operatorData is passed through to the receiver hook directly.
/// Returns the resulting balances for the from and to addresses.
fn Transfer({to: Address, tokenIDs: TokenSet, operatorData: Bytes})
  -> {fromBalance: u64, toBalance: u64}

/// Transfers tokens to the specified address with the caller acting as an operator.
/// Each token specified must exist and the caller must be approved to transfer it. 
/// The caller is considered approved by being an operator for the token-id or 
/// by being an account-level operator for the address that owns the token.
/// The entire batch of transfers aborts if any of the specified tokens does not meet the criteria.
/// Transferring to the owner must be treated as a normal transfer.
/// Transferring a token also revokes any operators from the token.
/// Returns the resulting balances for the from and to addresses.
/// The operatorData is passed through to the receiver hook directly
fn TransferFrom({from: Address, to: Address, tokenIDs: TokenSet, operatorData: Bytes})
  -> {fromBalance: u64, toBalance: u64}

/// Approves an address as an operator for a set of tokens.
/// For all tokens specified, the caller must either own the token or be an account-level operator
/// for some other (unspecified) owner.
/// If not, the entire batch will fail to be approved.
fn Approve({operator: Address, tokenIDs: TokenSet}) -> ()

/// Revokes an address as an operator for a set of tokens.
/// For all tokens specified, the caller must either own the token or be an account-level operator
/// for some other (unspecified) owner.
/// If not, the entire batch will fail to be revoked. 
/// If the specified operator is not currently an operator for any tokens in the list,
/// the method must still succeed but have no effect on those tokens.
fn RevokeApproval({operator: Address, tokenIDs: TokenSet}) -> ()

/// Returns whether an address is an operator for a particular set of tokens.
/// The owner of a token is not implicitly considered as a valid operator of that token.
/// The return value is a subset of the provided token set containing only those token IDs
/// for which the address is an operator. 
fn IsApproved({operator: Address, tokenIDs: TokenSet}) -> TokenSet

/// Sets an address as an account-level operator for any token (including future tokens)
/// that are owned by the caller.
/// This does not replace any token-level approvals prior or subsequent.
fn ApproveForAll({operator: Address}) -> ()

/// Returns whether an address is an account-level operator for another address.
/// Addresses are not reflexive operators for themselves.
fn IsApprovedForAll({owner: Address, operator: Address}) -> bool

/// Revokes an address as an account-level operator for the caller.
/// Note that the operator may still be an operator for specific tokens owned by the caller.
fn RevokeApprovalForAll({operator: Address}) -> ()

/// Burns tokens owned by the caller.
/// Aborts if any of the tokens is not owned by the caller.
///
/// Returns the caller's resulting balance.
fn Burn({tokenIDs: TokenSet}) -> u64;

/// Burns tokens where the caller is an operator for the owner.
/// Aborts if the caller is not an operator for any of the tokens.
fn BurnFrom({from: Address, tokenIDs: TokenSet}) -> ();

Optional Extension - Enumerable TokenIDs An actor may choose to implement a method to retrieve the list of all circulating tokens.

/// Enumerates some number, up to `limit`, of circulating (i.e. non-burnt) Token IDs.
/// May return fewer than `limit`, including zero, tokens even if more remain.
/// The cursor is an opaque pagination token.
/// An empty byte array requests enumeration of all tokens (up to `limit`).
/// If more tokens remain, a next cursor must be returned, and must differ from the provided one.
/// This cursor may be passed to a subsequent call to
/// continue enumeration from where the previous call left off.
/// A sequence of calls, each passing the cursor provided by the previous one,
/// must enumerate all tokens.
/// Tokens need not be enumerated in ID order.
///
/// A cursor is valid only while the token state remains unchanged.
/// Implementations should abort if provided a stale cursor.
fn ListTokens({cursor: byte[], limit: u64}) -> {tokens: TokenSet, next_cursor: Option<byte[]>}

Optional Extension - Enumerable Balances An actor may choose to implement a method to retrieve the list of all tokens owned by an address.

/// Enumerates some number, up to `limit`, of tokens owned by an address.
/// See ListTokens for detailed semantics.
fn ListOwnedTokens({owner: Address, cursor: byte[], limit: u64}) 
  -> {tokens: TokenSet, next_cursor: Option<byte[]>}

Optional Extension - Enumerable Operators

An actor may choose to implement the following methods to support enumeration of operators.

/// Returns all the operators approved for a specific token.
/// The result does not include account-level operators unless such operators are also
/// explicitly approved for the token.
fn ListTokenOperators({tokenID: TokenID, cursor: byte[], limit: u64}) 
    -> {operators: ActorIDSet, next_cursor: Option<byte[]>}

/// Enumerates tokens for which an account is an explicit operator.
/// For account-level operators, the result only includes tokens for which the address would
/// be an operator even if account-level approval were revoked.
/// (Use ListOwnedTokens to enumerate the tokens an account-level operator can control,
/// which is all of them).
fn ListOperatorTokens({operator: Address, cursor: byte[], limit: u64})
  -> {tokens: TokenSet, next_cursor: Option<byte[]>}

/// Returns all the account-level operators approved by an owner.
fn ListAccountOperators({owner: Address, cursor: byte[], limit: u64})
    -> {operators: ActorIDSet, next_cursor: Option<byte[]>}

Receiver Interface

An actor must implement a receiver hook to receive NFTs. The receiver hook is defined in FRC-0046 and must not abort when handling an incoming token. When transferring batch of tokens, the receiver hook is invoked just once, meaning the entire set of NFTs is either accepted or rejected (by aborting).

/// Type of the payload accompanying the receiver hook for a FRC-0053 NFT.
struct FRC53TokensReceived {
    // The address from which tokens were debited
    from: Address,
    // The address to which tokens were credited (which will match the hook receiver)
    to: Address,
    // The actor which initiated the mint or transfer
    operator: Address,
    // The tokens being transferred
    tokenIDs: TokenSet,
    // Arbitrary data provided by the operator when initiating the transfer
    operatorData: Bytes,
    // Arbitrary data provided by the token actor
    tokenData: Bytes,
}

/// Receiver hook type value for an FRC-0053 token
const FRC53TokenType = frc42_hash("FRC53")

// Invoked by a NFT actor after transfer of tokens to the receiver’s address.
// The NFT collection state must be persisted such that the receiver will observe the new balances.
// To reject the transfer, this hook must abort.
fn Receive({type: uint32, payload: byte[]})

Behaviour

Universal receiver hook

The NFT collection must invoke the receiver hook method on the receiving address whenever it credits tokens. The type parameter must be FRC53TokenType and the payload must be the IPLD-CBOR serialized FRC53TokensReceived structure.

The attempted credit is only persisted if the receiver hook is implemented and does not abort. A mint or transfer operation should abort if the receiver hook does, or in any case must not credit tokens to that address.

Minting

API methods for minting are left unspecified. A newly minted token cannot have the same ID as an existing token or a token that was previously burned. Minting must invoke the receiver hook on the receiving address and fail if it aborts.

Transfers

Empty transfers are allowed, including when the from address has zero balance. An empty transfer must invoke the receiver hook of the to address and abort if the hook aborts. An empty transfer can thus be used to send messages between actors in the context of a specific NFT collection.

Operators

Operators can be approved at two separate levels.

Token level operators are approved by the owner of the token via the Approve method. If an account is an operator on a token, it is permitted to debit (burn or transfer) that token. An NFT can have many operators at a time, but transferring the token will revoke approval of all its existing operators.

Account level operators are approved via the ApproveForAll method. If an owner approves an operator at the account level, that operator has permission to debit any token belonging to the owner's account. This includes tokens that are not yet owned by the account at the time of approval.

Addresses

Addresses for receivers and operators must be resolvable to an actor ID. Balances must only be credited to an actor ID. All token methods must attempt to resolve addresses provided in parameters to actor IDs. A token should attempt to initialise an account for any address which cannot be resolved by sending a zero-value transfer of the native token to the address.

Note that this means that an uninitialized actor-type (f2) address cannot receive tokens or be authorized as an operator. Future changes to the FVM may permit initialization of such addresses by sending a message to them, in which case they should automatically become functional for this standard.

Extensions

An NFT collection may implement other methods for transferring tokens and managing operators. These must maintain the invariants about supply and balances, and invoke the receiver hook when crediting tokens.

An NFT collection may implement restrictions on allowances and transfer of tokens.

Design Rationale

Batching

Some methods on this interface accept a set of TokenIDs. When a list of IDs is specified, the method must succeed or fail atomically. This simplifies retry logic for actors when methods fail and encourages further inspection of the intended parameters when human errors may have been made.

Synergy with fungible tokens

In order for higher synergy with the existing FRC-0046 fungible token standard, this proposal aims for a conceptually similar interface in terms of balances, supply and operators. For the same safety reasons described in FRC-0046, this token standard requires a universal receiver on actors that wish to hold tokens.

This allows easier interactions between fungible tokens and NFTs and opens possibilities for composition of the two standards to represent different structures of ownership such as fractionalized NFTs or semi-fungible tokens. Instead of encoding such semantics directly into the standard, a minimal interface is proposed instead to make the primary simple use cases more efficient and straightforward.

Transfers

There is no technical need to separate the Transfer and TransferFrom methods. A transfer method could function given only the list of token ids that the caller wishes to transfer and for each token asserts that:

  • The caller is the owner of the token OR
  • The caller is an approved operator on the token OR
  • The caller is an approved operator on the account that owns the token

However, the authors judge that the benefit of aligning with existing conventions (FRC-46, ERC-721 etc.) and having separate flows for operators and token owners will reduce risk of mistakes and unexpected behaviour.

Metadata

This proposal does not specify a structure or format of metadata. It is encouraged that what is stored on chain is an ipfs:// link that can be resolved to the metadata.

Backwards Compatability

There are no implementations of NFT collections yet on Filecoin.

Test Cases

Extensive test cases are present in the implementation of this proposal at https://github.com/helix-onchain/filecoin/tree/main/frc53_nft.

Security Considerations

Reentrancy

Receiver hooks introduce the possibility of complex call flows, the most concerning of which might be a malicious receiver that calls back to a token actor in an attempt to exploit a re-entrancy bug. We expect that one or more high quality reference implementations of the token state and logic will keep the vast majority of NFT actors safe. We judge the risk to more complex actors as lesser than the aggregate risk of losses due to misdirected transfers.

Incentive Considerations

N/A

Product Considerations

??

Implementation

An implementation of this standard is in development at https://github.com/helix-onchain/filecoin/tree/main/frc53_nft.

Copyright

Copyright and related rights waived via CC0.