FA2 Token Development (Tezos)

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
FA2 Token Development (Tezos)
Medium
~2-3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

FA2 Token Development (Tezos)

FA2 (TZIP-12) is multi-asset token standard in Tezos ecosystem. Unlike ERC-20/ERC-721, one FA2 contract can manage fungible and non-fungible tokens simultaneously with arbitrary token_id. This is architectural solution: instead of deploying separate contract for each token — one contract for entire asset portfolio.

Tezos smart contracts are written primarily in SmartPy or LIGO. Runtime is Michelson. Understanding Michelson is needed for debugging and gas optimization, but writing directly in it isn't necessary.

FA2 Specification: mandatory interface

Transfer entrypoint

Key entrypoint — transfer. Takes list transfer_batch: each batch describes sender, list of recipients and token_id/amount.

# SmartPy: FA2 transfer type
# transfer_params: list of {from_: address, txs: list of {to_: address, token_id: nat, amount: nat}}

@sp.entrypoint
def transfer(self, batch):
    for transfer in batch:
        from_ = transfer.from_
        # Check authorization: sender himself or approved operator
        sp.verify(
            (from_ == sp.sender) | 
            self.data.operators.contains(sp.record(
                owner=from_, 
                operator=sp.sender, 
                token_id=transfer.txs[0].token_id  # per-token operator
            )),
            "FA2_NOT_OPERATOR"
        )
        for tx in transfer.txs:
            self._transfer_tokens(from_, tx.to_, tx.token_id, tx.amount)

Operators in FA2 are third parties whom owner allows to manage his tokens. Analogue of setApprovalForAll in ERC-721, but more granular: can give permission for specific token_id.

Balance_of view

@sp.onchain_view()
def balance_of(self, requests):
    # requests: list of {owner: address, token_id: nat}
    # returns: list of {request: ..., balance: nat}
    results = []
    for req in requests:
        balance = self.data.ledger.get(
            sp.record(owner=req.owner, token_id=req.token_id), 
            default=0
        )
        results.append(sp.record(request=req, balance=balance))
    return results

This is on-chain view — doesn't change state, called from other contracts or off-chain. Tezos differs from EVM in that views are explicitly declared as onchain_view.

Update_operators

@sp.entrypoint
def update_operators(self, updates):
    for update in updates:
        update.match_cases({
            "add_operator": lambda op: self._add_operator(op),
            "remove_operator": lambda op: self._remove_operator(op),
        })

def _add_operator(self, op):
    # Only owner can add operators for his tokens
    sp.verify(op.owner == sp.sender, "FA2_NOT_OWNER")
    self.data.operators.add(sp.record(
        owner=op.owner, 
        operator=op.operator, 
        token_id=op.token_id
    ))

Complete FA2 implementation on SmartPy

import smartpy as sp

FA2_ERRORS = sp.utils.import_script_from_url(
    "https://smartpy.io/templates/fa2_lib.py"
)

@sp.module
def main():
    class FA2Token(
        FA2_ERRORS.Fungible,    # fungible token mixin
        FA2_ERRORS.Admin,       # admin control
        FA2_ERRORS.MintFungible,
        FA2_ERRORS.BurnFungible,
        sp.Contract
    ):
        def __init__(self, admin, metadata, token_metadata):
            FA2_ERRORS.Admin.__init__(self, admin)
            FA2_ERRORS.Fungible.__init__(self, {
                "ledger": sp.big_map(),          # (owner, token_id) → balance
                "operators": sp.big_map(),       # (owner, operator, token_id) → unit
            })
            self.init_metadata("metadata", metadata)
            
            # Initialize token 0
            self.data.token_metadata = sp.big_map({
                0: sp.record(
                    token_id=0,
                    token_info=token_metadata
                )
            })
            self.data.supply = sp.big_map({0: 0})
        
        @sp.entrypoint
        def mint(self, to_, token_id, amount):
            sp.verify(sp.sender == self.data.admin, "NOT_ADMIN")
            # Update recipient balance
            key = sp.record(owner=to_, token_id=token_id)
            current = self.data.ledger.get(key, default=0)
            self.data.ledger[key] = current + amount
            # Update total supply
            self.data.supply[token_id] = self.data.supply.get(token_id, default=0) + amount

Storage: big_map vs map

In Tezos important to distinguish map and big_map:

  • map — stored fully in memory when executing. Expensive in gas for large collections.
  • big_map — lazy loaded. When accessing key you pay only for reading that key. For ledger with thousands of addresses — big_map is mandatory.
# Correct: big_map for ledger
self.data.ledger = sp.big_map(
    tkey=sp.TRecord(owner=sp.TAddress, token_id=sp.TNat),
    tvalue=sp.TNat
)

# Incorrect for production: map recalculated fully on each access
# self.data.ledger = sp.map(...)  — only for small collections

Tzip-16: token metadata

TZIP-16 is metadata standard for Tezos contracts. Metadata stored in contract storage or linked to IPFS/HTTPS.

# Contract metadata (TZIP-16)
contract_metadata = {
    "name": "My FA2 Token",
    "description": "Utility token for platform",
    "interfaces": ["TZIP-012", "TZIP-016"],
    "homepage": "https://example.com"
}

# Token metadata (TZIP-021 for NFT)
token_metadata = {
    "name": sp.utils.bytes_of_string("MyToken"),
    "symbol": sp.utils.bytes_of_string("MTK"),
    "decimals": sp.utils.bytes_of_string("6"),
    "description": sp.utils.bytes_of_string("Utility token"),
    "thumbnailUri": sp.utils.bytes_of_string("ipfs://Qm...")
}

Standard requires storing values as bytes (UTF-8 encoded). Tools (wallets, explorers) decode back to strings. Creates inconvenience in code, but ensures compatibility.

Multi-asset: multiple tokens in one contract

FA2's strength is token_id. One contract, three tokens:

# Token 0: governance token (fungible)
# Token 1: utility token (fungible)
# Token 2-10000: NFT collection (non-fungible, amount=1 always)

# On NFT mint
@sp.entrypoint
def mint_nft(self, to_, token_id, metadata):
    sp.verify(sp.sender == self.data.admin, "NOT_ADMIN")
    sp.verify(~self.data.ledger.contains(
        sp.record(owner=to_, token_id=token_id)), "NFT_EXISTS")
    self.data.ledger[sp.record(owner=to_, token_id=token_id)] = 1
    self.data.token_metadata[token_id] = sp.record(
        token_id=token_id,
        token_info=metadata
    )

For NFT: never mint amount > 1 for one token_id. This is convention, not protocol-enforced.

Testing with SmartPy

# SmartPy unit test
@sp.add_test(name="FA2 Transfer Test")
def test():
    sc = sp.test_scenario(main)
    
    admin = sp.test_account("Admin")
    alice = sp.test_account("Alice")
    bob = sp.test_account("Bob")
    
    token = main.FA2Token(
        admin=admin.address,
        metadata={"": sp.utils.bytes_of_string("ipfs://...")},
        token_metadata={"symbol": sp.utils.bytes_of_string("MTK")}
    )
    sc += token
    
    # Mint 1000 tokens to Alice
    token.mint(to_=alice.address, token_id=0, amount=1000).run(sender=admin)
    
    # Alice transfers 100 tokens to Bob
    token.transfer([sp.record(
        from_=alice.address,
        txs=[sp.record(to_=bob.address, token_id=0, amount=100)]
    )]).run(sender=alice)
    
    # Verify balances
    sc.verify(token.balance_of([
        sp.record(owner=alice.address, token_id=0)
    ])[0].balance == 900)
    sc.verify(token.balance_of([
        sp.record(owner=bob.address, token_id=0)
    ])[0].balance == 100)

Deployment and tools

SmartPy IDE (smartpy.io) — browser IDE with simulator. Taquito — JavaScript library for Tezos interaction (ethers.js analogue). Ghostnet — main Tezos testnet. Temple Wallet / Kukai — wallets for testing.

// Taquito: interaction with FA2 contract
import { TezosToolkit } from "@taquito/taquito"

const Tezos = new TezosToolkit("https://ghostnet.ecadinfra.com")

const contract = await Tezos.contract.at("KT1...")

// Transfer tokens
const op = await contract.methods.transfer([{
  from_: "tz1Alice...",
  txs: [{ to_: "tz1Bob...", token_id: 0, amount: 100 }]
}]).send()

await op.confirmation(3)

Process

Requirements analysis (2-3 days). How many token_id, fungible or NFT or mixed, custom entrypoints (whitelist, vesting), TZIP-16/21 requirements.

Development (1-3 weeks). Basic FA2 contract → custom logic → tests → deploy on Ghostnet → verification through explorer (tzkt.io).

Integration (1-2 weeks). Taquito frontend or backend integration, wallet connection, transaction tracking.

Typical FA2 token with standard functionality — 1-2 weeks. With custom tokenomics (vesting, bonding curve, governance) — 3-6 weeks.