diff --git a/packages/examples/kyc/Move.lock b/packages/examples/kyc/Move.lock new file mode 100644 index 0000000..1f93656 --- /dev/null +++ b/packages/examples/kyc/Move.lock @@ -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" } diff --git a/packages/examples/kyc/Move.toml b/packages/examples/kyc/Move.toml new file mode 100644 index 0000000..2e65f1d --- /dev/null +++ b/packages/examples/kyc/Move.toml @@ -0,0 +1,7 @@ +[package] +name = "kyc" +edition = "2024.beta" + +[dependencies] +pas = { local = "../../pas" } +ptb = { local = "../../ptb" } diff --git a/packages/examples/kyc/sources/kyc_registry.move b/packages/examples/kyc/sources/kyc_registry.move new file mode 100644 index 0000000..e9eb9e6 --- /dev/null +++ b/packages/examples/kyc/sources/kyc_registry.move @@ -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 = 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
, +} + +/// 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>>, +) { + 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>>) { + 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 { + internal::permit() +} diff --git a/packages/examples/kyc/sources/my_coin.move b/packages/examples/kyc/sources/my_coin.move new file mode 100644 index 0000000..c135a72 --- /dev/null +++ b/packages/examples/kyc/sources/my_coin.move @@ -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()); +} diff --git a/packages/examples/kyc/sources/treasury.move b/packages/examples/kyc/sources/treasury.move new file mode 100644 index 0000000..a98c03c --- /dev/null +++ b/packages/examples/kyc/sources/treasury.move @@ -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, + 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(); + + 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("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, + 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>, + cap: &mut TreasuryCap, + mut request: Request>>, + ctx: &mut TxContext, +) { + kyc_registry::approve_clawback(&mut request); + let balance = clawback_funds::resolve(request, policy); + cap.burn(balance.into_coin(ctx)); +}