Object Transfer Policies
A TransferPolicy on Sui is a customizable primitive that enables the owner of a type to create custom rules that define how the type can be transferred. You can set any number of rules, such as paying a royalty or commission, and all must be satisfied in a single transaction for the transfer to succeed. A TransferPolicy can be used within a Sui Kiosk marketplace or any other system that integrates the TransferPolicy primitive.
In a kiosk, creating and sharing a TransferPolicy<T> makes the type T tradable in that kiosk. On every kiosk purchase, the TransferPolicy must confirm the TransferRequest, or the transaction fails. For details, see How do transfer policies work with kiosks?.
TransferPolicy rules
By default, a single TransferPolicy enforces nothing and requires no user action. It confirms TransferRequests and therefore unblocks a transaction. However, the system allows setting rules. A rule is a way to request an additional action from the user before the request can be confirmed.
The rule logic enables someone to publish a new rule module, such as a fixed fee and add it to the TransferPolicy. After the rule is added, TransferRequest needs to collect a TransferReceipt marking that the requirement specified in the rule was completed.
A rule needs to provide 4 main components:
-
A
RuleWitnessstruct, which uniquely identifies the rule. Only the rule module can create it, so it serves as a static authorization method for the rule. -
A
configtype which is stored in theTransferPolicyand is used to configure the rule. -
A set function which adds the rule to the
TransferPolicy. TheTransferPolicyCapholder must perform this action. -
An actionable function which adds the receipt into the
TransferRequestand potentially adds to theTransferPolicybalance if the functionality involves some monetary transaction.
The TransferPolicy module allows removing any rule at any time, as guaranteed by the constraints on the rule's configuration.
How do I add a rule to a TransferPolicy?
-
Publish a rule module that defines the 4 components above.
-
As the
TransferPolicyCapholder, call the rule's set function (namedaddin the examples on this page). It callspolicy::add_rulewith the rule witness, the policy, the cap, and the rule'sConfig. -
On each transfer, the buyer calls the rule's actionable function, which calls
policy::add_receiptto add the rule's receipt to theTransferRequest. -
When
confirm_requestruns, the receipts are compared against the policy's rules, and the request is confirmed only if they match.
The dummy_rule module in the Example section contains the code for steps 1 through 3 in one module: add is the set function, and pay is the actionable function.
Every add function on this page takes the &mut TransferPolicy<T> and the &TransferPolicyCap<T>. Rules that have configuration take additional arguments, such as amount_bp in the royalty rule or start_time in the time-based rule.
Example
To demonstrate, consider the following example that implements value-added tax (VAT) fees on every merchant transaction by introducing a rule.
module examples::dummy_rule {
use sui::coin::Coin;
use sui::sui::SUI;
use sui::transfer_policy::{
Self as policy,
TransferPolicy,
TransferPolicyCap,
TransferRequest
};
/// The rule Witness; has no fields and is used as a
/// static authorization method for the rule.
public struct Rule has drop {}
/// Configuration struct with any fields (as long as it
/// has `drop`). Managed by the rule module.
public struct Config has store, drop {}
/// Function that adds a rule to the `TransferPolicy`.
/// Requires `TransferPolicyCap` to make sure the rules are
/// added only by the publisher of T.
public fun add<T>(
policy: &mut TransferPolicy<T>,
cap: &TransferPolicyCap<T>
) {
policy::add_rule(Rule {}, policy, cap, Config {})
}
/// Action function - perform a certain action (any, really)
/// and pass in the `TransferRequest` so it gets the receipt.
/// Receipt is a rule witness, so there's no way to create
/// it anywhere else but in this module.
///
/// This example also illustrates that rules can add Coin<SUI>
/// to the balance of the TransferPolicy allowing creators to
/// collect fees.
public fun pay<T>(
policy: &mut TransferPolicy<T>,
request: &mut TransferRequest<T>,
payment: Coin<SUI>
) {
policy::add_to_balance(Rule {}, policy, payment);
policy::add_receipt(Rule {}, request);
}
}
The dummy_rule module has no configuration and accepts a Coin<SUI> of any value.
Use multiple transfer policies
You can create multiple policies for different purposes. For example, a default VAT policy requires everyone to use it. However, travelers leaving the country can claim a VAT refund without altering the default policy.
To achieve this, you can issue a second TransferPolicy for the same type and wrap it in a custom object to implement the logic. For instance, a TaxFreePolicy object can bypass VAT. This object stores another TransferPolicy, accessible only if the buyer presents a valid Passport object. The inner policy might contain no rules, so the buyer pays no fees.
Royalty rules
To implement a percentage-based fee, such as royalties, a rule module must know the item's purchase price. The TransferRequest provides the following information that supports this and similar scenarios:
-
Item ID
-
Amount paid in SUI
-
The object ID that was used for selling, such as a kiosk
The sui::transfer_policy module provides public functions to access the fields paid(), item(), and from().
The following example expresses the royalty in basis points (bp). MAX_BP is 10_000, which represents 100% of the paid amount, so 1 bp is 0.01%.
module examples::royalty_rule {
// skipping dependencies
const MAX_BP: u16 = 10_000;
public struct Rule has drop {}
/// In this implementation rule has a configuration - `amount_bp`
/// which is the percentage of the `paid` in basis points.
public struct Config has store, drop { amount_bp: u16 }
/// When a rule is added, configuration details are specified
public fun add<T>(
policy: &mut TransferPolicy<T>,
cap: &TransferPolicyCap<T>,
amount_bp: u16
) {
assert!(amount_bp <= MAX_BP, 0);
policy::add_rule(Rule {}, policy, cap, Config { amount_bp })
}
/// To get the receipt, the buyer must call this function and pay
/// the required amount; the amount is calculated dynamically and
/// it is more convenient to use a mutable reference
public fun pay<T>(
policy: &mut TransferPolicy<T>,
request: &mut TransferRequest<T>,
payment: &mut Coin<SUI>,
ctx: &mut TxContext
) {
// using the getter to read the paid amount
let paid = policy::paid(request);
let config: &Config = policy::get_rule(Rule {}, policy);
let amount = (((paid as u128) * (config.amount_bp as u128) / MAX_BP) as u64);
assert!(coin::value(payment) >= amount, EInsufficientAmount);
let fee = coin::split(payment, amount, ctx);
policy::add_to_balance(Rule {}, policy, fee);
policy::add_receipt(Rule {}, request)
}
}
Because pay takes &mut Coin<SUI>, the coin is not consumed: the rule splits off the fee with coin::split, and the remaining balance stays in the buyer's coin.
Time-based rules
Rules apply to more than just payments and fees. Some rules might permit trading only before or after a specific time. Because rules are not standardized, you can encode logic with any object.
module examples::time_rule {
// skipping some dependencies
use sui::clock::{Self, Clock};
public struct Rule has drop {}
public struct Config has store, drop { start_time: u64 }
/// Start time is yet to come
const ETooSoon: u64 = 0;
/// Add a rule that enables purchases after a certain time
public fun add<T>(
policy: &mut TransferPolicy<T>,
cap: &TransferPolicyCap<T>,
start_time: u64
) {
policy::add_rule(Rule {}, policy, cap, Config { start_time })
}
/// Pass in the clock and prove that current time value is higher
/// than the `start_time`
public fun confirm_time<T>(
policy: &TransferPolicy<T>,
request: &mut TransferRequest<T>,
clock: &Clock
) {
let config: &Config = policy::get_rule(Rule {}, policy)
assert!(clock::timestamp_ms(clock) >= config.start_time, ETooSoon);
policy::add_receipt(Rule {}, request)
}
}
How do TransferRequest receipts satisfy rules?
The TransferRequest includes a field called receipts, which is a VecSet of TypeName. When the confirm_request call runs, the system compares the receipts against TransferPolicy.rules. If receipts do not match rules, the system rejects the request and the transaction fails.
In the following example, both the rules and receipts are empty, so they match trivially and the request is confirmed:
module sui::transfer_policy {
// ... skipped ...
public struct TransferRequest<phantom T> {
// ... other fields omitted ...
/// Collected receipts. Used to verify that all of the rules
/// were followed and `TransferRequest` can be confirmed.
receipts: VecSet<TypeName>
}
// ... skipped ...
public struct TransferPolicy<phantom T> has key, store {
// ... other fields omitted ...
/// Set of types of attached rules - used to verify `receipts` when
/// a `TransferRequest` is received in `confirm_request` function.
///
/// Additionally provides a way to look up currently attached rules.
rules: VecSet<TypeName>
}
// ...
}
Witness policy
There are 2 ways to authorize an action:
-
Static, by using a witness pattern, such as the
Rulestruct in the previous examples. Only the module that defines the witness type can create it. -
Dynamic, through a capability pattern. The caller presents a reference to a capability object, as the
capability_ruleexample shows.
Adding type parameters lets you create a generic rule that varies not just by configuration, but also by its type:
module examples::witness_rule {
// skipping dependencies
/// Rule is either not set or the witness does not match the expectation
const ERuleNotSet: u64 = 0;
/// This rule requires a witness of type W, see the implementation
public struct Rule<phantom W> has drop {}
public struct Config has store, drop {}
/// No special arguments are required to set this rule, but the
/// publisher now needs to specify a witness type
public fun add<T, W>(/* .... */) {
policy::add_rule(Rule<W> {}, policy, cap, Config {})
}
/// To confirm the action, buyer needs to pass in a witness
/// which should be acquired either by calling some function or
/// integrated into a more specific hook of a marketplace /
/// trading module
public fun confirm<T, W>(
_: W,
policy: &TransferPolicy<T>,
request: &mut TransferRequest<T>
) {
assert!(policy::has_rule<T, Rule<W>>(policy), ERuleNotSet);
policy::add_receipt(Rule<W> {}, request)
}
}
The witness_rule is generic and you can use it to require a custom witness depending on the settings. It is a way to link custom marketplace or trading logic to the TransferPolicy. With a slight modification, the rule can be turned into a generic capability requirement for any object, even a TransferPolicy for a different type or a TransferRequest.
module examples::capability_rule {
// skipping dependencies
/// Changing the type parameter name for better readability
public struct Rule<phantom Cap> has drop {}
public struct Config has store, drop {}
/// Absolutely identical to the witness setting
public fun add<T, Cap>(/* ... */) {
policy::add_rule(Rule<Cap> {}, policy, cap, Config {})
}
/// Almost the same with the witness requirement, only now we
/// require a reference to the type.
public fun confirm<T, Cap>(
cap: &Cap,
/* ... */
) {
assert!(policy::has_rule<T, Rule<Cap>>(policy), ERuleNotSet);
policy::add_receipt(Rule<Cap> {}, request)
}
}
How do transfer policies work with kiosks?
When a kiosk purchase occurs, the system creates a TransferRequest hot potato, a value that must be resolved before the transaction ends, and only the matching TransferPolicy can confirm it to unblock the transaction.
A kiosk can trade an item of type T only if the TransferPolicy for T exists and the buyer can access it. A purchase issues a TransferRequest that must be resolved in a matching TransferPolicy. If no policy exists or the buyer can't access it, the transaction fails.
This system gives you maximum freedom and flexibility. By removing transfer policy logic from the trading primitive, only you can set policies, and you control all enforcement as long as the primitive is used.