Skip to main content

Custom Transfer Rules

Every Sui object must have the key ability. The store ability, on the other hand, is an optional ability you can add to Sui objects. Objects with the store ability:

  • are transferable by anyone using the sui::transfer::public_transfer function; and
  • are able to be wrapped in other objects.

Importantly for custom transfer rules, if the Sui object O does not have the store ability, you cannot call the sui::transfer::public_transfer function to transfer it. The Move module that defines O is the only entity that can transfer objects of that type using the sui::transfer::transfer function. Consequently, the module that defines the object O can define a custom transfer function for O that can take any number of arguments, and enforce any restrictions desired for performing a transfer operation (for example, a fee must be paid in order to transfer the object).

The store ability and transfer rules

Custom transfer rules for objects enable you to define the transfer conditions that must be met for a valid transfer operation. You should be intentional about adding the store ability to an object because you are providing unrestricted access to that object without having to go through the module that defines it. After you enable public transfers on an object, there is no way of re-enabling custom transfer rules or any type of restrictions regarding the transfer of the object.

Example

This example creates an object type O that is transferrable only if the unlocked flag inside of it is set to true:

struct O has key {
id: UID,
// An `O` object can only be transferred if this field is `true`
unlocked: bool
}

Within the same module that defines the object O, you can then define a custom transfer rule transfer_unlocked for O that takes the object to transfer and the address to transfer it to, and verifies that the object is unlocked before transferring it to the specified address.

module examples::custom_transfer {
// Error code for trying to transfer a locked object
const EObjectLocked: u64 = 0;

struct O has key {
id: UID,
// An `O` object can only be transferred if this field is `true`
unlocked: bool
}

// Check that `O` is unlocked before transferring it
public fun transfer_unlocked(object: O, to: address) {
assert!(object.unlocked, EObjectLocked);
sui::transfer::transfer(object, to)
}
}

With custom transfer rules, you can define multiple different transfer rules for the same object. Each of these rules might have different restrictions that execution of the transaction can dynamically enforce. So, if you wanted to allow only locked objects to be transferred to a specific address you could add the following function to the previous module:

const EObjectNotLocked: u64 = 1;
const HomeAddress = @0xCAFE;


public fun transfer_locked(object: O) {
assert!(!object.unlocked, EObjectNotLocked);
sui::transfer::transfer(object, HomeAddress)
}

With these rules in place, there are two different custom transfer rules for any object O: either it's unlocked and anyone can transfer it, or it's locked, and it can only be transferred to 0xCAFE. Importantly, these two ways of transferring O are the only ways of transferring any object of type O. In particular, because O does not have the store ability, you cannot transfer it using the sui::transfer::public_transfer function. In fact, the only ways of transferring O are using examples::custom_transfer::transfer_unlocked and examples::custom_transfer::transfer_locked.