FA 2
Implements TZIP-12 norm for token, including non fungible.
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 ownersoperator
that defines which addresses can transfer tokens on the behalf of their ownerstransfer
entrypoint to transfer tokens while respecting allowance rulesupdate_operators
to add or remove operators on tokens for transfer operationsbalance_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:
- 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
- Fungible token
- Multi asset (or semi-fungible) token
Besides TZIP-12 features, these implementations provide:
- the use of TZIP-17
permit
mechanism for two-steps fee-less transfer - one-step fee-less transfer mechanism (
transfer_gasless
) royalties
information for NFT tokens, compliant with Rarible market place- a global operator mechanism to allow one operator to manage all tokens of an owner (
operator_for_all
,update_operator_for_all
) mint
entrypoint callable by contract owner onlyburn
entrypoint callable by token owner only
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"
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))
Type
Michelson
Related
Storage​
ledger
🔸​
Associates accounts to token information (identifier for NFT, balance of funcgible tokens).
Code
- NFT
- Fungible
- Multi asset
ledger
associates an NFT id to its owner (not the other way round).
asset ledger to big_map {
ltokenid : nat;
lowner : address;
}
Type
Related
ledger
associates an account to the number of tokens it owns.
asset ledger to big_map {
lowner : address;
lamount : nat = 0;
} initialized with {
{owner; INITIAL_TOTAL_SUPPLY}
}
Type
Related
ledger
associates an account and a token id to the number of tokens it owns.
asset ledger identified by lowner ltokenid to big_map {
lowner : address;
ltokenid : nat;
lamount : nat = 0;
}
Note that the asset is identified by the owner account and token id.
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>;
}
Type
Related
operator
🔸​
Code
asset operator identified by oaddr otoken oowner to big_map {
oaddr : address;
otoken : nat;
oowner : address;
}
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;
}
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
}
Parameter
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
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
Fails with
"CONTRACT_PAUSED"
"CALLER_NOT_OWNER"
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
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
- NFT
- Fungible
- Multi asset
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
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
do_require(td.token_id_dest = TOKEN_ID, FA2_TOKEN_UNDEFINED);
const amount = ledger[%from]?.lamount ? the : 0;
const new_amount ?=
int_to_nat(amount - td.token_amount_dest) : FA2_INSUFFICIENT_BALANCE;
if new_amount = 0 then
ledger.remove(%from)
else begin
ledger.update(%from, { lamount := new_amount });
end;
ledger.add_update(td.to_dest, { lamount += td.token_amount_dest })
end done
done
}
}
entry
list
called by
self_address
for
const
.
?=
?.
do_require
=
>=
<>
begin
into_to_nat
update
add_update
remove
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 tid = td.token_id_dest;
const amount = ledger[(%from, tid)]?.lamount ? the : 0;
const new_amount ?=
int_to_nat(amount - td.token_amount_dest) : FA2_INSUFFICIENT_BALANCE;
if new_amount = 0 then
ledger.remove((%from, tid))
else begin
ledger.update((%from, tid), { lamount := new_amount });
end;
ledger.add_update((td.to_dest, tid), { lamount += td.token_amount_dest })
end done
done
}
}
entry
list
called by
self_address
for
const
.
?=
?.
do_require
=
>=
<>
begin
into_to_nat
update
add_update
remove
Parameter
Fails with
"INVALID_CALLER"
"FA2_TOKEN_UNDEFINED"
ledger
."FA2_INSUFFICIENT_BALANCE"
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
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
Fails with
"CONTRACT_PAUSED"
"FA2_NOT_OPERATOR"
Related
mint
​
Code
- NFT
- Fungible
- Multi Asset
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
Fails with
"CONTRACT_PAUSED"
("ledger", "KEY_EXISTS")
ledger
.Related
entry mint (tow : address, nbt : nat) {
called by owner
require { fa2_r5: is_not_paused() }
effect {
ledger.add_update(tow, { lamount += nbt });
}
}
Parameters
Fails with
"CONTRACT_PAUSED"
("ledger", "KEY_EXISTS")
ledger
.Related
entry mint (tow : address, tid : nat, nbt : nat) {
called by owner
require { fa2_r5: is_not_paused() }
effect {
ledger.add_update((tow, tid), { lamount += nbt });
}
}
Parameters
Fails with
"CONTRACT_PAUSED"
("ledger", "KEY_EXISTS")
ledger
.Related
burn
​
Code
- NFT
- Fungible
- Multi Asset
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)
}
}
Parameter
Fails with
"FA2_TOKEN_UNDEFINED"
tid
is not found in ledger
."CALLER_NOT_OWNER"
tid
's owner is not caller
.Related
entry burn(nbt : nat) {
constant {
amount ?is ledger[caller]?.lamount otherwise FA2_INSUFFICIENT_BALANCE
}
require {
fa2_r6: is_not_paused();
fa2_r7: amount >= nbt otherwise FA2_INSUFFICIENT_BALANCE
}
effect {
if (amount > nbt) then
ledger.update(caller, { lamount -= nbt })
else ledger.remove(caller)
}
}
entry
nat
constant
?.
require
>=
effect
caller
update
remove
Parameter
Fails with
"FA2_INSUFFICIENT_BALANCE"
nbt
is greater than the number of tokens owned by caller
.Related
entry burn(tid : nat, nbt : nat) {
constant {
amount ?is ledger[(caller, tid)]?.lamount otherwise FA2_INSUFFICIENT_BALANCE
}
require {
fa2_r6: is_not_paused();
fa2_r7: amount >= nbt otherwise FA2_INSUFFICIENT_BALANCE
}
effect {
if (amount > nbt) then
ledger.update((caller, tid), { lamount -= nbt })
else ledger.remove((caller, tid))
}
}
entry
nat
constant
?.
require
>=
effect
caller
update
remove
Parameters
Fails with
"FA2_INSUFFICIENT_BALANCE"
nbt
is greater than the number of tokens owned by 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))
- NFT
- Fungible
- Multi asset
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)
})
}
getter balance_of (requests : list<balance_of_request>) : list<balance_of_response> {
return map(requests, br ->
let b =
if br.btoken_id <> TOKEN_ID then 0
else (ledger[br.bo_owner] ? the.lamount : 0) in
{
request = br;
balance_ = b
})
}