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
.