Skip to content
On this page

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:

PolicyNameDescription
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:

smartpy
@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 metadata
    • supports_operator: Set to True if operators can transfer tokens
    • supports_transfer: Set to True 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 transfer
  • check_operator_update_permissions_: A method that runs each time operators are changed and raises an exception to block the change
  • is_operator: A method that runs each time operator permissions are checked and returns False to block the transfer or True 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
smartpy
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:

smartpy
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:

smartpy
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
                    )
                )
                # ...