Skip to main content

FA 2

Implements TZIP-12 norm for token, including non fungible.

info

The FA 2 contracts presented below (NFT, Fungible, Multi-asset) have been audited by Inference AG. The full audit report may be found here.

FA 2 is a norm for any kind of tokens. It provides the following elements (marked 🔸):

  • ledger that associates tokens and owners
  • operator that defines which addresses can transfer tokens on the behalf of their owners
  • transfer entrypoint to transfer tokens while respecting allowance rules
  • update_operators to add or remove operators on tokens for transfer operations
  • balance_of TZIP-4 view to normalise the access to balance information

Repository


Author


Norms


Templates

Overview​

Below are presented implementations for 3 kinds of tokens:

  1. Non fungible token (NFT): an NFT is a unit of data that can represent digital files such as art, audio, videos, items in video games and other forms of creative work
  2. Fungible token
  3. Multi asset (or semi-fungible) token

Besides TZIP-12 features, these implementations provide:

Errors​

constant CALLER_NOT_OWNER         : string = "CALLER_NOT_OWNER"
constant FA2_INSUFFICIENT_BALANCE : string = "FA2_INSUFFICIENT_BALANCE"
constant FA2_INVALID_AMOUNT : string = "FA2_INVALID_AMOUNT"
constant FA2_NOT_OPERATOR : string = "FA2_NOT_OPERATOR"
constant FA2_TOKEN_UNDEFINED : string = "FA2_TOKEN_UNDEFINED"
constant SIGNER_NOT_FROM : string = "SIGNER_NOT_FROM"

constant string

Records​

transfer_param 🔸​

Code


record transfer_destination {
to_dest : address;
token_id_dest : nat;
token_amount_dest : nat
} as ((%to_, (token_id, amount)))

record transfer_param {
tp_from : address;
tp_txs : list<transfer_destination>;
} as ((%from_, %txs))

record address nat list

Type


Michelson


Related

Storage​

ledger 🔸​

Associates accounts to token information (identifier for NFT, balance of funcgible tokens).

Code


ledger associates an NFT id to its owner (not the other way round).

asset ledger to big_map {
ltokenid : nat;
lowner : address;
}

asset address nat

Type


Related

royalties​

Code


record part {
part_account : address;
part_value : nat;
}

asset royalties identified by rtokenid to big_map {
rtokenid : nat;
rvalue : list<part>;
}

record address nat asset list

Type


Related

operator 🔸​

Code


asset operator identified by oaddr otoken oowner to big_map {
oaddr : address;
otoken : nat;
oowner : address;
}

asset address nat unit

Type


Related

operator_for_all​

Code


asset operator_for_all identified by fa_oaddr fa_oowner to big_map {
fa_oaddr : address;
fa_oowner : address;
}

asset address unit

Type


Related

Functions​

get_from(txs)​

Code


function get_from(txs : list<transfer_param>) : option<address> {
match txs with
| hd::tl -> begin
const %from = hd.tp_from;
for tx in tl do
do_require(%from = tx.tp_from, FA2_NOT_OPERATOR)
done;
return some(%from)
end
| [] -> return none
end
}

function list option match begin const . for do_require =

Parameter


List of pairs of sender address and transfer destination.

Returns

Fails with

does not fail


Related

check_operator(txs)​

Code


function check_operator(txs : list<transfer_param>) : bool {
var res = true;
for tx in txs do
const %from = tx.tp_from;
const tds = tx.tp_txs;
for td in tds do begin
res &=
if caller <> %from then
(operator.contains((caller, td.token_id_dest, %from)) or
operator_for_all.contains((caller, %from)))
else
true;
end
done
done;
return res
}

function list bool var for const if begin . &= or <> contains caller

Parameter


List of pairs of sender address and transfer destination.

Returns

Fails with

does not fail


Related

Entrypoints​

update_operators(upl) 🔸​

Code


record operator_param {
opp_owner : address;
opp_operator : address;
opp_token_id : nat
} as ((owner, (operator, token_id)))

enum update_op =
| add_operator<operator_param>
| remove_operator<operator_param>

entry update_operators (upl : list<update_op>) {
require { fa2_r1 : is_not_paused() }
effect {
for up in upl do
match up with
| add_operator(param) ->
do_require(param.opp_owner = caller , CALLER_NOT_OWNER);
operator.put({ param.opp_operator; param.opp_token_id; param.opp_owner })
| remove_operator(param) ->
do_require(param.opp_owner = caller , CALLER_NOT_OWNER);
operator.remove((param.opp_operator, param.opp_token_id, param.opp_owner))
end;
done;
}
}

record address nat enum entry list require for match do_require put remove caller

Parameter


List of operators to add or remove.

Fails with

"CONTRACT_PAUSED"

When contract is already paused.


"CALLER_NOT_OWNER"

When specified token owner is not caller.


Related

update_operator_for_all(upl)​

Code


enum update_for_all_op =
| add_for_all<address>
| remove_for_all<address>

entry update_operators_for_all (upl : list<update_for_all_op>) {
require { fa2_r2 : is_not_paused() }
effect {
for up in upl do
match up with
| add_for_all(op) ->
operator_for_all.put({ op; caller})
| remove_for_all(op) ->
operator_for_all.remove((op, caller))
end;
done;
}
}

enum address entry list require for match do_require put remove caller

Parameter


List of operators to add or remove.

Fails with

"CONTRACT_PAUSED"

When contract is already paused.


Related

do_transfer(txs)​

Called by transfer and transfer_gasless to actually execute the transfer of tokens (that is to update ledger collection). It is assumed here that operator or permits' authorization rules have been applied.

Note that it may only be called by the contract itself.

Code


entry do_transfer(txs : list<transfer_param>) {
called by self_address
effect {
for tx in txs do
const %from = tx.tp_from;
const tds = tx.tp_txs;
for td in tds do begin
const tokenid = td.token_id_dest;
const towner ?= ledger[tokenid]?.lowner : FA2_TOKEN_UNDEFINED;
do_require(towner = %from and td.token_amount_dest = 1,
FA2_INSUFFICIENT_BALANCE);
ledger.update(tokenid, { lowner = td.to_dest });
end done
done
}
}

entry list called by self_address for const . ?= ?. do_require = update

Parameter


List of pairs of sender address and transfer destination.

Fails with

"INVALID_CALLER"

When caller is not current contract.


"FA2_TOKEN_UNDEFINED"

When specified token id is not found in ledger.


"FA2_INSUFFICIENT_BALANCE"

When specified token owner is not the one found in ledger.


Related

transfer_gasless(batch)​

Code


record gasless_param {
transfer_params : list<transfer_param>;
user_pk : key;
user_sig : signature
}

function check_owner(addr : address, txs : list<transfer_param>) : bool {
var res = true;
for tx in txs do
res &= addr = tx.tp_from
done;
return res
}

entry transfer_gasless (batch : list<gasless_param>) {
require { fa2_r3 : is_not_paused() }
effect {
for b in batch do
const txs = b.transfer_params;
const pk = b.user_pk;
const sig = b.user_sig;
const pkh_signer = key_to_address(pk);
do_require(check_owner(pkh_signer, txs), SIGNER_NOT_FROM);
transfer 0tz to permits
call check<key * signature * bytes>((pk, sig, pack(txs)));
transfer 0tz to entry self.do_transfer(txs);
done
}
}

record list key signature entry require effect for const transfer

Parameter


List of pairs of permits (key and signature) and transfer specification.

Fails with

"CONTRACT_PAUSED"

When contract is already paused.


Related

transfer(txs) 🔸​

Code


entry %transfer (txs : list<transfer_param>) {
require { fa2_r4 : is_not_paused() }
effect {
if not check_operator(txs) then begin
match get_from(txs) with
| some(%from) ->
transfer 0tz to permits
call consume<address * bytes * string>((%from, pack(txs), FA2_NOT_OPERATOR))
| none -> ()
end
end;

transfer 0tz to entry self.do_transfer(txs);
}
}

entry list require effect if match transfer address bytes string pack

Parameter


List of pairs of sender address and transfer destination.

Fails with

"CONTRACT_PAUSED"

When contract is already paused.


"FA2_NOT_OPERATOR"

When caller is not the owner, nor a declared operator, or when specified from has no permit


Related

mint​

Code


entry mint (tow : address, tid : nat, tmd: map<string, bytes>, roy : list<part>) {
called by owner
require { fa2_r5: is_not_paused() }
effect {
ledger.add({ ltokenid = tid; lowner = tow });
token_metadata.add_update(tid, {
token_id = tid;
token_info = tmd
});
royalties.put({ rtokenid = tid; rvalue = roy });
}
}

entry address nat map string bytes list require effect add add_update put

Parameters


tow :

Token owner.


tid :

Token identifier.


Token metadata.


Token royalties.

Fails with

"CONTRACT_PAUSED"

When contract is already paused.


("ledger", "KEY_EXISTS")

When token already exists in ledger.


Related

burn​

Code


entry burn(tid : nat) {
constant {
token_owner ?is ledger[tid]?.lowner otherwise FA2_TOKEN_UNDEFINED;
}
require {
fa2_r6: is_not_paused();
fa2_r7: token_owner = caller otherwise CALLER_NOT_OWNER
}
effect {
ledger.remove(tid);
token_metadata.remove(tid);
royalties.remove(tid)
}
}

entry nat constant ?. require = caller effect remove

Parameter


tid :

Token identifier.

Fails with

"FA2_TOKEN_UNDEFINED"

When tid is not found in ledger.


"CALLER_NOT_OWNER"

When tid's owner is not caller.


Related

balance_of(requests) 🔸​

Code


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_ = (ledger[br.btoken_id] ? (the.lowner = br.bo_owner ? 1 : 0) : 0)
})
}

record address nat getter list map [] ?:

Parameter


List of pairs of owner and token id to get balance of.

Fails with

does not fail


Related

Views​

get_royalties(tokenid)​

Code


view get_royalties(tokenId : nat) : list<part> {
return (royalties[tokenId] ? the.rvalue : make_list<part>([]))
}

view nat list [] ?: make_list

Parameter


tokenid :

Token identifier.

Returns


List of pairs of address and percentage

Fails with

does not fail


Related