In this section, we will learn how to create a TransferPolicy
and use it to enforce rules the buyers must comply before the purchased item is owned by them.
TransferPolicy
for type T
must be created for that type T
to be tradeable in the Kiosk system. TransferPolicy
is a shared object acting as a central authority enforcing everyone to check their purchase is valid against the defined policy before the purchased item is transferred to the buyers.
use sui::tx_context::{TxContext, sender};
use sui::transfer_policy::{Self, TransferRequest, TransferPolicy, TransferPolicyCap};
use sui::package::{Self, Publisher};
use sui::transfer::{Self};
public struct KIOSK has drop {}
fun init(witness: KIOSK, ctx: &mut TxContext) {
let publisher = package::claim(otw, ctx);
transfer::public_transfer(publisher, sender(ctx));
}
#[allow(lint(share_owned, self_transfer))]
/// Create new policy for type `T`
public fun new_policy(publisher: &Publisher, ctx: &mut TxContext) {
let (policy, policy_cap) = transfer_policy::new<TShirt>(publisher, ctx);
transfer::public_share_object(policy);
transfer::public_transfer(policy_cap, sender(ctx));
}
Create a TransferPolicy<T>
requires the proof of publisher Publisher
of the module comprising T
. This ensures only the creator of type T
can create TransferPolicy<T>
. There are 2 ways to create the policy:
- Use
transfer_policy::new()
to create new policy, make theTransferPolicy
shared object and transfer theTransferPolicyCap
to the sender by usingsui::transfer
.
sui client call --package $KIOSK_PACKAGE_ID --module kiosk --function new_policy --args $KIOSK_PUBLISHER --gas-budget 10000000
- Use
entry transfer_policy::default()
to automatically do all above steps for us.
You should already receive the Publisher
object when publish the package. Let's export it for later use.
export KIOSK_PUBLISHER=<Publisher object ID>
You should see the newly created TransferPolicy
object and TransferPolicyCap
object in the terminal. Let's export it for later use.
export KIOSK_TRANSFER_POLICY=<TransferPolicy object ID>
export KIOSK_TRANSFER_POLICY_CAP=<TransferPolicyCap object ID>
TransferPolicy
doesn't enforce anything without any rule, let's learn how to implement a simple rule in a separated module to enforce users to pay a fixed royalty fee for a trade to succeed.
💡Note: There is a standard approach to implement the rules. Please checkout the rule template here
module kiosk::fixed_royalty_rule {
/// The `amount_bp` passed is more than 100%.
const EIncorrectArgument: u64 = 0;
/// The `Coin` used for payment is not enough to cover the fee.
const EInsufficientAmount: u64 = 1;
/// Max value for the `amount_bp`.
const MAX_BPS: u16 = 10_000;
/// The Rule Witness to authorize the policy
public struct Rule has drop {}
/// Configuration for the Rule
public struct Config has store, drop {
/// Percentage of the transfer amount to be paid as royalty fee
amount_bp: u16,
/// This is used as royalty fee if the calculated fee is smaller than `min_amount`
min_amount: u64,
}
}
Rule
represents a witness type to add to TransferPolicy
, it helps to identify and distinguish between multiple rules adding to one policy. Config
is the configuration of the Rule
, as we implement fixed royaltee fee, the settings should include the percentage we want to deduct out of original payment.
/// 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>,
amount_bp: u16,
min_amount: u64
) {
assert!(amount_bp <= MAX_BPS, EIncorrectArgument);
transfer_policy::add_rule(Rule {}, policy, cap, Config { amount_bp, min_amount })
}
We use transfer_policy::add_rule()
to add the rule with its configuration to the policy.
Let's execute this function from the client to add the Rule
to the TransferPolicy
, otherwise, it is disabled. In this example, we configure the percentage of royalty fee is 0.1%
~ 10 basis points
and the minimum amount royalty fee is 100 MIST
.
sui client call --package $KIOSK_PACKAGE_ID --module fixed_royalty_rule --function add --args $KIOSK_TRANSFER_POLICY $KIOSK_TRANSFER_POLICY_CAP 10 100 --type-args $KIOSK_PACKAGE_ID::kiosk::TShirt --gas-budget 10000000
/// Buyer action: Pay the royalty fee for the transfer.
public fun pay<T: key + store>(
policy: &mut TransferPolicy<T>,
request: &mut TransferRequest<T>,
payment: Coin<SUI>
) {
let paid = transfer_policy::paid(request);
let amount = fee_amount(policy, paid);
assert!(coin::value(&payment) == amount, EInsufficientAmount);
transfer_policy::add_to_balance(Rule {}, policy, payment);
transfer_policy::add_receipt(Rule {}, request)
}
/// Helper function to calculate the amount to be paid for the transfer.
/// Can be used dry-runned to estimate the fee amount based on the Kiosk listing price.
public fun fee_amount<T: key + store>(policy: &TransferPolicy<T>, paid: u64): u64 {
let config: &Config = transfer_policy::get_rule(Rule {}, policy);
let amount = (((paid as u128) * (config.amount_bp as u128) / 10_000) as u64);
// If the amount is less than the minimum, use the minimum
if (amount < config.min_amount) {
amount = config.min_amount
};
amount
}
We need a helper fee_amount()
to calculate the royalty fee given the policy and the payment amount. We use transfer_policy::get_rule()
to enquire the configuration and use it for fee calculation.
pay()
is a function that users must call themselves to fulfill the TransferRequest
(described in the next section) before transfer_policy::confirm_request()
. transfer_policy::paid()
gives us original payment of the trade represented by TransferRequest
. After royalty fee calculation, we will add the fee to the policy through transfer_policy::add_to_balance()
, any fee collected by the policy is accumulated here and TransferPolicyCap
owner can withdraw later. Last but not least, we use transfer_policy::add_receipt()
to flag the TransferRequest
that this rule is passed and ready to be confirmed with transfer_policy::confirm_request()
.
use sui::transfer_policy::{Self, TransferRequest, TransferPolicy};
/// Buy listed item
public fun buy(kiosk: &mut Kiosk, item_id: object::ID, payment: Coin<SUI>): (TShirt, TransferRequest<TShirt>){
kiosk::purchase(kiosk, item_id, payment)
}
/// Confirm the TransferRequest
public fun confirm_request(policy: &TransferPolicy<TShirt>, req: TransferRequest<TShirt>) {
transfer_policy::confirm_request(policy, req);
}
When buyers buy the asset by using kiosk::purchase()
API, an item is returned alongside with a TransferRequest
. TransferRequest
is a hot potato forcing us to consume it through transfer_policy::confirm_request()
. transfer_policy::confirm_request()
's job is to verify whether all the rules configured and enabled in the TransferPolicy
are complied by the users. If one of the enabled rules are not satisfied, then transfer_policy::confirm_request()
throws error leading to the failure of the transaction. As a consequence, the item is not under your ownership even if you tried to transfer the item to your account before transfer_policy::confirm_request()
.
💡Note: The users must compose a PTB with all necessary calls to ensure the TransferRequest is valid before confirm_request()
call.
The flow can be illustrated as follow:
Buyer -> kiosk::purchase()
-> Item
+ TransferRequest
-> Subsequent calls to fulfill TransferRequest
-> transfer_policy::confirm_request()
-> Transfer Item
under ownership
Recall from the previous section, the item must be placed inside the kiosk, then it must be listed to become sellable. Assuming the item is already listed with price 10_000 MIST
, let's export the listed item as terminal variable.
export KIOSK_TSHIRT=<Object ID of the listed TShirt>
Let's build a PTB to execute a trade. The flow is straightforward, we buy the listed item from the kiosk, the item and TransferRequest
is returned, then, we call fixed_royalty_fee::pay
to fulfill the TransferRequest
, we confirm the TransferRequest
with confirm_request()
before finally transfer the item to the buyer.
sui client ptb \
--assign price 10000 \
--split-coins gas "[price]" \
--assign coin \
--move-call $KIOSK_PACKAGE_ID::kiosk::buy @$KIOSK @$KIOSK_TSHIRT coin.0 \
--assign buy_res \
--move-call $KIOSK_PACKAGE_ID::fixed_royalty_rule::fee_amount "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" @$KIOSK_TRANSFER_POLICY price \
--assign fee_amount \
--split-coins gas "[fee_amount]"\
--assign coin \
--move-call $KIOSK_PACKAGE_ID::fixed_royalty_rule::pay "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" @$KIOSK_TRANSFER_POLICY buy_res.1 coin.0 \
--move-call $KIOSK_PACKAGE_ID::kiosk::confirm_request @$KIOSK_TRANSFER_POLICY buy_res.1 \
--move-call 0x2::transfer::public_transfer "<$KIOSK_PACKAGE_ID::kiosk::TShirt>" buy_res.0 <buyer address> \
--gas-budget 10000000