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.
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.
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 addressconfig
: a dictionary that lets you tweak the contractmetadata
: 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.
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.
@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.
class ExampleFA2(
FA2_change_metadata
FA2_token_metadata,
# FA2_mint,
FA2_administrator,
FA2_pause,
FA2_core):
pass
Here are all the available mixins:
mixins | description |
---|---|
FA2_core | Implements the strict standard. |
FA2_administrator | is_administrator method and set_administrator entrypoint. |
FA2_change_metadata | set_metadata entrypoint. |
FA2_mint | mint entrypoint. |
FA2_pause | is_paused method and set_pause entrypoint. |
FA2_token_metadata | set_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.
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.
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.
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.
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.
example_md = FA2.FA2.make_metadata(
decimals = 0,
name = "Example FA2",
symbol = "DFA2" )
This is equivalent to
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:
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:
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:
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:
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.
Key | Default | description |
---|---|---|
add_mutez_transfer | False | Add an entrypoint for the admin to transfer tez from the contract's balance. |
allow_self_transfer | False | This contract is as an operator for all addresses/tokens it contains. |
assume_consecutive_token_ids | True | If true don't use a set of token ids, only keep how many there are. |
debug_mode | False | Use maps instead of big-maps to simplify the contract's state inspection. |
force_layouts | True | Legacy. |
lazy_entrypoints | False | add flag lazy-entrypoints |
non_fungible | False | Enforce the non-fungibility of the tokens (i.e. total supply has to be 1). |
readable | True | Legacy. |
single_asset | False | Save some gas and storage by working only for the token-id 0 . |
store_total_supply | True | Store the total-supply for each token (next to the token-metadata). |
support_operator | True | If False, remove operator logic (maintain the presence of the entrypoint). |
use_token_metadata_offchain_view | False | Include offchain view for accessing the token metadata (requires TZIP-016 contract metadata). |
The config is returned by instantiating the class FA2_config
.
Example:
FA2.FA2_config(assume_consecutive_token_ids = False, debug_mode = True)