blank Skip to main content

Tezos Token Standards: Practical Examples of Implementing FA1.2 and FA2 Tokens

Tokenization is a game changer in the blockchain world. People can tokenize all kinds of assets, from traditional currencies to real estate and even artworks. The challenge is to tokenize these assets while making ownership management flexible and easy.

In this article, we talk about the specifics of asset tokenization in Tezos, the types of tokens this platform supports, and token standards it enforces. We discuss key differences between FA2 and FA1.2, the two key token standards in Tezos, and analyze their implementation based on a practical example.

This article will be useful to blockchain professionals exploring token implementation options on Tezos.

The need for token standardization

In this article, we discuss tokens as crypto assets and not native cryptocurrencies. As crypto assets, tokens are units of value that blockchain-based organizations or projects develop on top of existing blockchain networks. Tokens are multifunctional by their nature, allowing you to tokenize almost anything.

Take the gaming industry. In video games, while a player pays real money for in-game items, they don’t get ultimate ownership over these items. For instance, the game’s developers can ban a player’s account and thus restrict the player’s access to all purchased items. However, with tokenized in-game items, once a player pays for an item, they become its true and only owner. Furthermore, since the transaction history is fully recorded on the blockchain and can’t be tampered with, the ownership over a particular asset becomes easy to prove and validate, solving the problem of item duplication and misuse.

With the help of tokens, you can digitize anything valuable in the material world and trade it as cryptocurrency. For example, there are tokens tied to other currencies, like ETHtz and USDtez. At the same time, you can tokenize valuable possessions like pieces of art and copyrights using non-fungible tokens (NFTs).

Fungibility is the property of something being interchangeable with other similar assets or goods, and it is one of the criteria commonly used to distinguish between different types of tokens:

 types of tokens

Related services

Blockchain Consulting and Development Services

However, working with different types of tokens can be challenging for developers. You can write a custom smart contract with rich token processing functionality. But if other contracts and decentralized applications (dApps), like marketplaces or exchanges, don’t know how to interact with it, your contract may be useless.

One way to solve this problem is by writing a separate smart contract that implements a unique approach for dApps to interact with your contract. However, it will be more efficient to follow a unified approach when creating, issuing, and deploying new tokens on particular platforms. That’s what token standards are for — they set common rules that ensure smooth interactions between different contracts operating with tokens.

Each blockchain platform has its own set of token standards, and in this article, we discuss the key token standards of the Tezos blockchain.

Read also:
Tezos Blockchain and Smart Contract Overview

Tezos token standards overview

As a relatively new blockchain platform, Tezos draws attention from both developers and businesses. Recently, Ubisoft joined Tezos as a corporate baker. Also, three Swiss companies — Crypto Finance AG, InCore Bank, and Inacta — have announced that they will jointly use the Tezos blockchain to tokenize regulated assets using the DAR-1 token standard, which is reportedly based on the Tezos FA2 standard.

Token standards available in Tezos are described in the Tezos Interoperability Proposal (TZIP) documents that you can find in the platform’s repository on GitLab.

Among other TZIPs, token standards are identified as Financial Applications (FA). There are three completed versions of the FA Tezos token standards:

  • FA1 (TZIP 5 Abstract Ledger)
  • FA1.2 (TZIP 7 Approvable Ledger)
  • FA2 (TZIP 12 Multi-Asset Interface)

While the FA1 standard is already deprecated, the other two standards have been successfully implemented in SmartPy and LIGO. For more details about the current statuses and implementation references of these token standards, see Table 1 below.

Table 1: Tezos token standards overview

TZIPTitleCreation DateStatusLIGOSmartPy
TZIP-005FA1 — Abstract Ledger12 April 2019Deprecated
TZIP-007 FA1.2 — Approvable Ledger20 June /2019FinalFA1.2SmartPy IDE – FA1.2 
TZIP-012  FA2 — Multi-Asset Interface24 January 2020FinalFA2SmartPy IDE – FA2

Now, let’s compare Tezos standards and take a closer look at each of them.

FA1 Abstract Ledger

FA1 was the first Tezos token standard. This standard was basically a minimalist version of the ledger, hence its name. The main purpose of FA1 was to map identities to balances and provide interactions with fungible assets for contract developers, libraries, client tools, etc.

In contrast to the object-oriented programming paradigm, there’s no mandatory inheritance between Tezos token standards, even though the FA1 standard contains the word “abstract” in its name. All subsequent standards don’t have to be compatible with FA1.

FA1.2 Approvable Ledger

The FA1.2 standard is a symbiosis between the FA1 standard and the EIP-20 standard used in Ethereum. The key mechanism of the FA1.2 standard is the ability to approve the spending of tokens from other accounts. However, this standard can only be used for fungible tokens.

When implementing a token based on this standard, you must include all of the following entry points in its interface:

Table 2: Mandatory interface entry points of the FA1.2 standard

(address :from, (address :to, nat :value)) transfer
(address :spender, nat :value)approve
(view (address :owner, address :spender) nat)getAllowance
(view (address :owner) nat)getBalance
(view unit nat)getTotalSupply

FA1.2 doesn’t prohibit developers from extending the token contract with additional functionality. For example, the SmartPy template of FA1.2 has additional entry points for minting and burning tokens, governance management, and so on.

FA2 — Multi-Asset Interface

The FA2 standard is the most recent Tezos token standard. 

How does FA1.2 differ from FA2? It’s important to understand that FA2 can’t be seen as a direct heir to FA1.2 for several reasons:

  1. In contrast to the FA1.2 standard, FA2 supports multiple assets, enabling the coexistence of different types of tokens such as fungible and non-fungible. In Ethereum, the same possibility is implemented in the EIP-1155 multi-token standard.
  2. The FA2 standard issues token transfer permissions differently than FA1.2. In FA2, permission can be granted using the update_operators entry point. In the FA2 specification, an operator is an address that can create transactions on behalf of the owner (whose address already stores tokens).
  3. The interface of the FA2 standard includes the following necessary entry points:

Table 3: Interface entry points of the FA2 standard

(list :transfer



(address :from_)

(list :txs


(address :to_)


(nat :token_id)

(nat :amount)






(pair :balance_of


(list :requests


(address :owner)

(nat :token_id)



(contract :callback



(pair :request

(address :owner)

(nat :token_id)


(nat :balance)





(list :update_operators



(pair :add_operator

(address :owner)


(address :operator)

(nat :token_id)



(pair :remove_operator

(address :owner)


(address :operator)

(nat :token_id)





(pair (address :owner) (nat :token_id))getBalance
(nat :token_id) (nat :supply)total_supply
() ((list nat))all_tokens

Now that we know all the key aspects of the existing standards in Tezos, it’s time for the practical part. Since the FA1 standard is already deprecated, we will focus our attention on the other two standards — FA1.2 and FA2 — and write a smart contract that can interact with both of them.

Read also:
How to Implement a Custom Blockchain for Your Business: Graphene Framework

Writing a marketplace smart contract for Tezos

Let’s create a universal smart contract that allows you to buy and sell an asset, regardless of the token standard. We’ll use SmartPy for writing this smart contract, since it can be compiled in Michelson. You can find the full code of this smart contract here.

Note: In order to reduce the code volume and simplify the smart contract’s logic, we identify sales by IDs.

Here are the entry points that define the main business logic of our smart contract:

  • (A) registerMarket (tokenAddress, tokenType) — registers a new market (token) that users will interact with
  • (A) removeMarket (tokenAddress) — removes the market
  •  sellAsset (tokenAddress, tokenId, amount, price) — puts your own asset up for sale and assigns a unique ID to this sale
  • cancelSale (saleId) — cancels the sale; you can only cancel your own sale or one you have admin-level access to
  • buyAsset (saleId) — enables you to buy an asset with a certain saleId; for the sale to take place, you must also pass the number of mutez that matches the price specified when calling sellAsset ()

Note: (A) means that only the admin can send this entry point.

The business logic of our marketplace includes three main stages:

  1. The marketplace admin registers new markets.
  2. One user account puts up a token for sale (remembers the ID of the sale).
  3. The second account, which is going to buy the token, specifies the ID of the sale and pays the mutez according to the price.

Here’s what it looks like from the admin’s and the user’s points of view:

 smart contract logic on Tezos

As the result of a successful sale, the token will go through the following transfer path:

 token sale path

In addition to the business logic, the contract also has governance logic, which restricts access to the registerMarket and removeMarket entry points for non-admin users.

This logic can be described in a separate class that will look like this:

class Governance:
    def isAdministrator(self, sender):
        return sender == self.data.administrator
    def verifyAdministrator(self, sender):
        sp.verify(self.isAdministrator(sender), self.error.notAdmin())
    def setAdministrator(self, administrator):
        sp.set_type(administrator, sp.TAddress)
        self.data.administrator = administrator

You may have noticed the call to self.error.notAdmin(). Since any additional string in the code means extra gas costs, it’s better to use an error code (a short and unique name) instead of strings.

The snippet below demonstrates such an approach:

class Errors:
    def __init__(self):
        self.prefix = "TIOF_" # TIOF - abbreviation from tokenization is our future
    def make(self, s): return (self.prefix + s)
    def notRegistered(self):            return self.make("NOT_REGISTERED")
    def notAdmin(self):                 return self.make("NOT_ADMIN")
    def notAdminOrSeller(self):         return self.make("NOT_ADMIN_OR_SELLER")
    def nonExistentMarket(self):        return self.make("NOT_EXISTENT_MARKET")
    def alredyRegisteredMarket(self):   return self.make("ALREADY_REGISTERED")
    def nonExistentSale(self):          return self.make("NOT_EXISTENT_SALE")
    def priceMismatch(self):            return self.make("PRICE_MISMATCH")

The code above followed the self-documentation approach; therefore, instead of magic numbers, a list of constants was given:

# Constants
TTokenType = sp.TBounded([FA_1_2_TOKEN_TYPE, FA_2_TOKEN_TYPE])

The TTokenType parameter is limited to a set of values [FA_1_2_TOKEN_TYPE, FA_2_TOKEN_TYPE]. The sp.TBounded parameter prohibits the use of constants outside the subset.

The business logic calls for making token transactions between addresses. For this reason, we created the generic TransferTokens class:

class TransferTokens:
    def transferFA2(self, sender,receiver,amount,tokenAddress,id):
    def transferFA12(self, sender,receiver,amount,tokenAddress): 
    def transferTokenGeneric(self, params):
        sp.set_type(params, sp.TRecord(sender = sp.TAddress, receiver = sp.TAddress, amount = sp.TNat,
         tokenAddress = sp.TAddress, id = sp.TNat, tokenType = TTokenType)) 
        sp.if params.tokenType == sp.bounded(FA_2_TOKEN_TYPE): 
            self.transferFA2(params.sender, params.receiver, params.amount, params.tokenAddress, params.id)
            self.transferFA12(params.sender, params.receiver, params.amount, params.tokenAddress)

Now the utility functionality is ready. Let’s move on to the Marketplace contract:

class Marketplace(sp.Contract, Governance, TransferTokens):
    def __init__(self, administrator):
        self.error = Errors()
        self.init(administrator = administrator,
    # key - token address. Records consist of the type of token (FA1.2 or FA2) and a set of active sales
        markets = sp.big_map(l = {}, tkey = sp.TAddress,
          tvalue = sp.TRecord(tokenType = TTokenType,
            sales = sp.TSet(sp.TNat))),
        saleCounter = sp.nat(0), # counter for saleId generation
        sales = sp.big_map(l = {}, tkey = sp.TNat, # key - saleId
                    tvalue = sp.TRecord(tokenAddress = sp.TAddress,
                        tokenId = sp.TNat, 
                        amount = sp.TNat,
                        price = sp.TMutez,
                        seller = sp.TAddress)))
    def registerMarket(self, params):
        sp.set_type(params, sp.TRecord(tokenAddress = sp.TAddress, tokenType = TTokenType))
        self.data.markets[params.tokenAddress] = sp.record(tokenType = params.tokenType, sales = sp.set([]))
    def removeMarket(self, tokenAddress):
        sp.set_type(tokenAddress, sp.TAddress)
        sp.for saleId in self.data.markets[tokenAddress].sales.elements():
            del self.data.sales[saleId]
        del self.data.markets[tokenAddress]
    def buyAsset(self, saleId):
        sp.set_type(saleId, sp.TNat)
        sp.verify(sp.amount == self.data.sales[saleId].price, self.error.priceMismatch())
        self.transferTokens(sp.self_address, sp.sender, self.data.sales[saleId].amount, self.data.sales[saleId].tokenAddress, self.data.sales[saleId].tokenId)
        sp.send(self.data.sales[saleId].seller, self.data.sales[saleId].price)
    def sellAsset(self, params):
        sp.set_type(params, sp.TRecord(tokenAddress = sp.TAddress, tokenId = sp.TNat, amount = sp.TNat, price = sp.TMutez))
        self.transferTokens(sp.sender, sp.self_address, params.amount, params.tokenAddress, params.tokenId)
        saleId = self.makeSaleId()
        self.data.sales[saleId] = sp.record(tokenAddress =  params.tokenAddress, tokenId = params.tokenId, amount = params.amount,
price = params.price, seller = sp.sender)
    def cancelSale(self, saleId):
        sp.set_type(saleId, sp.TNat)
        self.verifyAdminOrSeller(sp.sender, saleId)
    def verifySaleExists(self, saleId):
        sp.verify(self.data.sales.contains(saleId), self.error.nonExistentSale())
    def verifyMarketNotExists(self, tokenAddress):
        sp.verify(~ self.isMarketExistent(tokenAddress), self.error.alredyRegisteredMarket())
    def verifyMarketExists(self, tokenAddress):
        sp.verify(self.isMarketExistent(tokenAddress), self.error.nonExistentMarket())
    def verifyAdminOrSeller(self, sender, saleId):
        sp.verify(self.isAdministrator(sp.sender) | (sender == self.data.sales[saleId].seller),
    def transferBackTokens(self, saleId):
    def transferTokens(self, sender, receiver, amount, tokenAddress,id):
        params = sp.record(sender = sender, receiver = receiver, amount = amount, tokenAddress = tokenAddress, id = id, tokenType = self.getTokenType(tokenAddress))
    def isMarketExistent(self, tokenAddress):
        return self.data.markets.contains(tokenAddress)
    def getTokenType(self, tokenAddress):
        return self.data.markets[tokenAddress].tokenType
    def removeSale(self, saleId):                             self.data.markets[self.data.sales[saleId].tokenAddress].sales.remove(saleId)
        del self.data.sales[saleId]
    def makeSaleId(self):
        self.data.saleCounter += 1
        return self.data.saleCounter

Now the code of our marketplace smart contract is ready and it’s time to check how it interacts with FA1.2 and FA2 tokens.

Read also:
Smart Contract Security Audit: Penetration Testing and Static Analysis

Analyzing smart contract interactions with tokens

To see how our smart contract interacts with different types of tokens, let’s analyze three tokenization use cases and present them in unit tests:

  1. Ounces of silver. These assets are fungible, so both token standards are suitable for this use case. In our example, we use the FA1.2 standard.
  2. Collection of gallery masterpieces. Artworks can’t be called interchangeable, and the gallery is not limited to one masterpiece. For this use case, we can only use NFTs and the FA2 token standard.
  3. An ICO platform. On this platform, anybody can create a product (a concert, art, etc.) and look for investors willing to support it. The product is tokenized and investors can buy tokens in order to invest in that product. Once the product’s life cycle has concluded, tokens can be sold back for a share of the profits. This platform entails the creation of various types of tokens. If you use FA1.2 here, you will have to create a new contract for every new product, which is quite expensive and inconvenient. In addition, there may be a need for NFTs. Therefore, the FA2 standard is better suited.

SmartPy has a built-in scenario unit testing tool that you can use to check a smart contract’s performance. Another option is to deploy contracts and analyze them manually, but this method is less convenient than unit testing. 

Now let’s create three tokens in a test scenario and mint the initial balances.

Read also:
NFT for Business: Use Cases, Benefits, and Nuances to Consider

A fungible FA1.2 token for ounces of silver

We’ll start with creating a fungible FA1.2 token for operating with ounces of silver.

@sp.add_test(name = "MarketPlace test")
def test():
        scenario = sp.test_scenario()
        admin = sp.test_account("Administrator")
        alice = sp.test_account("Alice")
        bob   = sp.test_account("Bob")
        # display accounts:
        scenario.show([admin, alice, bob])
        scenario.h1("Contracts initialization")
        marketPlace = MarketPlace(administrator = admin.address)
        scenario += marketPlace
        scenario.h2("FA1.2 token - fungible silver ounces")
        token_metadata = {
            "decimals"    : "0",
            "name"        : "ounces of silver",   
            "symbol"      : "Silver",              # Silver ounce
            "icon"        : 'https://smartpy.io/static/img/logo-only.svg'
        contract_metadata = {
            "" : "ipfs://QmaiAUj1FFNGYTu8rLBjc3eeN9cSKwaF8EGMBNDmhzPNFd",
        fa12 = FA12.FA12(
            config = FA12.FA12_config(support_upgradable_metadata = True),
                token_metadata      = token_metadata,
                contract_metadata   = contract_metadata
        scenario += fa12
        scenario.h3("Initial Minting for Alice")
        fa12.mint(address = alice.address, value = 50).run(sender = admin)

The FA1.2 token is ready, and the admin has minted 50 ounces of silver. Now, for Alice to be able to put her tokens up for sale, she needs to give the marketplace approval for managing her balance:

scenario.h3("Approve interaction with Alice’s balance for marketplace")
fa12.approve(spender = marketPlace.address, value = 50).run(sender = alice)

This is how the marketplace contract interacts with the FA1.2 token:

scenario.h2("Interaction with FA1.2 token (silver ounces)")
scenario.h4("[Error] Common user trying to register market")
params = sp.record(tokenAddress = fa12.address, tokenType = sp.bounded(FA_1_2_TOKEN_TYPE))
marketPlace.registerMarket(params).run(sender = alice, valid = False)
scenario.h4("Register FA1.2 token")
marketPlace.registerMarket(params).run(sender = admin)
scenario.h4("Alice sells 5 ounces of silver for 1 000 mutez")
params = sp.record(tokenAddress = fa12.address, tokenId = 0, amount = sp.nat(5), price = sp.mutez(1000))
marketPlace.sellAsset(params).run(sender = alice)
lastSaleId = getLastSaleId(marketPlace)
scenario.h4("Bob buys 5 ounces of silver for 1 000 mutez")
marketPlace.buyAsset(lastSaleId).run(sender = bob, amount = sp.mutez(1000))

Now, let’s see how to implement FA2 tokens in Tezos.

Read also:
Decentralized Finance (DeFi) Solutions: Benefits, Challenges, and Best Practices to Build One

An NFT for a gallery’s collection of masterpieces

In this example, we create an FA2 NFT and mint two masterpieces for Alice and Bob:

scenario.h2("FA2 NFT - Gallery's collection of masterpieces")
config = FA2.FA2_config(non_fungible = True)
fa2NFT = FA2.FA2(config = config,
    metadata = sp.utils.metadata_of_url("https://example.com"),
    admin = admin.address)
scenario += fa2NFT
scenario.h3("Initial Minting")
scenario.p("The administrator mints one token-0 and one token-1.")
tokenMetadata = FA2.FA2.make_metadata(
    name = "Mona Lisa - Leonardo da Vinci",
    decimals = 0,
    symbol= "LDVMona" )
monaLisaTokenId = 0
fa2NFT.mint(address = alice.address,
                    amount = 1,
                    metadata = tokenMetadata,
                    token_id = 0).run(sender = admin)
tokenMetadata = FA2.FA2.make_metadata(
    name = "Guernica - Pablo Picasso",
    decimals = 0,
    symbol= "PPGuernica" )
guernicaTokenId = 1
fa2NFT.mint(address = bob.address,
                    amount = 1,
                    metadata = tokenMetadata,
                    token_id = 1).run(sender = admin)

Similarly to FA1.2, FA2 tokens can only manage a user’s balance when given permission. However, this time you permit interactions with the user’s balance not by giving approval but by adding a special operator. An operator doesn’t require specifying a certain number of tokens, giving unlimited access to the user’s balance until the operator is removed.

Here’s a snippet of adding an operator:

scenario.h3("Alice gives an operator for marketPlace")
    sp.variant("add_operator", fa2NFT.operator_param.make(
        owner = alice.address,
        operator = marketPlace.address,
        token_id = 0))
]).run(sender = alice)

And this is how our marketplace contract interacts with the FA2 NFT:

scenario.h2("Interaction with FA2 NFT - Gallery's collection of masterpieces")
scenario.h3("Register token")
params = sp.record(tokenAddress = fa2NFT.address, tokenType = sp.bounded(FA_2_TOKEN_TYPE))
marketPlace.registerMarket(params).run(sender = admin)
scenario.h4("Alice sells 'Mona Lisa' token")
params = sp.record(tokenAddress = fa2NFT.address, tokenId = monaLisaTokenId, amount = sp.nat(1), price = sp.mutez(24 * (10 ** 6)))
marketPlace.sellAsset(params).run(sender = alice) 
lastSaleId = getLastSaleId(marketPlace)
scenario.h4("Bob buys 'Mona Lisa' token")
marketPlace.buyAsset(lastSaleId).run(sender = bob, amount = sp.mutez(24 * (10 ** 6)))

An FA2 token for an ICO platform

Now, let’s create an FA2 token for a STAXE ICO platform where users can create products and seek investment for them. In this scenario, the initialization of a smart contract is similar to the scenario with gallery masterpieces, with only one difference — the non_fungible flag is disabled:

scenario.h2("FA2 multi-asset fungible token - STAXE ICO platform")
config = FA2.FA2_config()
fa2FungibleMultiAsset = FA2.FA2(config = config,
    metadata = sp.utils.metadata_of_url("https://staxe.io/creatives-en"),
    admin = admin.address)
scenario += fa2FungibleMultiAsset
scenario.h3("Initial minting")
scenario.p("The administrator mints invested tokens")
tokenMetadata = FA2.FA2.make_metadata(
    name = "Wake N’Wave Festival",
    decimals = 0,
    symbol= "MonarEP" )
fa2FungibleMultiAsset.mint(address = alice.address,
                    amount = 100,
                    metadata = tokenMetadata,
                    token_id = 0).run(sender = admin)
monarEPTokenId = 0
tokenMetadata = FA2.FA2.make_metadata(
    name = "Luna Llena - Plaza de Toros Las Ventas Madrid concert",
    decimals = 0,
    symbol= "LunaLlena" )
fa2FungibleMultiAsset.mint(address = bob.address,
                    amount = 40,
                    metadata = tokenMetadata,
                    token_id = 1).run(sender = admin)
scenario.p("Alice adds operator for marketPlace")
    sp.variant("add_operator", fa2FungibleMultiAsset.operator_param.make(
        owner = alice.address,
        operator = marketPlace.address,
        token_id = monarEPTokenId))
]).run(sender = alice)

In our examples above, we initialized four smart contracts: a marketplace smart contract, one contract for an FA1.2 token, and two contracts for FA2 tokens. 

Read also:
Hyperledger Fabric: Concepts, Configuration, and Deployment


When implementing new smart contracts and tokens, developers should first learn about the mandatory requirements of the platform they’re working with. In Tezos, you can choose between two token standards — FA1.2 and FA2. The first will work best for tokenizing traditional fungible assets, such as currencies and precious metals. The FA2 standard, in turn, can be used for operating with both fungible tokens and NFTs.

At Apriorit, we have a team of passionate blockchain developers with deep knowledge of Tezos, Ethereum, Hyperledger Fabric, and other platforms. Get in touch with us to start discussing your blockchain project right away!

Tell us about your project

Send us a request for proposal! We’ll get back to you with details and estimations.

By clicking Send you give consent to processing your data

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us