B9lab Logo
Tezos Developer Portal
Developer PortalDeveloper Portal

FA2

A unified token contract interface


Reading Time: 247 min

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's now focus our attention on token standardisation.

Let's first begin with a general definition on 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 tokenisation isn't limited to only financial assets. Moreover, by the digitalisation of an 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 fungability 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 standardisation 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 standardisation.

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 tokenisation was added with the introdcution of the FA2 standard. FA2 is agnostic to the token type and therefore, applicable to different types.

info icon

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 in regard to 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 standardisation of permissions, standardisation brings the benefit of increasing trust in external smart contracts and centrally managed tokens. For this reason, FA2 standardises:

  • transfer semantics,
  • metadata,
  • accessing balances,
  • total supply, and
  • permission rights.
info icon

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 this 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 as 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 a generalised 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 extending 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 entry points and data structures

FA2 formalises token design. Thus, a specific list of entry points and related data structures have to be implemented.

Therefore, every contract must contain the following entry points:

  • Transfer
  • Balance_of
  • Update_operators
tip icon

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's 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 easy 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 entry points 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. Basically 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.
reading icon
Discuss on Slack
Rate this Page
Would you like to add a message?
Submit
Thank you for your Feedback!