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.
Contents
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:
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.
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. In collaboration with Ubisoft, Tezos has introduced fully-decentralized NFT smart contracts. 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.
Tezos supports multiple smart contract languages and has a robust NFT ecosystem. It continues to scale through EVM- and WASM-compatible features, making it a versatile platform for various types of decentralized applications.
In June 2023, Tezos deployed its 14-th upgrade, known as Nairobi, which has increased transaction speeds by up to eight times. This upgrade is a significant milestone in Tezos’ journey toward scalability and efficiency.
In 2023, Tezos is hosting its Premier Developer Conference, Tez/Dev, focusing on harnessing Tezos for success. The conference aims to bring together core development teams to discuss recent and future developments, including scalability features set to come with the Mumbai upgrade, targeting 1 million transactions per second.
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.
In June 2023, the Tezos protocol was successfully upgraded, contributing to the development and growth of Tezos in a collaborative environment globally.
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). Now deprecated
- FA1.2 (TZIP 7 Approvable Ledger). Last updated on June 29, 2023
- FA2 (TZIP 12 Multi-Asset Interface). Last updated on July 10, 2023
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
TZIP | Title | Creation Date | Status | LIGO | SmartPy |
---|---|---|---|---|---|
TZIP-005 | FA1 โ Abstract Ledger | 12 April 2019 | Deprecated | โ | โ |
TZIP-007 | FA1.2 โ Approvable Ledger | 20 June 2019 | Final | FA1.2 | SmartPy IDE – FA1.2 |
TZIP-012 | FA2 โ Multi-Asset Interface | 24 January 2020 | Final | FA2 | SmartPy IDE – FA2 |
TZIP-021 | DeFi Token Standard | 15 June 2023 | Draft | TZIP-21 | SmartPy IDE โ TZIP-21 |
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
Parameters | Name |
---|---|
(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:
- 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.
- 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).
- The interface of the FA2 standard includes the following necessary entry points:
Table 3: Interface entry points of the FA2 standard
Parameters | Name |
---|---|
(list :transfer (pair (address :from_) (list :txs (pair (address :to_) (pair (nat :token_id) (nat :amount) ) ) ) ) ) | transfer |
(pair :balance_of (list :requests(pair (address :owner) (nat :token_id) ) ) (contract :callback (list (pair (pair :request (address :owner) (nat :token_id) ) (nat :balance) ) ) ) ) | balance_of |
(list :update_operators (or (pair :add_operator (address :owner) (pair (address :operator) (nat :token_id) ) ) (pair :remove_operator (address :owner) (pair (address :operator) (nat :token_id) ) ) ) ) | update_operators |
(pair (address :owner) (nat :token_id)) | getBalance |
(nat :token_id) (nat :supply) | total_supply |
() ((list nat)) | all_tokens |
TZIP-21 โ DeFi Token Standard
TZIP-21 is the latest addition to the Tezos token standards, specifically designed for decentralized finance (DeFi) applications. This standard aims to provide a unified interface for DeFi tokens, making it easier for developers to create financial products on the Tezos blockchain. TZIP-21 includes features like interest-bearing tokens and flash loans, among others. It’s currently in the draft stage and is expected to be finalized later this year.
In addition to the existing token standards, Tezos continues to innovate in other areas. In Q2 2023, Tezos made significant strides in its rollup infrastructure roadmap, introducing WASM rollups. This development aims to enhance the scalability and efficiency of the Tezos blockchain. Additionally, Tezos introduced the Tezos Ecosystem DAO, aimed at managing and distributing XTZ to support community initiatives. These advancements signify Tezos’ commitment to fostering a robust and versatile blockchain ecosystem. In July 2023, Tezos introduced new technical features such as Data Availability Layer (DAL) and Etherlink. These features were revealed at TezDev 2023 and are expected to further enhance the blockchain’s capabilities.
The Ithaca2 upgrade introduced a new consensus method called Tenderbake, enabling faster finality (verification of transactions). Additionally, the Granada upgrade brought liquidity baking to Tezos, laying the foundation for improved scalability.
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.
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.
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:
- The marketplace admin registers new markets.
- One user account puts up a token for sale (remembers the ID of the sale).
- 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:
As the result of a successful sale, the token will go through the following transfer 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. Tezos offers formal verification methods for added security in smart contracts.
This logic can be described in a separate class that will look like this:
class Governance(sp.Contract):
โฏ โฏ โฏ โฏ def __init__(self, administrator):
โฏ โฏ โฏ โฏ โฏ โฏ self.data.administrator = administrator
โฏ โฏ โฏ โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def isAdministrator(self, sender):
โฏ โฏ โฏ โฏ โฏ โฏ return sender == self.data.administrator
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def verifyAdministrator(self, sender):
โฏ โฏ โฏ โฏ โฏ โฏ assert self.isAdministrator(sender), "TIOF_NOT_ADMIN"
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.entry_point
โฏ โฏ โฏ โฏ def setAdministrator(self, administrator):
โฏ โฏ โฏ โฏ โฏ โฏ sp.cast(administrator, sp.address)
โฏ โฏ โฏ โฏ โฏ โฏ self.verifyAdministrator(sp.sender)
โฏ โฏ โฏ โฏ โฏ โฏ self.data.administrator = administrator
The code above followed the self-documentation approach; therefore, instead of magic numbers, a list of constants was given:
@sp.module
def main():
โฏ โฏ FA_1_2_TOKEN_TYPE = sp.nat(0)
โฏ โฏ FA_2_TOKEN_TYPE = sp.nat(1)
The business logic calls for making token transactions between addresses. For this reason, we created the generic TransferTokens class:
class TransferTokens(sp.Contract):
โฏ โฏ โฏ โฏ @sp.private()
โฏ โฏ โฏ โฏ def transferFA2(self, sender, receiver, amount, tokenAddress, id):
โฏ โฏ โฏ โฏ โฏ โฏ ...
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private()
โฏ โฏ โฏ โฏ def transferFA12(self, sender, receiver, amount, tokenAddress):
โฏ โฏ โฏ โฏ โฏ โฏ ...
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.entrypoint
โฏ โฏ โฏ โฏ def transferTokenGeneric(self, params):
โฏ โฏ โฏ โฏ โฏ โฏ sp.cast(params, sp.record(sender = sp.address, receiver = sp.address, amount = sp.nat,
โฏ โฏ โฏ โฏ โฏ โฏ โฏtokenAddress = sp.address, id = sp.nat, tokenType = sp.nat))
โฏ โฏ โฏ โฏ โฏ โฏ if params.tokenType == FA_2_TOKEN_TYPE:
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ self.transferFA2((params.sender, params.receiver, params.amount, params.tokenAddress, params.id))
โฏ โฏ โฏ โฏ โฏ โฏ else:
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ 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(Governance, TransferTokens):
โฏ โฏ โฏ โฏ def __init__(self, administrator):
โฏ โฏ โฏ โฏ โฏ โฏ Governance.__init__(self, administrator)
โฏ โฏ โฏ โฏ โฏ โฏ TransferTokens.__init__(self)
โฏ โฏ โฏ โฏ โฏ โฏ # key - token address. Records consist of the type of token (FA1.2 or FA2) and a set of active sales
โฏ โฏ โฏ โฏ โฏ โฏ self.data.markets = sp.cast(sp.big_map(), sp.big_map[sp.address,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ sp.record(tokenType = sp.nat, sales = sp.set[sp.nat])])
โฏ โฏ โฏ โฏ โฏ โฏ self.data.saleCounter = sp.nat(0) # counter for saleId generation
โฏ โฏ โฏ โฏ โฏ โฏ self.data.sales = sp.cast(sp.big_map(), sp.big_map[sp.nat, # key - saleId
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ sp.record(tokenAddress = sp.address,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ tokenId = sp.nat,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ amount = sp.nat,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ price = sp.mutez,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ seller = sp.address)])
โฏ โฏ โฏ โฏ @sp.entrypoint
โฏ โฏ โฏ โฏ def registerMarket(self, params):
โฏ โฏ โฏ โฏ โฏ โฏ sp.cast(params, sp.record(tokenAddress = sp.address, tokenType = sp.nat))
โฏ โฏ โฏ โฏ โฏ โฏ self.verifyAdministrator(sp.sender)
โฏ โฏ โฏ โฏ โฏ โฏ self.verifyMarketNotExists(params.tokenAddress)
โฏ โฏ โฏ โฏ โฏ โฏ self.data.markets[params.tokenAddress] = sp.record(tokenType = params.tokenType, sales = sp.set())
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.entrypoint
โฏ โฏ โฏ โฏ def removeMarket(self, tokenAddress):
โฏ โฏ โฏ โฏ โฏ โฏ sp.cast(tokenAddress, sp.address)
โฏ โฏ โฏ โฏ โฏ โฏ self.verifyAdministrator(sp.sender)
โฏ โฏ โฏ โฏ โฏ โฏ โฏ
โฏ โฏ โฏ โฏ โฏ โฏ for saleId in self.data.markets[tokenAddress].sales.elements():
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ self.transferBackTokens(saleId)
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ del self.data.sales[saleId]
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ โฏ โฏ del self.data.markets[tokenAddress]
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.entrypoint
โฏ โฏ โฏ โฏ def buyAsset(self, saleId):
โฏ โฏ โฏ โฏ โฏ โฏ sp.cast(saleId, sp.nat)
โฏ โฏ โฏ โฏ โฏ โฏ self.verifySaleExists(saleId)
โฏ โฏ โฏ โฏ โฏ โฏ assert sp.amount == self.data.sales[saleId].price, "TIOF_PRICE_MISMATCH"
โฏ โฏ โฏ โฏ โฏ โฏ self.transferTokens(sp.record(sender=sp.self_address(),
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ receiver=sp.sender,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ amount=self.data.sales[saleId].amount,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ tokenAddress=self.data.sales[saleId].tokenAddress,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ id=self.data.sales[saleId].tokenId))
โฏ โฏ โฏ โฏ โฏ โฏ sp.send(self.data.sales[saleId].seller, self.data.sales[saleId].price)
โฏ โฏ โฏ โฏ โฏ โฏ self.removeSale(saleId)
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.entry_point
โฏ โฏ โฏ โฏ def sellAsset(self, params):
โฏ โฏ โฏ โฏ โฏ โฏ sp.cast(params, sp.record(tokenAddress = sp.address, tokenId = sp.nat, amount = sp.nat, price = sp.mutez))
โฏ โฏ โฏ โฏ โฏ โฏ self.transferTokens(sp.record(sender=sp.sender,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ receiver=sp.self_address(),
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ amount=params.amount,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ tokenAddress=params.tokenAddress,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ id=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)
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ โฏ โฏ self.data.markets[params.tokenAddress].sales.add(saleId)
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.entrypoint
โฏ โฏ โฏ โฏ def cancelSale(self, saleId):
โฏ โฏ โฏ โฏ โฏ โฏ sp.cast(saleId, sp.nat)
โฏ โฏ โฏ โฏ โฏ โฏ self.verifySaleExists(saleId)
โฏ โฏ โฏ โฏ โฏ โฏ self.verifyAdminOrSeller(sp.record(sender = sp.sender, saleId = saleId))
โฏ โฏ โฏ โฏ โฏ โฏ self.transferBackTokens(saleId)
โฏ โฏ โฏ โฏ โฏ โฏ self.removeSale(saleId)
โฏ โฏ โฏ โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def verifySaleExists(self, saleId):
โฏ โฏ โฏ โฏ โฏ โฏ assert self.data.sales.contains(saleId), "TIOF_NOT_EXISTENT_SALE"
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def verifyMarketNotExists(self, tokenAddress):
โฏ โฏ โฏ โฏ โฏ โฏ assert not self.isMarketExistent(tokenAddress), "TIOF_ALREADY_REGISTERED"
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def verifyMarketExists(self, tokenAddress):
โฏ โฏ โฏ โฏ โฏ โฏ assert self.isMarketExistent(tokenAddress), "TIOF_NOT_EXISTENT_MARKET"
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def verifyAdminOrSeller(self, sender, saleId):
โฏ โฏ โฏ โฏ โฏ โฏ assert (self.isAdministrator(sender) or (sender == self.data.sales[saleId].seller)), "TIOF_NOT_ADMIN_OR_SELLER"
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def transferBackTokens(self, saleId):
โฏ โฏ โฏ โฏ โฏ โฏ self.transferTokens(sp.record(sender=sp.self_address(),
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ receiver=self.data.sales[saleId].seller,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ amount=self.data.sales[saleId].amount,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ tokenAddress=self.data.sales[saleId].tokenAddress,
โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ id=self.data.sales[saleId].tokenId))
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ 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))
โฏ โฏ โฏ โฏ โฏ โฏ self.transferTokenGeneric(params)
โฏ โฏ โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def isMarketExistent(self, tokenAddress):
โฏ โฏ โฏ โฏ โฏ โฏ return self.data.markets.contains(tokenAddress)
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-only")
โฏ โฏ โฏ โฏ def getTokenType(self, tokenAddress):
โฏ โฏ โฏ โฏ โฏ โฏ return self.data.markets[tokenAddress].tokenType
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-write")
โฏ โฏ โฏ โฏ def removeSale(self, saleId): โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ โฏ
โฏ โฏ โฏ โฏ โฏ โฏ self.data.markets[self.data.sales[saleId].tokenAddress].sales.remove(saleId)
โฏ โฏ โฏ โฏ โฏ โฏ del self.data.sales[saleId]
โฏ โฏ โฏ
โฏ โฏ โฏ โฏ @sp.private(with_storage="read-write")
โฏ โฏ โฏ โฏ 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.
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:
- 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.
- 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.
- 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.
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(main)
scenario.table_of_contents()
admin = sp.test_account("Administrator")
alice = sp.test_account("Alice")
bob = sp.test_account("Bob")
# display accounts:
scenario.h1("Accounts")
scenario.show([admin, alice, bob])
scenario.h1("Contracts initialization")
scenario.h2("MarketPlace")
marketPlace = main.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(
admin.address,
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.h1("Marketplace")
scenario.h2("Interaction with FA1.2 token (silver ounces)")
scenario.h3("Register")
scenario.h4("[Error] Common user trying to register market")
params = sp.record(tokenAddress = fa12.address, tokenType = 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.h3("SellAsset")
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.h3("BuyAsset")
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")
fa2NFT.update_operators([
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 = FA_2_TOKEN_TYPE)
marketPlace.registerMarket(params).run(sender = admin)
scenario.h3("SellAsset")
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.h3("BuyAsset")
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")
fa2FungibleMultiAsset.update_operators([
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.
Error handling and security in FA1.2 and FA2
In FA12, the error handling is relatively straightforward but effective. Specific error messages are returned to guide both developers and users:
notenoughbalance
. This error is triggered when a user attempts to transfer more tokens than they possess. This is a crucial safeguard against unauthorized transactions.unsafeallowancechange
. This error occurs when there’s an attempt to change the allowance of tokens from a non-zero value to another non-zero value without first setting it to zero. This prevents potential manipulation of token allowances.
FA2 offers more comprehensive error handling, adding layers of security and robustness:
- Batch Transfer Failures. If a batch transfer fails at any point, the entire transaction is reverted. This ensures atomicity and prevents partial transfers that could lead to inconsistent states.
- Specific Error Messages. FA2 introduces error messages like
fa2insufficientbalance
andfa2tokenundefined
, which provide clear indications of what went wrong. This is particularly useful for debugging and provides a better user experience.
As for real-world implications, a discussion on Tezos Stack Exchange highlighted that not fully implementing the FA standard could lead to issues in token recognition by platforms like TzKT. This underlines the importance of adhering to the standard’s error-handling mechanisms for seamless interaction with other platforms and services.
Continuing the security enhancement topic, both FA12 and FA2 have built-in error-handling mechanisms that not only make the development process easier but also enhance the overall security of the Tezos ecosystem. These mechanisms act as the first line of defense against common vulnerabilities like re-entrancy attacks or allowance manipulation.
By providing clear and specific error messages, these standards help in identifying and preventing potential security risks, thereby contributing to a more secure and stable platform.
Conclusion
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.
It’s worth noting that the Tezos community is actively discussing further improvements in error handling, as seen in issues on GitLab related to FA2. This indicates that the Tezos ecosystem is continually evolving to address the complexities and challenges of blockchain security.
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!