Skip to main content

Voucher Tutorial

This tutorial will guide you through the process of creating a Closed Loop Token (CLT) dApp for municipalities to issue vouchers for local businesses.

The use case is: A municipality aims to promote energy efficiency among households by distributing voucher tokens that can be exchanged for energy-efficient LED light bulbs. This initiative encourages households to reduce energy consumption, thereby contributing to environmental sustainability.

Let's dive right into it. We can start by minting a new token. Tokens are similar to coins in IOTA, but if you have read the prior articles, you already know that they can have their own rules and policies.

Since they share similarities with coins, we can start by creating an init function with an OTW for our module and create a coin with a name, symbol, and description using coin::create_currency:

    fun init(otw: VOUCHER, ctx: &mut TxContext) {
let (treasury_cap, coin_metadata) = coin::create_currency(
otw,
0, // no decimals
b"VCHR", // symbol
b"Voucher Token", // name
b"Token used for voucher clt tutorial", // description
option::none(), // url
ctx
);

As tokens are special, we need to discuss the token policy. The token policy is an object that contains a spend_balance function and the rules we define. So, it plays an important role in the token lifecycle: defining what the token owner can and cannot do.

Let's create the policy and share it as a shared object later on:

        let (mut policy, policy_cap) = token::new_policy(&treasury_cap, ctx);

The new_policy function also returns a policy capability that grants admin rights, similar to the treasury capability. After creating the token and its policy, we:

  • Share the token policy
        token::share_policy(policy);
  • Send the policy capability to the caller
        transfer::public_transfer(policy_cap, ctx.sender());
  • Freeze the coin metadata
        transfer::public_freeze_object(coin_metadata);
  • Send the treasury cap to the caller
        transfer::public_transfer(treasury_cap, ctx.sender());

Now that we created the logic for initializing our module and creating the voucher token, the municipality could deploy the contract but not yet distribute the token. So, let's implement a function to do that. This is relatively easy; we create a gift_voucher function that will mint the tokens and send them to the receiver:

    public fun gift_voucher(
cap: &mut TreasuryCap<VOUCHER>,
amount: u64,
recipient: address,
ctx: &mut TxContext
) {
let token = token::mint(cap, amount, ctx);
let req = token.transfer(recipient, ctx);

token::confirm_with_treasury_cap(cap, req, ctx);
}

As you can see, the token::transfer function returns an ActionRequest. This object can not be dropped, so without passing it to another function that destroys it, the contract wouldn't work. This means we have to confirm the transfer for which we have multiple options.

In this case, as the caller of the gift_voucher function is the municipality, we can approve the action directly using our treasury capability, as shown in the last line of the gift_voucher function.

        token::confirm_with_treasury_cap(cap, req, ctx);

Since tokens are different from coins, with token::spend, we again get an ActionRequest object, which we have to approve. This time, we can't approve with the treasury capability as the caller is the household. Even if we did have the treasury capability, since it grants admin rights, it would confirm everything without checking the rules. So, we actually want to use the policy rules feature, as a household should only be allowed to spend the voucher token for LED bulbs in a certified shop.

So let's look into rules and add some to our policy.

Rules

We can define our rules and the corresponding logic in the same module as the voucher token or create another module for a more modular approach. We are going with the latter as this allows us to reuse the rules for other projects.

So let's think about the rule(s) we need. We want houseowners to be able to spend the voucher token only in certified shops. But we also don't want shops to be able to spend the voucher token; they should only be allowed to send it back to the municipality. With that in mind let's define our rule.

A rule is represented as a witness - a type with a drop ability. So let's define that first:

    public struct Allowlist has drop {}

Next, add a function to add addresses to the rule configuration:

    public fun add_addresses<T>(
policy: &mut TokenPolicy<T>,
cap: &TokenPolicyCap<T>,
mut addresses: vector<address>,
ctx: &mut TxContext,
) {
if (!has_config(policy)) {
token::add_rule_config(Allowlist {}, policy, cap, bag::new(ctx), ctx);
};

let config_mut = config_mut(policy, cap);
while (vector::length(&addresses) > 0) {
bag::add(config_mut, vector::pop_back(&mut addresses), true)
}
}

First, we check if the token policy already has a rule configuration. If not, we create a new one with token::add_rule_config passing the rule witness, the token policy and its capability, and a new bag in which we want to store the addresses.

Then we can get the mutable config with our config_mut helper function, which is just a wrapper of token::rule_config_mut.. And as a last step, we add the address to our bag.

Bonus Task

You could also implement additional functions to remove addresses from the rule configuration.

Next, we have to create functionality to stamp the action request if the rule is met. This is done by adding a verify function to the rules module:

    public fun verify<T>(
policy: &TokenPolicy<T>,
request: &mut ActionRequest<T>,
ctx: &mut TxContext
) {
assert!(has_config(policy), ENoConfig);

let config = config(policy);
let sender = token::sender(request);
let recipient = token::recipient(request);

if (request.action()==token::spend_action()) {
// Sender needs to be a shop
assert!(bag::contains(config, sender), ESenderNotAShop);
} else if (request.action()==token::transfer_action()) {
// The sender can't be a shop
assert!(!bag::contains(config, sender), ESenderIsAShop);

// The recipient has to be a shop
let recipient = *option::borrow(&recipient);
assert!(bag::contains(config, recipient), ERecipientNotAShop);
};

token::add_approval(Allowlist {}, request, ctx);
}

Let's break it down:

  1. Check if the policy has a rule config.

  2. Get the config, sender, and receiver.

  3. We split the verification into two parts:

    • If the action is a spend_action, someone is trying to return the token to the municipality. We check if the sender is on our list. If this is true, we stamp the action request. If not, we abort as we only want to allow shops to return the token.
    • If the action is a transfer_action, someone is trying to buy a LED bulb. So, we check if the sender is on our list. If this is true, that means a shop is trying to spend our token. We can't allow that, so we abort and never stamp the action request.
    • We also check if the receiver is a shop.
  4. If we don't abort, we stamp the action request.

Back to Our Voucher Module

Now that we created the rules modules, let's use them in our voucher module. Since both modules belong to the same package, we can import the rules like this:

    use clt_tutorial::allowlist_rule;

Now we need to register the rule for the needed actions in our init function. Just add the following lines between the policy creation and the sharing:

        policy.add_rule_for_action<VOUCHER, allowlist_rule::Allowlist>( &policy_cap, token::spend_action(), ctx);
policy.add_rule_for_action<VOUCHER, allowlist_rule::Allowlist>( &policy_cap, token::transfer_action(), ctx);

We defined the rule for the default spend and transfer actions. But we could also pass any other action as a string here and make the rule work for custom functions of our module.

The municipality also needs a way to register shops. So, we should add a function which internally calls the add_addresses function from our rules module:

    public fun register_shop(
policy: &mut TokenPolicy<VOUCHER>,
cap: &TokenPolicyCap<VOUCHER>,
addresses: vector<address>,
ctx: &mut TxContext
) {
allowlist_rule::add_addresses(policy, cap, addresses, ctx)
}

And now we are back to our buy_led_bulb function. We can now verify/stamp the action request with the rules capability:

        allowlist_rule::verify(policy, &mut req, ctx);

In this tutorial we are returning the action request. We could also approve it right away. So, in this case, we have to call the token_policy::confirm_request function in a PTB afterward, which will check the request for the approval stamp and make our TX succeed.

Now the shop owns the voucher, and the household just got a new energy-efficient LED bulb.

The last thing to do is for the shop to return the voucher to the municipality. This is done by calling the return_voucher function:

    public fun return_voucher(
token: Token<VOUCHER>,
policy: &TokenPolicy<VOUCHER>,
ctx: &mut TxContext
): ActionRequest<VOUCHER> {
let mut action_request = token.spend(ctx);

allowlist_rule::verify(policy, &mut action_request, ctx);

action_request
}

This is similar to the buy_led_bulb function, where we verify the rules of the transfer action. But, in this case, we use the spend action. So, the token will be consumed, and the spend_balance will be added to the action request. Once the action is confirmed, the spend_balance will be added to the balance of the token policy.

One more thing

Some observant readers might have noticed that we never specified a rule for the gift_voucher function. This is because the municipality is the owner of the token/treasury cap and can do whatever it wants with it.

This is the end of our tutorial. We hope you enjoyed it and learned something new about CLTs and Move.