FA2
A unified token contract interface
There are many considerations when implementing a smart contract, especially one that is the basis for tokens, from which language to choose to what standard to implement. We have talked about smart contract development on Tezos and how to work with clients, let us now focus our attention on token standardization.
Let us first begin with a general definition of what is meant by a token in the blockchain world:
A token is a digital representation of an asset or utility.
In the past, the asset represented was often a financial asset, but tokenization isn't limited to only financial assets. Moreover, by the digitalization of a utility we mean tokens that are used for certain functionalities on-chain, so to say: The digital representation of a product and/or service for its holder.
The first challenge, when it comes to creating tokens, is deciding on the type of token. One can differentiate between fungible (like ERC-20 tokens), non-fungible (e.g. ERC-721) also known by their abbreviation NFT, and even semi-fungible tokens. The fungibility of a token might change throughout its lifecycle. In addition, tokens can differentiate in their transferability.
What is meant by fungibility when talking about tokens? Fungibility refers to an instance in which a token is interchangeable with other instances of that token or not. Non-fungible tokens are important when a token requires a certain degree of uniqueness to be valuable.
Now, one could ask: Why is standardization gaining track?
The demand for token standards has continuously risen, especially since the development of ERC standards and the STO wave. As a reaction, FA1.2 (TZIP-7), a token standard, was released in 2019 leading to implementations in among others SmartPy and LIGO as well as financial use cases for fungible token contracts.
FA: A unified token contract interface
FA2 is a token standard on Tezos. The standard is based on a proposal for a unified token contract interface. It addresses two aspects of importance to token standards: Token type and permission standardization.
It was meant to help support different token types. While FA1.2 requires multiple smart contracts for multiple tokens, FA2 allows single- and multi-token smart contracts for a variety of tokens. Thus, FA2 maintains the functionalities from the FA1.2 standard. At the same time, the creation of NFTs was taken into consideration, i.e. a greater potential for tokenization was added with the introduction of the FA2 standard. FA2 is agnostic to the token type and therefore, applicable to different types.
Take a look at the proposal for FA2 (TZIP-12).
The main aim was to create a standard that allows for a variety of implementations for different token types, while providing widely-applicable common interface standards to integrate wallets, exchanges, and external developers. This was especially challenging regarding the longevity to the standard: As Tezos is a self-amending protocol with evolving functionalities, FA2 needed to cover a broad spectrum to maintain its compatibility in the long term.
When it comes to the standardization of permissions, standardization brings the benefit of increasing trust in external smart contracts and centrally managed tokens. For this reason, FA2 standardizes:
- Transfer semantics,
- Metadata,
- Accessing balances,
- Total supply, and
- Permission rights.
Remember the role permissions play in the deployment pattern of a blockchain? If not, take a quick look at the section Main Ideas Behind the Tezos Protocol - Managing Chains.
When we talk about permissions regarding tokens, they can be understood as:
the rules that determine who can send how many tokens, receive them, and manage tokens for other users.
With FA2, those implementing the token contracts can configure freely:
- The token type(s),
- The token management (administration, whitelisting, etc.),
- The supply operations (minting and burning tokens),
- The permissioning architecture, and
- Questions on contract upgradability.
Specifically the permissioning architecture relates to whether permissioning is determined in the contract, i.e. a monolith, with a transfer hook to another contract, or a separate wrapper contract. Each of these options represents an FA2 implementation pattern for permissions.
FA2 permission implementation patterns
We can differentiate three implementation patterns:
- Monolith,
- Wrapper, and
- Transfer hook.
In a monolith smart contract implementation, the permissions are included in the FA2 token contract. This is often considered the most viable option for Tezos when considering gas efficiency because calls to a single contract are more efficient. Monolith architectures do have disadvantages: They are less modular and permissioning is less flexible. In addition, upgrading the permissioning often requires re-deploying the smart contract or a migration. FA2 addresses this last aspect by including specifications for operators and signing-off permissions for tokens to another contract, either generalized or application-specific permissioning contracts.
In a wrapper implementation, the separate wrapper contract applies permissions by forwarding calls to the main contract, which manages the token ledger. Wrappers allow for modularity and extend the functionalities of the main token contract. Moreover, they allow for easier upgrades and replacements. Wrapper approaches can lead to fragmentation and increase complexity.
When using transfer hooks, the main token contract calls another contract, which includes the permissioning specifications, i.e. "permissioning policies". Its main benefits are a separation of concerns, granular permissioning rules, and the possibility to upgrade permissions. At the same time, transfer hooks bring gas limitation concerns, as do wrappers. Hooks are trickier security-wise and are much more gas expensive than wrappers. Additionally, permissioning policies are more complex and the contract size increases with contract use because of increasing permissioning policies.
FA2 entrypoints and data structures
FA2 formalizes token design. Thus, a specific list of entrypoints and related data structures have to be implemented.
Therefore, every contract must contain the following entrypoints:
Transfer
,Balance_of
, andUpdate_operators
.
You can find an overview document of the semantics, metadata, a set of standard errors, and error mnemonics used for FA2 implementation here.
Implementations
It is recommended to audit such a token contract. Therefore it makes sense to look for community implementations. There is a LIGO/Michelson implementation and a SmartPy implementation. Let us try the SmartPy FA2 contract.
You can go to SmartPy IDE and pick up the FA2 contract template. You will see a lot of useful comments. The first definition is the FA2_config
class. It is used for metaprogramming, which we saw in the SmartPy section:
# The option `debug_mode` makes the code generation use
# regular maps instead of big-maps, hence it makes inspection
# of the state of the contract easier.
self.single_asset = single_asset
# This makes the contract save some gas and storage by
# working only for the token-id `0`.
...
So the configuration is flexible and offers an easily adjustable contract. Then you will see some type definitions, e.g. Batch_transfer
which represents a transfer batch.
Then you see the class FA2_core
which implements the FA2 strict standard. In it you can find the definition of the entrypoints we mentioned before:
@sp.entry_point
def transfer(self, params):
...
@sp.entry_point
def balance_of(self, params):
...
@sp.entry_point
def update_operators(self, params):
...
And:
self.token_meta_data = Token_meta_data(self.config)
With some other classes, which implement useful functions, it is inherited by the FA2
class.
class FA2(FA2_change_metadata, FA2_token_metadata, FA2_mint, FA2_administrator, FA2_pause, FA2_core)
In addition, FA2
class will implement:
@sp.offchain_view(pure = True)
def is_operator(self, query):
...
And:
self.init_metadata("metadata_base", metadata_base)
As well as:
sp.add_compilation_target("FA2_comp", FA2(config = environment_config(),
metadata = sp.metadata_of_url("https://example.com"),
admin = sp.address("tz1M9CMEtsXm3QxA7FmMU2Qh7xzsuGXVbcDr")))
We have not used the metadata standard for Tezos smart contracts, TZIP-16, so far with SmartPy. Some data can be stored off-chain with a link on-chain (as big_map %metadata string bytes
) to be easier accessible.
You can deploy and test such a contract to play with it. Here let us have a quick look at the tests this implementation includes. Those will let you remember the FA2 definition.
Have a look at the multi token transfer test:
scenario.h3("Multi-token Transfer Bob -> Alice")
scenario += c1.transfer(
[
c1.batch_transfer.item(from_ = bob.address,
txs = [
sp.record(to_ = alice.address,
amount = 10,
token_id = 0),
sp.record(to_ = alice.address,
amount = 10,
token_id = 1)]),
# We voluntarily test a different sub-batch:
c1.batch_transfer.item(from_ = bob.address,
txs = [
sp.record(to_ = alice.address,
amount = 10,
token_id = 2)])
]).run(sender = bob)
How would such transactions look like in Michelson? We can easily see it in the IDE:
{
"prim": "Right",
"args": [
{
"prim": "Right",
"args": [
{
"prim": "Left",
"args": [
[
{
"prim": "Pair",
"args": [
{ "string": "tz1Ns3YQJR6piMZ8GrD2iYu94Ybi1HFfNyBP" },
[
{ "prim": "Pair", "args": [ { "string": "tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi" }, { "prim": "Pair", "args": [ { "int": "0" }, { "int": "10" } ] } ] },
{ "prim": "Pair", "args": [ { "string": "tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi" }, { "prim": "Pair", "args": [ { "int": "1" }, { "int": "10" } ] } ] }
]
]
},
{
"prim": "Pair",
"args": [
{ "string": "tz1Ns3YQJR6piMZ8GrD2iYu94Ybi1HFfNyBP" },
[ { "prim": "Pair", "args": [ { "string": "tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi" }, { "prim": "Pair", "args": [ { "int": "2" }, { "int": "10" } ] } ] } ]
]
}
]
]
}
]
}
]
}
You can see that we transfer from_
the address tz1Ns3YQJR6piMZ8GrD2iYu94Ybi1HFfNyBP
to_
:
tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi
10 tokens of type 0;tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi
10 tokens of type 1;tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi
10 tokens of type 2.