Skip to main content

Integrating with Permissioned Assets

caution

Permissioned Asset Standard is currently available on Testnet. It is not yet live on Mainnet.

To integrate with the Permissioned Assets Standard (PAS), you must:

  1. Define your approval witness using public struct MyApproval() has drop;.

  2. Create a policy with policy::new_for_currency and set the required approval per action.

  3. Build your approval logic using a function that takes &mut Request<...>, validates, and calls request.approve(MyApproval()).

  4. Set template commands so clients can build programmable transaction blocks (PTBs) automatically.

You can use PAS through the TypeScript package or learn more in the GitHub repo.

Define an approval witness

An approval witness is a zero-sized struct with drop that acts as a type-level stamp. Each witness represents a distinct approval your contract can grant:

/// Witness for approved transfers between accounts.
public struct MyTransferApproval() has drop;

/// Witness for approved clawbacks (issuer withdrawal).
public struct MyClawbackApproval() has drop;

You can define one witness type per action to serve as its approval. Each witness is registered independently in the policy and can optionally have its own template command.

Set up a policy for a currency

A Policy<Balance<C>> defines which approval is required for each action type. You need a TreasuryCap<C> to prove ownership of the currency.

The following example creates a policy in a package:

let (mut policy, policy_cap) = policy::new_for_currency(
&mut namespace,
&mut treasury_cap,
true, // clawback_allowed
);

// Require MyTransferApproval witness for transfers
policy.set_required_approval<Balance<T>, MyTransferApproval>(&policy_cap, "send_funds");

// Require MyClawbackApproval witness for clawbacks
policy.set_required_approval<Balance<T>, MyClawbackApproval>(&policy_cap, "clawback_funds");

policy.share();

Supported actions are "send_funds", "unlock_funds", and "clawback_funds".

Define approval logic

Your approval function takes a mutable reference to a Request, runs your validation, and stamps it with the witness.

Transfers require an external check, for example a KYC registry:

public fun approve_transfer(
registry: &KYCRegistry,
request: &mut Request<SendFunds<Balance<MY_COIN>>>,
) {
assert!(registry.users.contains(&request.data().recipient()), ENotKYCd);
request.approve(MyTransferApproval());
}

See the KYC example kyc_registry for a complete implementation.

Set template commands

Templates store pre-built Move call command structures on-chain for client-side automation. They allow SDKs to construct the correct Move calls for approval resolution without hardcoding package IDs or function signatures.

tip

Templates are purely an off-chain utility. They do not affect on-chain execution or security. They describe which Move calls an SDK should make. This is what allows a single generic SDK to support any PAS-managed asset, regardless of its specific approval logic.

Templates are useful because when a transfer happens, the PAS SDK needs to know:

  1. Which contract to call for approval

  2. What arguments that contract expects

  3. What type parameters to use

Templates store this information as an on-chain Command object keyed by the approval witness type.

When an issuer upgrades their approval logic (for example, from TransferApproval to TransferApprovalV2), they update the template on-chain. All clients automatically pick up the new resolution path without client-side updates, redeployments, or coordination across wallets and frontends.

Setting a template

A template command is a MoveCall that describes the approval function to call. You build it with ptb::move_call and register it through set_template_command:

public fun set_template_command<A: drop>(
templates: &mut Templates,
_: internal::Permit<A>,
command: Command,
)

The template is keyed by A, the witness type used to approve requests. This creates a one-to-one mapping: for each approval type, there is exactly one template command.

Example

Given the approval function from the KYC example kyc_registry:

public fun approve_transfer(
registry: &KYCRegistry,
request: &mut Request<SendFunds<Balance<MY_COIN>>>,
) {
assert!(registry.users.contains(&request.data().recipient()), ENotKYCd);
request.approve(MyTransferApproval());
}

You register a template that describes how to call it:

let type_name = type_name::with_defining_ids<MY_COIN>();

let cmd = ptb::move_call(
type_name.address_string().to_string(), // package ID
"kyc_registry", // module
"approve_transfer", // function
vector[
ptb::object_by_id(object::id(registry)), // KYCRegistry shared object
ptb::ext_input<PAS>("request"), // the request (resolved by SDK)
],
vector[], // type arguments
);

templates.set_template_command(internal::permit<MyTransferApproval>(), cmd);

Common argument types for templates

ConstructorDescription
ptb::ext_input<T>(name)Custom input resolved off-chain by the SDK, namespaced by type T.
ptb::object_by_id(id)Object resolved off-chain by its ID.
ptb::receiving_object_by_id(id)Receiving object resolved off-chain by its ID.
ptb::pure(value)BCS-encoded pure value.

For the full list of ptb constructors (including system shorthands and fully-resolved refs), see the PTB package.

Supported ext_input names

The following are the custom input names the PAS SDK resolves at PTB construction time. When you use ptb::ext_input<PAS>(name), the PAS type (from pas::templates) provides the namespace:

NameResolves to
"request"The active Request object being approved.
"policy"The Policy object for the managed asset.
"sender_account"The sender's Account object.
"receiver_account"The receiver's Account object.

How clients use templates

  1. The client reads the template from the Templates shared object.

  2. The template describes the Move call needed (package, module, function, arguments).

  3. The client builds the PTB: create request, template command (approval), resolve.

This enables generic wallets and frontends to execute compliant transfers without knowing the specific compliance contract details.