FA 2


This contract follows the Financial Asset 2 (FA 2) TZIP 12 specification for non-fungible token on Tezos.

You can observe the contract in action in the Collectible cards DApp example.

A contract template is available to transfer ownership of a FA 2 NFT based on an auction process.


FA 2 introduces the concept of operator, which is an account that can transfer a token on behalf of the owner. The delegation is done by the owner with the update_operators entrypoint.


tokencollectionToken data, like price.
ledgercollectionAssociation between token id and its owner.
operatorcollectionDelegation data: which operator can transfer which token owned by which owner?
token_metadatacollectionToken metadata.


update_operatorsuplupl is a list of delegation data (named operator_param with token, owner and operator), either to add or remove an operator to a token and owner. It fails if the caller is not the declared owner in upl.
transfertxsTransfers token ownerships specified in txs, a list of transfer_param (from, to, token). If caller is not the token owner, it must be declared in operator to be able to transfer, otherwise it fails.
balance_ofrequestsReturns the list a token balance for each token id in requests.


Deploy the contract from Archetype code below with the following Completium CLI example command:

completium-cli deploy nft.arl


archetype nft
asset ledger identified by ltoken to big_map {
ltoken : nat;
lowner : address;
asset operator identified by oaddr otoken oowner {
oaddr : address;
otoken : nat;
oowner : address;
asset token_metadata to big_map {
key_token_id : nat;
token_id : nat;
symbol : string;
name : string;
decimals : nat;
extras : map<string, string>;
record operator_param {
opp_owner : address;
opp_operator : address;
opp_token_id : nat
} as ((owner, (operator, token_id)))
entry update_operators (upl : list<or<operator_param, operator_param>>) {
for up in upl do
match up with
| left(param) -> (* add *)
dorequire(ledger[param.opp_token_id].lowner = source, "CALLER NOT OWNER");
operator.add({param.opp_operator; param.opp_token_id; param.opp_owner})
| right(param) -> (* remove *)
dorequire(ledger[param.opp_token_id].lowner = source, "CALLER NOT OWNER");
operator.remove((param.opp_operator, param.opp_token_id, param.opp_owner))
record transfer_destination {
to_dest : address;
token_id_dest : nat;
token_amount_dest : nat
} as ((to_, (token_id, amount)))
entry %transfer (txs : list<address * list<transfer_destination>>) {
for tx in txs do
var %from = tx[0];
var tds = tx[1];
for td in tds do begin
if caller <> %from then begin
(* check operator *)
(* set token ownership *)
ledger.addupdate(td.token_id_dest,{ lowner = td.to_dest });
end done;
record balance_of_request {
bo_owner : address;
btoken_id : nat;
} as ((owner, token_id))
record balance_of_response {
request : balance_of_request;
balance_ : nat;
} as ((request, balance))
getter balance_of (requests : list<balance_of_request>) : list<balance_of_response> {
return map(requests, br -> {
request = br;
balance_ = (if ledger[br.btoken_id].lowner = br.bo_owner
then 1
else 0)
entry token_metadata_registry (c : contract<address>) {
transfer 0tz to entry c(selfaddress);