Transfer policies
By default, only the owner of a token and their designated operators can transfer tokens. The administrator account, if it is used, has no different transfer permissions than any other account.
You can change who can transfer tokens by setting a transfer policy. See tzip-12/permissions-policy.md for detailed info about transfer policies in the FA2 standard.
The FA2 library provides the three standard policies and a non-standard one:
Policy | Name | Description |
---|---|---|
NoTransfer | "no-transfer" | Tokens cannot be transferred; any attempt to transfer tokens raises an FA2_TX_DENIED exception. |
OwnerTransfer | "owner-transfer" | Only owners can transfer their tokens; operators cannot transfer tokens. |
OwnerOrOperatorTransfer (default) | "owner-or-operator-transfer" | Owner or operators of the owner can transfer tokens. Only owner can change their operators. |
PauseOwnerOrOperatorTransfer | "pauseable-owner-or-operator-transfer" | Equivalent to OwnerOrOperatorTransfer on which it adds the set_pause entrypoint. The administrator can use this entrypoint to pause any use of the transfer and update_operator entrypoints. This policy requires the admin mixin. |
Setting the transfer policy
In SmartPy the order in which superclasses are listed is important. Therefore you must list the transfer policy in the correct place.
You must inherit the transfer policy after the Admin
mixin (if it is used) and before the the base classes. Then you must call the policy's __init__()
method before the Admin
mixin's __init__()
method and after the base classes, as in this example:
@sp.module
def main():
class MyNftContract(
main.Admin,
main.PauseOwnerOrOperatorTransfer,
main.Nft,
main.BurnNft,
main.MintNft
):
def __init__(self, admin_address, contract_metadata, ledger, token_metadata):
main.MintNft.__init__(self)
main.BurnNft.__init__(self)
main.Nft.__init__(self, contract_metadata, ledger, token_metadata)
main.PauseOwnerOrOperatorTransfer.__init__(self)
main.Admin.__init__(self, admin_address)
For more information about ordering, see Mixins.
Writing a custom policy
You can write your own policy by creating a class that respects the following interface.
self.private.policy
: A record with general information about the security policy, containing these fields:name
: A name for the policy, which is added to the contract metadatasupports_operator
: Set toTrue
if operators can transfer tokenssupports_transfer
: Set toTrue
if anyone can transfer tokens
check_tx_transfer_permissions_
: A method that runs for each batch in a transfer request and raises an exception to block the transfercheck_operator_update_permissions_
: A method that runs each time operators are changed and raises an exception to block the changeis_operator
: A method that runs each time operator permissions are checked and returnsFalse
to block the transfer orTrue
to allow it
For example, this security policy allows only certain tokens to be transferred:
- The
__init__()
method sets data about transfers and operators in the contract storage:- It creates a set of token IDs that can be transferred
- It creates a set of token IDs that can be transferred by operators
- It sets a global operator that can transfer any token
- The
check_tx_transfer_permissions_()
method raises an exception if an operation tries to transfer a token that is not in the set of tokens that can be transferred - The
check_operator_update_permissions_()
method raises an exception if an operation tries to set an operator for a token that is not in the set of tokens that can be transferred by operators - The
is_operator_()
method returns true if the account that submitted the operation is an operator or the global operator
import smartpy as sp
from smartpy.templates import fa2_lib as fa2
t = fa2.t
@sp.module
def myPolicies():
import t
class MyPolicy(sp.Contract):
def __init__(self, global_operator):
# Use this method to set the fields in the self.private.policy
# and to set any other values that the class needs to refer to in its methods
self.private.policy = sp.record(
# Name of your policy, added to the contract metadata.
name="your-policy-name",
# Set to True if operators can transfer tokens
supports_operator = True,
# Set to False to prevent all token transfers
supports_transfer = True
)
# Set any other initial storage values here, as in this example:
self.data.transferrable_tokens = {0, 2, 3, 7}
self.data.operator_transferrable_tokens = {0}
self.data.global_operator = global_operator
self.data.operators = sp.cast(
sp.big_map(), sp.big_map[t.operator_permission, sp.unit]
)
@sp.private(with_storage="read-only")
def check_tx_transfer_permissions_(self, params):
"""Called each time a transfer transaction is being looked at."""
sp.cast(
params,
sp.record(
from_=sp.address,
to_=sp.address,
token_id=sp.nat,
),
)
# Check if the token is transferrable
if not self.data.transferrable_tokens.contains(params.token_id):
raise "FA2_TX_DENIED"
@sp.private(with_storage="read-only")
def check_operator_update_permissions_(self, operator_permission):
"""Called each time an update_operator action is being looked at."""
sp.cast(operator_permission, t.operator_permission)
# Check if operators are permitted for this token ID
if not self.data.operator_transferrable_tokens.contains(operator_permission.token_id):
raise "FA2_OPERATORS_UNSUPPORTED"
@sp.private(with_storage="read-only")
def is_operator_(self, operator_permission) -> sp.bool:
"""Return True if `operator_permission` describes a registered operator, False otherwise."""
sp.cast(operator_permission, t.operator_permission)
# Return true if there is an operator defined or if the account is the global operator
is_global_operator = operator_permission.operator == self.data.global_operator
sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
is_operator = self.data.operators.contains(operator_permission)
if not is_global_operator and not is_operator:
raise "FA2_TX_DENIED"
To use a custom policy, import and initialize it just like one of the policies in the library, as in this example continued from above:
main = fa2.main
@sp.module
def myModule():
import main
import myPolicies
class myNFTContract(
main.Admin,
myPolicies.MyPolicy,
main.Nft,
main.BurnNft,
main.MintNft
):
def __init__(self, admin_address, contract_metadata, ledger, token_metadata):
main.MintNft.__init__(self)
main.BurnNft.__init__(self)
main.Nft.__init__(self, contract_metadata, ledger, token_metadata)
myPolicies.MyPolicy.__init__(self, admin_address)
main.Admin.__init__(self, admin_address)
Using policies in custom entrypoints
You can access policies' methods and attributes in your custom entrypoints via self.private
, as in this example:
class ExampleFa2Nft(main.Nft):
@sp.entrypoint
def customBurn(self, batch):
# Check that transfer is allowed
assert self.private.policy.supports_transfer, "FA2_TX_DENIED"
for action in batch:
self.check_tx_transfer_permissions_(
sp.record(
from_=action.from_, to_=action.from_, token_id=action.token_id
)
)
# ...