Skip to content
On this page

FA2 - legacy template โ€‹

INFO

The template FA2.py will become obsolete in favor of the new fa2_lib.py, introduced on March 4, 2022.

Template: view, download.
This is the only authoritative source and must be read in order to dive into details.
Example: FA2 contract is a sample boilerplate.
TZIP specifications: TZIP-12 and TZIP-16.

FA2/TZIP-12 is a standard for a unified token contract interface, supporting a wide range of token types and implementations.

A token contract can be designed to support a single token type (e.g. ERC-20 or ERC-721) or multiple token types (e.g. ERC-1155), to optimize batch transfers and atomic swaps of the tokens.

Tokens can be fungible tokens or non-fungible tokens (NFTs).

SmartPy provides a FA2 template that can be configured and adapted with custom logic to support a very wide range of your needs.

Simple FA2 contract โ€‹

Import โ€‹

To create a FA2 contract you need to import SmartPy and the FA2 template.

python
import smartpy as sp
FA2 = sp.io.import_script_from_url("https://legacy.smartpy.io/templates/FA2.py")

You can then create your FA2 contract by extending the template FA2.FA2 class.

python
class ExampleFA2(FA2.FA2):
    pass

At this stage you have a complete FA2 contract with a lot of entry points and helpers. We'll see how to choose more precisely what functionalities to keep, how to modify them and how to modify the storage later on. For now, let's compile it.

Compilation target โ€‹

In order to instantiate the FA2 you need to give three parameters:

  • admin: the admin address
  • config: a dictionary that lets you tweak the contract
  • metadata: the TZIP-16 metadata of the contract.

The config can be built by calling FA2.FA2_config(). See config.

The metadata is explained in detail in metadata.

python
sp.add_compilation_target(
    "FA2_Tokens",
    ExampleFA2(
        admin   = sp.address("tz1M9CMEtsXm3QxA7FmMU2Qh7xzsuGXVbcDr"),
        config  = FA2.FA2_config(),
        metadata = sp.utils.metadata_of_url("https://example.com")
    )
)

Test โ€‹

You can now write a very basic test with your token contract.

python
@sp.add_test(name="FA2 tokens")
def test():
    sc = sp.test_scenario()
    sc.table_of_contents()
    FA2_admin = sp.test_account("FA2_admin")
    sc.h2("FA2")
    exampleToken = ExampleFA2(
        FA2.FA2_config(),
        admin = FA2_admin.address,
        metadata = sp.utils.metadata_of_url("https://example.com")
    )
    sc += exampleToken

FA2 customisation โ€‹

entrypoints and mixins โ€‹

The FA2 template is divided into small classes that you can inherit separately if you don't need all the features.

In this example we don't implement the mint entrypoint.

python
class ExampleFA2(
        FA2_change_metadata
        FA2_token_metadata,
        # FA2_mint,
        FA2_administrator,
        FA2_pause,
        FA2_core):
    pass

Here are all the available mixins:

mixinsdescription
FA2_coreImplements the strict standard.
FA2_administratoris_administrator method and set_administrator entrypoint.
FA2_change_metadataset_metadata entrypoint.
FA2_mintmint entrypoint.
FA2_pauseis_paused method and set_pause entrypoint.
FA2_token_metadataset_token_metadata_view and make_metadata _methods.

The FA2.FA2 class is implemented by inheriting from all the mixins.

Init and storage โ€‹

You can reimplement the __init__ method and call the one of FA2_core.

python
class ExampleFA2(FA2.FA2_core):
    def __init__(self, config, admin):
        FA2.FA2_core.__init__(self, config,
                            paused = False,
                            administrator = admin)

This __init__ method takes additional storage fields. They'll be added in the contract storage.

python
FA2.FA2_core.__init__(self, config, paused = False, administrator = admin,
                    my_custom_bigmap = sp.big_map(
                        tkey = sp.TAddress,
                        tvalue = sp.TNat,
                        l = {}
                    ) # Add a bigmap into the final storage
                    )

You can also call use self.update_initial_storage() to update the storage.

python
class ExampleFA2(FA2.FA2):
    def __init__(self, config, admin):
        FA2.FA2_core.__init__(self, config,
                            paused = False,
                            administrator = admin)
        self.update_initial_storage(
            x = 0,
            y = sp.map()
        )

Custom entrypoints โ€‹

You can reimplement the provided entrypoints or add yours exactly like you add entrypoints in SmartPy contracts.

In this example, we replace the mint entrypoint by our implementation.

python
class ExampleFA2(FA2.FA2):
    @sp.entrypoint
    def mint(self, params):
        """ A very simple implementation of the mint entrypoint"""
        sp.verify(self.is_administrator(sp.sender), message = self.error_message.not_admin())
        with sp.if_(self.data.ledger.contains(user)):
            self.data.ledger[user].balance += params.amount
        with sp.else_():
            self.data.ledger[user] = Ledger_value.make(params.amount)
        with sp.if_(~ self.token_id_set.contains(self.data.all_tokens, params.token_id)):
            self.token_id_set.add(self.data.all_tokens, params.token_id)
            self.data.token_metadata[params.token_id] = sp.record(
                token_id    = params.token_id,
                token_info  = params.metadata
            )

Basic usage โ€‹

Mint โ€‹

The mint entrypoint that we provide doesn't let you modify

the token_metadata after the initial mint. If you want to change this, see custom entrypoints. :::

Let's defined the metadata of the token we want to mint.

python
example_md = FA2.FA2.make_metadata(
    decimals = 0,
    name     = "Example FA2",
    symbol   = "DFA2" )

This is equivalent to

python
sp.map(l = {
    # Remember that michelson wants map already in ordered
    "decimals" : sp.utils.bytes_of_string("%d" % 0),
    "name" : sp.utils.bytes_of_string("Example FA2"),
    "symbol" : sp.utils.bytes_of_string("DFA2")
}

You can also add an icon url or other custom metadata by creating a custom map.

Example in a scenario:

python
example_md = FA2.FA2.make_metadata(
        name     = "Example FA2",
        decimals = 0,
        symbol   = "DFA2" )
    exampleToken.mint(
        address  = FA2_admin.address, # Who will receive the original mint
        token_id = 0,
        amount   = 100_000_000_000,
        metadata = example_md
    ).run(sender = FA2_admin)

Transfer โ€‹

Transfers are a list of batches. A batch is a list of transactions from one sender.

Batch items can be created by the helper contract.batch_transfer.item.

Example:

python
c1.transfer(
    [
        c1.batch_transfer.item(from_ = alice.address,
                            txs = [
                                sp.record(to_ = bob.address,
                                            amount = 10,
                                            token_id = 0),
                                sp.record(to_ = bob.address,
                                            amount = 10,
                                            token_id = 1)]),
        c1.batch_transfer.item(from_ = bob.address,
                            txs = [
                                sp.record(to_ = alice.address,
                                            amount = 11,
                                            token_id = 0)])
    ]).run(sender = admin)

Operators โ€‹

Operators can be modified by calling update_operators with a list of variants that remove or add operators.

Example:

python
c1.update_operators([
    sp.variant("remove_operator", c1.operator_param.make(
        owner = alice.address,
        operator = op1.address,
        token_id = 0)),
    sp.variant("add_operator", c1.operator_param.make(
        owner = alice.address,
        operator = op2.address,
        token_id = 0))
]).run(sender = alice)

Ledger keys โ€‹

All the info about how many tokens are held by an address are in the ledger bigmap.

The keys of the bigmap can be created by calling contract.ledger_key.make(address, token_id).

Example:

python
scenario.verify(
    c1.data.ledger[c1.ledger_key.make(bob.address, 0)].balance == 10)

Config โ€‹

The config dictionary contains the meta-programming configuration. It is used to modify global logic in the FA2 contract that potentially affects multiple entrypoints. All values of the dictionary are boolean values.

KeyDefaultdescription
add_mutez_transferFalseAdd an entrypoint for the admin to transfer tez from the contract's balance.
allow_self_transferFalseThis contract is as an operator for all addresses/tokens it contains.
assume_consecutive_token_idsTrueIf true don't use a set of token ids, only keep how many there are.
debug_modeFalseUse maps instead of big-maps to simplify the contract's state inspection.
force_layoutsTrueLegacy.
lazy_entrypointsFalseadd flag lazy-entrypoints
non_fungibleFalseEnforce the non-fungibility of the tokens (i.e. total supply has to be 1).
readableTrueLegacy.
single_assetFalseSave some gas and storage by working only for the token-id 0.
store_total_supplyTrueStore the total-supply for each token (next to the token-metadata).
support_operatorTrueIf False, remove operator logic (maintain the presence of the entrypoint).
use_token_metadata_offchain_viewFalseInclude offchain view for accessing the token metadata (requires TZIP-016 contract metadata).

The config is returned by instantiating the class FA2_config.

Example:

python
FA2.FA2_config(assume_consecutive_token_ids = False, debug_mode = True)