Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/examples/kyc/Move.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by move; do not edit
# This file should be checked in.

[move]
version = 4

[pinned.testnet.MoveStdlib]
source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" }
use_environment = "testnet"
manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363"
deps = {}

[pinned.testnet.Sui]
source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" }
use_environment = "testnet"
manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C"
deps = { MoveStdlib = "MoveStdlib" }

[pinned.testnet.kyc]
source = { root = true }
use_environment = "testnet"
manifest_digest = "F42F13A0483640C91598349E0E0C125899BD5635A850639255F6EC2E598ADAF8"
deps = { pas = "pas", ptb = "ptb", std = "MoveStdlib", sui = "Sui" }

[pinned.testnet.pas]
source = { local = "../../pas" }
use_environment = "testnet"
manifest_digest = "38AA62656ABE7551C444DA427ADBAA7751CB67250663D39FCDE36E938138EA7D"
deps = { ptb = "ptb", std = "MoveStdlib", sui = "Sui" }

[pinned.testnet.ptb]
source = { local = "../../ptb" }
use_environment = "testnet"
manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87"
deps = { std = "MoveStdlib", sui = "Sui" }
7 changes: 7 additions & 0 deletions packages/examples/kyc/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "kyc"
edition = "2024.beta"

[dependencies]
pas = { local = "../../pas" }
ptb = { local = "../../ptb" }
80 changes: 80 additions & 0 deletions packages/examples/kyc/sources/kyc_registry.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/// Example: KYC compliance with PAS.
///
/// Demonstrates a KYC registry where users must pass verification
/// before they can receive tokens.
module kyc::kyc_registry;

use kyc::my_coin::MY_COIN;
use pas::clawback_funds::ClawbackFunds;
use pas::request::Request;
use pas::send_funds::SendFunds;
use sui::balance::Balance;
use sui::vec_set::{Self, VecSet};

// ==== Error Codes ====

#[error(code = 0)]
const ENotKYCd: vector<u8> = b"Address has not passed KYC";

// ==== Structs ====

/// Witness stamp for approved transfers.
public struct TransferApproval() has drop;

/// Witness stamp for approved clawbacks (burn).
public struct ClawbackApproval() has drop;

/// On-chain KYC registry.
public struct KYCRegistry has key {
id: UID,
users: VecSet<address>,
}

/// Admin capability for managing the registry.
public struct RegistryCap has key, store { id: UID }

fun init(ctx: &mut TxContext) {
transfer::share_object(KYCRegistry {
id: object::new(ctx),
users: vec_set::empty(),
});
transfer::transfer(RegistryCap { id: object::new(ctx) }, ctx.sender());
}

// ==== Public ====

/// Validates the recipient has passed KYC, then stamps the request.
public fun approve_transfer(
registry: &KYCRegistry,
request: &mut Request<SendFunds<Balance<MY_COIN>>>,
) {
assert!(registry.users.contains(&request.data().recipient()), ENotKYCd);
request.approve(TransferApproval());
}

/// Add a user to the KYC registry.
public fun add_user(registry: &mut KYCRegistry, _cap: &RegistryCap, user: address) {
registry.users.insert(user);
}

/// Remove a user from the KYC registry.
public fun remove_user(registry: &mut KYCRegistry, _cap: &RegistryCap, user: address) {
registry.users.remove(&user);
}

// ==== Package ====

/// Stamps the clawback request (no KYC check — issuer can always claw back).
public(package) fun approve_clawback(request: &mut Request<ClawbackFunds<Balance<MY_COIN>>>) {
request.approve(ClawbackApproval());
}

/// Asserts user has passed KYC.
public(package) fun validate_mint(registry: &KYCRegistry, user: address) {
assert!(registry.users.contains(&user), ENotKYCd);
}

/// Permit for TransferApproval (only this module can create it).
public(package) fun transfer_approval_permit(): internal::Permit<TransferApproval> {
internal::permit()
}
22 changes: 22 additions & 0 deletions packages/examples/kyc/sources/my_coin.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// MY_COIN currency type definition and creation.
module kyc::my_coin;

use sui::coin_registry;

public struct MY_COIN has drop {}

fun init(otw: MY_COIN, ctx: &mut TxContext) {
let (initializer, cap) = coin_registry::new_currency_with_otw(
otw,
6,
b"MYC".to_string(),
b"My Coin".to_string(),
b"Example regulated coin with KYC compliance".to_string(),
b"https://example.com".to_string(),
ctx,
);
let metadata = initializer.finalize(ctx);

transfer::public_transfer(cap, ctx.sender());
transfer::public_transfer(metadata, ctx.sender());
}
83 changes: 83 additions & 0 deletions packages/examples/kyc/sources/treasury.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/// Treasury operations for MY_COIN.
///
/// Handles minting (deposit into Account) and burning (clawback from Account),
/// enforcing KYC compliance rules on all operations.
module kyc::treasury;

use kyc::kyc_registry::{Self, KYCRegistry, TransferApproval, ClawbackApproval};
use kyc::my_coin::MY_COIN;
use pas::account::Account;
use pas::clawback_funds::{Self, ClawbackFunds};
use pas::namespace::Namespace;
use pas::policy::{Self, Policy};
use pas::request::Request;
use pas::templates::{PAS, Templates};
use ptb::ptb;
use std::type_name;
use sui::balance::Balance;
use sui::coin::TreasuryCap;

// ==== Setup ====

/// One-time setup: PAS policy + compliance template.
/// Call after publishing (TreasuryCap is created in `my_coin::init`).
#[allow(lint(self_transfer))]
public fun setup(
namespace: &mut Namespace,
templates: &mut Templates,
registry: &KYCRegistry,
treasury_cap: &mut TreasuryCap<MY_COIN>,
ctx: &mut TxContext,
) {
// 1. Create policy with clawback enabled
let (mut policy, policy_cap) = policy::new_for_currency(
namespace,
treasury_cap,
true, // clawback allowed (for burn)
);

// 2. Set required approvals per action
policy.set_required_approval<_, TransferApproval>(&policy_cap, "send_funds");
policy.set_required_approval<_, ClawbackApproval>(&policy_cap, "clawback_funds");

// 3. Register template so the SDK can auto-construct approve_transfer calls
let type_name = type_name::with_defining_ids<MY_COIN>();

let cmd = ptb::move_call(
type_name.address_string().to_string(),
"kyc_registry",
"approve_transfer",
vector[ptb::object_by_id(object::id(registry)), ptb::ext_input<PAS>("request")],
vector[],
);

templates.set_template_command(kyc_registry::transfer_approval_permit(), cmd);

policy.share();
transfer::public_transfer(policy_cap, ctx.sender());
}

// ==== Mint & Burn ====

/// Mint tokens and deposit into a user's Account.
public fun mint(
registry: &KYCRegistry,
to_account: &Account,
cap: &mut TreasuryCap<MY_COIN>,
amount: u64,
) {
registry.validate_mint(to_account.owner());
to_account.deposit_balance(cap.mint_balance(amount));
}

/// Burn tokens from a user's Account via clawback.
public fun burn(
policy: &Policy<Balance<MY_COIN>>,
cap: &mut TreasuryCap<MY_COIN>,
mut request: Request<ClawbackFunds<Balance<MY_COIN>>>,
ctx: &mut TxContext,
) {
kyc_registry::approve_clawback(&mut request);
let balance = clawback_funds::resolve(request, policy);
cap.burn(balance.into_coin(ctx));
}
Loading