From 489fa8024ac3688f1c005430b4b7841383492dc9 Mon Sep 17 00:00:00 2001 From: Haris Katimertzis Date: Thu, 5 Mar 2026 10:24:44 +0200 Subject: [PATCH 1/4] docs: add PAS Quickstart and Architecture Overview --- docs/pas-architecture.mdx | 387 ++++++++++++++++++++++++++++++++++++++ docs/pas-quickstart.mdx | 170 +++++++++++++++++ 2 files changed, 557 insertions(+) create mode 100644 docs/pas-architecture.mdx create mode 100644 docs/pas-quickstart.mdx diff --git a/docs/pas-architecture.mdx b/docs/pas-architecture.mdx new file mode 100644 index 0000000..bee356b --- /dev/null +++ b/docs/pas-architecture.mdx @@ -0,0 +1,387 @@ +# PAS Architecture Overview + +Deep dive into the Permissioned Asset Standard — how it enforces restricted asset movement on Sui. + +--- + +## Motivation + +On Sui, any object with the `store` ability can be freely transferred by its owner. +e.g. `Balance` and `Coin` both have `store`, meaning if you hold them, you can send them +anywhere on the network with no restrictions. + +This is great for general-purpose assets, but a problem for regulated assets that need +transfer controls, compliance checks, or issuer oversight. + +**PAS solves this by proxying asset ownership through Chests, objects that hold assets +and ensure they remain within a closed chest-based system.** + +## Core Concept + +For each wallet address (or object ID), PAS creates a **1:1 derived Chest**, a shared object +that holds assets on behalf of that address. The owner can prove ownership via an `Auth` proof, +but they cannot freely transfer the assets. In this way, you create a proxy of ownership where +the assets follow the constraints of PAS modules, and sequentially, the rules that an issuer has +defined across one or more packages via approval witnesses. + +Every movement of funds goes through **hot potato Requests** that must collect a predefined set of **approval stamps** (witness structs) +before they can resolve within the transaction. + +``` +Without PAS: Wallet → owns T → can transfer freely +With PAS: Wallet → proves ownership via Auth → Chest holds T → Request needed to move +``` + +Each asset type can have its own **Policy** that defines which witnesses are required to approve +each action. This means different assets can enforce completely different rules, one might require +a single compliance stamp, another might need approvals from multiple independent contracts. + +The result: assets are held in a closed system where every movement is gated by programmable, +composable approval logic that the issuer defines at the Policy level. + +There is no way to transfer a managed `Balance` out of the system without going through +a request that collects the required approvals. + +For Currency, this is enforced at the Move type level: +- `Balance` is stored inside Chests using Sui's `balance::send_funds` / `balance::redeem_funds` (derived object storage) +- The only way to move funds is through **Request hot potatoes** that must be resolved in the same transaction +- Resolution requires matching the approval set defined in the `Policy>` + +--- + +## Object Model + +``` +Namespace (shared, singleton) +├── Chest (@0xAlice) ← derived from (namespace_id, ChestKey(alice_addr)) +├── Chest (@0xBob) ← derived from (namespace_id, ChestKey(bob_addr)) +├── Policy> ← derived from (namespace_id, PolicyKey>) +│ └── PolicyCap> ← derived from (policy_id, PolicyCapKey) +└── Templates ← derived from (namespace_id, TemplateKey) +``` + +All objects use **derived addresses** (`sui::derived_object`), making them deterministic and queryable +without on-chain lookups. + +### Namespace + +The Namespace is the root of the system. Responsibilities: + +- Derives addresses for Chests, Policies, and Templates +- Holds the `Versioning` state for emergency version blocking +- Keeps the `UpgradeCap` UID to gate admin operations (version blocking, setup) + +### Chest + +Chests are **shared objects** derived from the Namespace's UID and the owner's address. + +| Property | Detail | +|---|---| +| **Creation** | Permissionless — anyone can create a Chest for any address | +| **Ownership** | Wallet address (`ctx.sender()`) or Object (`UID`) | +| **Storage** | Holds `Balance` as object balance, or `T` directly as objects on the Chest's UID //TODO Check again | +| **Derivation** | `derived_object::claim(namespace_uid, ChestKey(owner))` | + +### Policy + +A `Policy` defines resolvable actions for a managed asset type T. + +- **Required approvals** per action type (`send_funds`, `unlock_funds`, `clawback_funds`) +- **Clawback flag** — whether admin clawback is allowed +- **Versioning** — synced from Namespace, can block package versions + +For currencies, you create a `Policy>` specifically: +Created via `policy::new_for_currency(&mut namespace, &mut treasury_cap, clawback_allowed)`. +Requires `TreasuryCap` as proof of currency ownership. + +### PolicyCap + +The capability to manage a Policy. Derived 1:1 from the Policy UID and PolicyCapKey. Used to: + +- Set/update required approvals per action +- Remove action approvals (makes requests for that action unresolvable) + +--- + +## The Request Pattern + +Every state-changing operation in PAS follows the **Request hot potato pattern**: + +``` +Create Request → Approve (1..N) → Resolve +``` + +### How It Works + +1. **Create**: A chest method wraps data `T` into a `Request>`. + The Request starts with an empty approval set. + +2. **Approve**: Your package calls `request.approve(MyWitness())` to stamp the Request with a + type-level proof. Multiple approvals can be collected from different packages. + +3. **Resolve**: A resolution function verifies that the collected approvals **exactly match** + the required approvals in the Policy, destroys the request object and or executes an action or unwraps data. + +The Request is a **hot potato**, it has no `drop` or `store` ability, so the transaction will abort +if it's not resolved. + +### Request Types + +```move +Request> // Transfer between chests +Request> // Issuer funds withdrawal +Request> // Withdraw from system as the owner of funds +``` + +### Approval Matching + +Approvals are matched by **type identity** using `TypeName`. The approval set must be **exactly equal** +(same types, same count, same order via `VecSet` insertion) to the Policy's required approvals. + +e.g. TransferApproval witness struct defined in contracts + +```move +// Policy requires: { TransferApproval } +// Request has: { TransferApproval } ← resolves +// Request has: { TransferApproval, ExtraApproval } ← aborts (count mismatch) +// Request has: { WrongApproval } ← aborts (type mismatch) +``` + +--- + +## Actions in Detail + +### SendFunds (Transfer) + +Moves `T` from one Chest to another. + +| Field | Description | +|---|---| +| `sender` | Wallet/object address (NOT chest address) | +| `recipient` | Wallet/object address (NOT chest address) | +| `sender_chest_id` | ID of the source Chest | +| `recipient_chest_id` | Derived ID of the destination Chest | +| `funds` | The `T` being transferred | + +**Resolution**: `clawback_funds::resolve(request, &policy)` — verifies approvals, then **returns `T` to the caller**. + +When `T` is `Balance`, use `send_funds::resolve_balance(request, &policy)` — this verifies approvals +and sends the balance directly to the recipient Chest via `balance::send_funds`. + +**Key insight**: `resolve_balance` sends the balance directly. The caller doesn't get it back — +it goes straight to the destination Chest. + +### ClawbackFunds (Issuer Withdrawal) + +Forcibly withdraws `T` from a Chest. No `Auth` required. + +| Field | Description | +|---|---| +| `owner` | Wallet/object address of the source Chest | +| `chest_id` | ID of the source Chest | +| `funds` | The `T` being clawed back | + +**Resolution**: `clawback_funds::resolve(request, &policy)` — verifies approvals AND +`policy.clawback_allowed`, then **returns `T` to the caller**. + +**Key insight**: Unlike SendFunds, the caller receives the funds and decides what to do with them +(burn, deposit elsewhere, etc.). + +### UnlockFunds (System Exit) + +Removes `T` from the closed system as the owner of the funds. `Auth` required. + +**Two resolution paths:** + +| Path | When | Resolution | +|---|---|---| +| `unlock_funds::resolve(request, &policy)` | Managed asset (`Policy` exists) | Requires matching approvals | +| `unlock_funds::resolve_unrestricted_balance(request, &namespace)` | Unmanaged balance (no Policy) | No approvals needed | + +The unrestricted path is `Balance`-specific — it exists for assets like SUI that may accidentally +end up in a Chest. It **aborts** if a `Policy>` exists for the type — you can't bypass managed asset controls. + +--- + +## Resolution: Move vs PTB + +### Resolving Actions in Move (Inside Your Contract) + +Your contract calls approve + resolve within its own function. The balance never leaves your control: + +//TODO sendFunds request probably better example +```move +public fun burn( + policy: &Policy>, + mut request: Request>>, +) { + // Approve inside your contract + request.approve(ClawbackApproval()); + + // Resolve — balance returned to this function + let balance = clawback_funds::resolve(request, policy); + + // Burn the balance + treasury_cap.burn(balance.into_coin(ctx)); +} +``` + +**Use case**: Burn, seize, or any operation where your contract needs the balance. + +### Resolving in a PTB (Client-Side) + +The request is created and approved across multiple Move calls in a single PTB, then resolved +as a separate Move call: + +``` +PTB: + 1. chest::new_auth() → auth + 2. chest.send_balance(auth, to, amount) → request + 3. your_contract::transfer(&mut request) → approves internally + 4. send_funds::resolve_balance(request, policy) → sends to recipient +``` + +**Use case**: Transfers, where the compliance contract approves and PAS handles delivery. + +**Important**: Steps 3 and 4 **must** be in the same PTB. The request is a hot potato — if the +transaction ends without resolution, it aborts. + +--- + +## Template Commands + +Templates store pre-built Move Call commands structures for client-side automation. They allow the SDK +to construct the correct Move calls for resolution of actions without hardcoding package IDs and function signatures. + +### Why Templates? + +When a transfer happens, the PAS SDK needs to know: +1. Which contract to call for approval (step 3 in the PTB above) +2. What arguments that contract expects +3. What type parameters to use + +Templates store this as an on-chain `Command` object keyed by the approval witness type. + +### Setting a Template + +```move +public fun set_template_command( + templates: &mut Templates, + _: internal::Permit, + command: Command, +) +``` + +The template is keyed by `A` — the witness type used to approve requests. +This creates a 1:1 mapping: for each approval type, there's exactly one template command. + +### How Clients Use Templates + +1. Client reads the template from the `Templates` shared object +2. Template describes the Move call needed (package, module, function, args) +3. 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. + +--- + +## Balance Tracking + +PAS uses Sui's address balances: + +### How Balances Are Stored + +``` +Chest (shared object) + └── UID + └── Balance stored via balance::send_funds(balance, chest_object_address) +``` + +Balances are **not** stored as fields on the Chest struct. They're stored as object balance +on the Chest's `UID`, using `balance::send_funds` to send funds to the Chest's object address +and `balance::withdraw_funds_from_object` (via `UID.withdraw_funds_from_object`) to pull them out. + +//TODO Explain the need for internal balance tracking + +### Balance Flow + +``` +Deposit: Balance → balance::send_funds(balance, chest_addr) → stored on Chest UID +Withdraw: Chest UID → balance::withdraw_funds_from_object(amount) → Balance +``` + +Deposits are permissionless (anyone can deposit into any Chest). +Withdrawals are internal (`public(package)`) — only PAS modules can withdraw, and only through Requests. + +--- + +## Versioning + +PAS includes a version-blocking system for emergency shutdowns or deprecating features. + +### How It Works + +- Every Namespace, Chest, and Policy stores a `Versioning` snapshot +- `Versioning` contains a `VecSet` of blocked versions +- Every operation calls `versioning.assert_is_valid_version()` which checks + `breaking_version!()` against the blocked set +- `breaking_version!()` is a compile-time macro returning the package's current breaking version + +### Blocking a Version + +```move +// Only the UpgradeCap holder can block versions +namespace.block_version(&upgrade_cap, version_number); +``` + +### Syncing Versioning + +Chests and Policies cache their versioning. After a version block/unblock, they need syncing: + +```move +// Permissionless — anyone can sync +chest.sync_versioning(&namespace); +policy.sync_versioning(&namespace); +``` + +--- + +## Security Model + +### What PAS Guarantees + +1. **Closed loop**: Managed assets cannot leave the system without going through a Request with + matching approvals +2. **Type-safe approvals**: Approval witnesses are checked by `TypeName` — you can't forge an + approval from a different package +3. **Atomic resolution**: Requests are hot potatoes. They must be resolved in the same transaction + or the transaction aborts +4. **Deterministic addressing**: All objects use derived addresses. No hidden state, no + non-deterministic object creation + +### What PAS Does NOT Do + +- **Access control**: PAS doesn't decide WHO can transfer. That's your contract's job (via approval witnesses) +- **Compliance rules**: PAS doesn't rules. Your contract implements those + before calling `request.approve()` + +### Trust Boundaries + +| Component | Trust Level | +|---|---| +| Namespace `UpgradeCap` holder | Can block versions (emergency shutdown) | +| `PolicyCap` holder | Can change approval requirements for T| +| `TreasuryCap` holder | Can create a Policy (one-time) for Balance | +| Chest owner (`Auth`) | Can initiate send/unlock from their chest | +| Anyone | Can create chests, deposit, sync versioning | +| Approval witness package | Controls who can approve requests | + +## Integration Checklist + +When building on PAS: + +1. **Define your approval witness**: `public struct MyApproval() has drop;` +2. **Create a Policy** with `policy::new_for_currency` and set required approvals per action +3. **Build your approval logic**: A function that takes `&mut Request<...>`, validates, and calls `request.approve(MyApproval())` +4. **Set template commands** so clients can build PTBs automatically \ No newline at end of file diff --git a/docs/pas-quickstart.mdx b/docs/pas-quickstart.mdx new file mode 100644 index 0000000..6b2d208 --- /dev/null +++ b/docs/pas-quickstart.mdx @@ -0,0 +1,170 @@ +# PAS Quickstart + +Minimal examples to get you moving assets through PAS (Permissioned Asset Standard). + +## What is PAS? + +PAS is a permissioned asset framework on Sui. Assets of different types live +under **Chests** (one per address independent of types) and can only move between +Chests through hot potato **Requests**, that must be approved and resolved in the same PTB. + +Every action follows the same pattern: **Create Request → Approve → Resolve**. + +## Setup + +### 1. Create a Policy for a Currency + +A `Policy>` defines which approvals are required for each action type. +You need a `TreasuryCap` to prove ownership of the currency: + +use in a package: + +```move +let (mut policy, policy_cap) = policy::new_for_currency( + &mut namespace, + &mut treasury_cap, + true, // clawback_allowed +); + +// Require `MyApproval` witness for transfers +policy.set_required_approval, MyApproval>(&policy_cap, "send_funds"); + +// Require `MyApproval` witness for clawbacks +policy.set_required_approval, MyApproval>(&policy_cap, "clawback_funds"); + +policy.share(); +``` + +**Supported actions:** `"send_funds"`, `"unlock_funds"`, `"clawback_funds"` + +### 3. Create Chests + +Chest creation is permissionless. One chest per address and can hold : + +```move +chest::create_and_share(&mut namespace, @0xAlice); +chest::create_and_share(&mut namespace, @0xBob); +``` + +--- + +## Transfer (Send Balance) + +Transfer balance of permissioned assets from Chest A to Chest B. Requires the owner's `Auth` proof. + +### Move + +```move +// 1. Create auth proof +let auth = chest::new_auth(ctx); + +// 2. Create the transfer request (withdraws Balance from sender chest) +let request = from_chest.send_balance( + &auth, + &to_chest, + amount, + ctx, +); + +// 3. Approve with your witness +request.approve(MyApproval()); + +// 4. Resolve — sends Balance to recipient chest +send_funds::resolve_balance(request, &policy); +``` + +--- + +## Clawback (Admin Withdrawal) + +Forcibly withdraw tokens from a chest. No `Auth` required — this is an admin action. +The policy must have `clawback_allowed: true`. + +### Move + +```move +// 1. Create clawback request (no Auth needed) +let mut request = from_chest.clawback_balance(amount, ctx); + +// 2. Approve +request.approve(MyApproval()); + +// 3. Resolve — returns the Balance to the caller +let balance: Balance = clawback_funds::resolve(request, &policy); + +// 4. Do something with the balance (deposit elsewhere, burn, etc.) +to_chest.deposit_balance(balance); +``` + +--- + +## Unlock (Withdraw from System) + +Remove tokens from the closed-loop system entirely. + +### Managed Assets (Policy exists) + +```move +let auth = chest::new_auth(ctx); +let mut request = chest.unlock_balance(&auth, amount, ctx); + +request.approve(MyApproval()); +let balance: Balance = unlock_funds::resolve(request, &policy); +// Balance is now outside the system +``` + +### Unmanaged Assets (No Policy — e.g., SUI accidentally sent to a Chest) + +```move +let auth = chest::new_auth(ctx); +let request = chest.unlock_balance(&auth, amount, ctx); + +// No approval needed — resolves with empty approval set +let balance: Balance = unlock_funds::resolve_unrestricted_balance(request, &namespace); +``` + +--- + +## Deposit + +Deposit tokens into a chest. No request needed: + +```move +chest.deposit_balance(balance); +``` + +Or send to a chest address directly (useful before the chest object is available in the transaction): + +```move +balance.send_funds(namespace.chest_address(owner)); +``` + +--- + +## Auth: Wallet vs Object Ownership + +Chests can be owned by wallet addresses or objects. + +```move +// Wallet-owned: proves ownership via transaction sender +let auth = chest::new_auth(ctx); + +// Object-owned: proves ownership via UID reference +let auth = chest::new_auth_as_object(&mut my_object_uid); +``` + +--- + +## Derived Object Addresses + +All PAS objects (Chests, Policies) use deterministic derived addresses. You can compute them off-chain: + +```move +// Get the chest address for an owner +let chest_addr: address = namespace.chest_address(@0xAlice); + +// Get the policy address for a type +let policy_addr: address = namespace.policy_address>(); +``` + +--- \ No newline at end of file From a2a70715d8a85630a9dec4ea1bc35ff5b107526e Mon Sep 17 00:00:00 2001 From: Haris Katimertzis Date: Mon, 9 Mar 2026 14:11:52 +0200 Subject: [PATCH 2/4] docs: address review comments --- docs/pas-architecture.mdx | 126 +++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 42 deletions(-) diff --git a/docs/pas-architecture.mdx b/docs/pas-architecture.mdx index bee356b..e24168f 100644 --- a/docs/pas-architecture.mdx +++ b/docs/pas-architecture.mdx @@ -13,8 +13,8 @@ anywhere on the network with no restrictions. This is great for general-purpose assets, but a problem for regulated assets that need transfer controls, compliance checks, or issuer oversight. -**PAS solves this by proxying asset ownership through Chests, objects that hold assets -and ensure they remain within a closed chest-based system.** +**PAS solves this by proxying asset ownership through Chests, objects that hold assets +and enforce a closed-loop system where every movement is gated by programmable approval logic.** ## Core Concept @@ -138,6 +138,9 @@ Request> // Withdraw from system as the owner of funds Approvals are matched by **type identity** using `TypeName`. The approval set must be **exactly equal** (same types, same count, same order via `VecSet` insertion) to the Policy's required approvals. +> **v1 limitation**: In the current version, each action supports only a **single approval witness**. +> Multi-approval support (requiring stamps from multiple independent contracts) is planned for a future release. + e.g. TransferApproval witness struct defined in contracts ```move @@ -187,9 +190,14 @@ Forcibly withdraws `T` from a Chest. No `Auth` required. **Key insight**: Unlike SendFunds, the caller receives the funds and decides what to do with them (burn, deposit elsewhere, etc.). -### UnlockFunds (System Exit) +### UnlockFunds (Closed-Loop Exit) + +Removes `T` from the closed-loop system entirely. `Auth` required. -Removes `T` from the closed system as the owner of the funds. `Auth` required. +> **Warning**: Enabling `unlock_funds` allows assets to leave the closed-loop system completely, +> bypassing all future transfer controls. Once unlocked, the asset becomes a regular on-chain object +> with no PAS restrictions. Only allow this action if your use case explicitly requires users to +> withdraw assets from the managed system. **Two resolution paths:** @@ -237,7 +245,7 @@ as a separate Move call: PTB: 1. chest::new_auth() → auth 2. chest.send_balance(auth, to, amount) → request - 3. your_contract::transfer(&mut request) → approves internally + 3. your_contract::approve_transfer(&mut request) → approves internally 4. send_funds::resolve_balance(request, policy) → sends to recipient ``` @@ -250,8 +258,13 @@ transaction ends without resolution, it aborts. ## Template Commands -Templates store pre-built Move Call commands structures for client-side automation. They allow the SDK -to construct the correct Move calls for resolution of actions without hardcoding package IDs and function signatures. +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. + +> **Important**: Templates are purely an **off-chain utility**. They do not affect on-chain execution +> or security. They simply 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. ### Why Templates? @@ -262,8 +275,15 @@ When a transfer happens, the PAS SDK needs to know: Templates store this as an on-chain `Command` object keyed by the approval witness type. +**Key benefit**: When an issuer upgrades their approval logic (e.g. from `TransferApproval` to +`TransferApprovalV2`), they update the template on-chain. All clients automatically pick up the new +resolution path. No client-side updates, no redeployments, no 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 via `set_template_command`: + ```move public fun set_template_command( templates: &mut Templates, @@ -272,9 +292,62 @@ public fun set_template_command( ) ``` -The template is keyed by `A` — the witness type used to approve requests. +The template is keyed by `A`, the witness type used to approve requests. This creates a 1:1 mapping: for each approval type, there's exactly one template command. +#### Example + +Given an example approval function in your package: + +```move +public fun approve_transfer(request: &mut Request>>, _clock: &Clock) { + // Your validation logic here + request.approve(TransferApproval()); +} +``` + +You register a template that describes how to call it: + +```move +let type_name = type_name::with_defining_ids(); + +let cmd = ptb::move_call( + type_name.address_string().to_string(), // package ID + "my_module", // module + "approve_transfer", // function + vector[ + ptb::ext_input("pas:request"), // the request (resolved by SDK) + ptb::object_by_id(@0x6.to_id()), // Clock object + ], + vector[(*type_name.as_string()).to_string()], // type arguments +); + +templates.set_template_command(internal::permit(), cmd); +``` + +#### Common Argument Types for Templates + +| Constructor | Description | +|---|---| +| `ptb::ext_input(name)` | Custom input resolved off-chain by the SDK (see below) | +| `ptb::object_by_id(id)` | Object resolved off-chain by its ID | +| `ptb::object_by_type()` | Object resolved off-chain by its Move type | +| `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. //TODO link the package + +#### Supported `ext_input` Names + +These are the custom input names the PAS SDK resolves at PTB construction time: + +| Name | Resolves to | +|---|---| +| `"pas:request"` | The active `Request` object being approved | +| `"pas:policy"` | The `Policy` object for the managed asset | +| `"pas:sender_chest"` | The sender's `Chest` object | +| `"pas:receiver_chest"` | The receiver's `Chest` object | + ### How Clients Use Templates 1. Client reads the template from the `Templates` shared object @@ -288,7 +361,7 @@ specific compliance contract details. ## Balance Tracking -PAS uses Sui's address balances: +PAS uses Sui's [address balances](https://docs.sui.io/guides/developer/address-balances-migration): ### How Balances Are Stored @@ -316,37 +389,6 @@ Withdrawals are internal (`public(package)`) — only PAS modules can withdraw, --- -## Versioning - -PAS includes a version-blocking system for emergency shutdowns or deprecating features. - -### How It Works - -- Every Namespace, Chest, and Policy stores a `Versioning` snapshot -- `Versioning` contains a `VecSet` of blocked versions -- Every operation calls `versioning.assert_is_valid_version()` which checks - `breaking_version!()` against the blocked set -- `breaking_version!()` is a compile-time macro returning the package's current breaking version - -### Blocking a Version - -```move -// Only the UpgradeCap holder can block versions -namespace.block_version(&upgrade_cap, version_number); -``` - -### Syncing Versioning - -Chests and Policies cache their versioning. After a version block/unblock, they need syncing: - -```move -// Permissionless — anyone can sync -chest.sync_versioning(&namespace); -policy.sync_versioning(&namespace); -``` - ---- - ## Security Model ### What PAS Guarantees @@ -370,12 +412,12 @@ policy.sync_versioning(&namespace); | Component | Trust Level | |---|---| -| Namespace `UpgradeCap` holder | Can block versions (emergency shutdown) | -| `PolicyCap` holder | Can change approval requirements for T| +| `PolicyCap` holder | Can change approval requirements for T | | `TreasuryCap` holder | Can create a Policy (one-time) for Balance | | Chest owner (`Auth`) | Can initiate send/unlock from their chest | | Anyone | Can create chests, deposit, sync versioning | | Approval witness package | Controls who can approve requests | +| PAS package `UpgradeCap` holder | Can manage the `Versioning` system to coordinate safe package upgrades and migrations | ## Integration Checklist From 6a77c62d6171f71de79134a02f28e89ff1065c06 Mon Sep 17 00:00:00 2001 From: Haris Katimertzis Date: Mon, 9 Mar 2026 18:51:21 +0200 Subject: [PATCH 3/4] feat: acme securities example --- packages/testing/acme_compliance/Move.lock | 35 +++ packages/testing/acme_compliance/Move.toml | 7 + .../sources/acme_compliance.move | 202 ++++++++++++++++++ .../acme_compliance/sources/acme_token.move | 22 ++ .../sources/acme_treasury.move | 96 +++++++++ 5 files changed, 362 insertions(+) create mode 100644 packages/testing/acme_compliance/Move.lock create mode 100644 packages/testing/acme_compliance/Move.toml create mode 100644 packages/testing/acme_compliance/sources/acme_compliance.move create mode 100644 packages/testing/acme_compliance/sources/acme_token.move create mode 100644 packages/testing/acme_compliance/sources/acme_treasury.move diff --git a/packages/testing/acme_compliance/Move.lock b/packages/testing/acme_compliance/Move.lock new file mode 100644 index 0000000..ce7cda3 --- /dev/null +++ b/packages/testing/acme_compliance/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.acme_compliance] +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/testing/acme_compliance/Move.toml b/packages/testing/acme_compliance/Move.toml new file mode 100644 index 0000000..7e6e63b --- /dev/null +++ b/packages/testing/acme_compliance/Move.toml @@ -0,0 +1,7 @@ +[package] +name = "acme_compliance" +edition = "2024.beta" + +[dependencies] +pas = { local = "../../pas" } +ptb = { local = "../../ptb" } diff --git a/packages/testing/acme_compliance/sources/acme_compliance.move b/packages/testing/acme_compliance/sources/acme_compliance.move new file mode 100644 index 0000000..142a637 --- /dev/null +++ b/packages/testing/acme_compliance/sources/acme_compliance.move @@ -0,0 +1,202 @@ +/// Example: Securities compliance with PAS. +/// +/// Demonstrates two common compliance rules for tokenized securities: +/// 1. Accredited investor checks (both sender and recipient must be accredited) +/// 2. Holding limits (min/max balance per investor) +/// +/// Balance tracking is maintained internally because PAS stores balances inside +/// Chests using Sui's address balances, which the compliance contract cannot +/// read directly during approval. +module acme_compliance::acme_compliance; + +use pas::clawback_funds::ClawbackFunds; +use pas::request::Request; +use pas::send_funds::SendFunds; +use sui::balance::Balance; +use sui::table::Table; +use acme_compliance::acme_token::ACME_TOKEN; + +// ==== Error Codes ==== + +#[error] +const ENotWhitelisted: vector = b"Address is not in the investor registry"; +#[error] +const ENotAccredited: vector = b"Investor is not accredited"; +#[error] +const EBelowMinBalance: vector = b"Balance would fall below minimum holding requirement"; +#[error] +const EAboveMaxBalance: vector = b"Balance would exceed maximum holding limit"; + +// ==== Witness ==== + +/// Witness stamp for approved transfers. +public struct TransferApproval() has drop; + +/// Witness stamp for approved clawbacks (burn). +public struct ClawbackApproval() has drop; + +// ==== Structs ==== + +/// On-chain registry of investor status and balances. +public struct InvestorRegistry has key { + id: UID, + investors: Table, + /// Minimum balance per investor (0 = no minimum) + min_balance: u64, + /// Maximum balance per investor (0 = no maximum) + max_balance: u64, +} + +public struct InvestorProfile has drop, store { + accredited: bool, + balance: u64, +} + +/// Admin capability for managing the registry +public struct RegistryCap has key, store { id: UID } + +// ==== Init ==== + +fun init(ctx: &mut TxContext) { + transfer::share_object(InvestorRegistry { + id: object::new(ctx), + investors: sui::table::new(ctx), + min_balance: 0, + max_balance: 0, + }); + transfer::transfer(RegistryCap { id: object::new(ctx) }, ctx.sender()); +} + +// ==== Approval Functions ==== + +/// Validates accreditation and holding limits for both parties, +/// updates internal balance tracking, then stamps the request. +public fun approve_transfer( + registry: &mut InvestorRegistry, + request: &mut Request>>, +) { + let sender = request.data().sender(); + let recipient = request.data().recipient(); + let amount = request.data().funds().value(); + + assert_eligible(registry, sender); + assert_eligible(registry, recipient); + + // Check sender's remaining balance meets minimum (if non-zero after transfer) + let sender_balance_after = registry.investors.borrow(sender).balance - amount; + if (sender_balance_after > 0 && registry.min_balance > 0) { + assert!(sender_balance_after >= registry.min_balance, EBelowMinBalance); + }; + + // Check recipient's new balance within limits + let recipient_balance_after = registry.investors.borrow(recipient).balance + amount; + assert_within_limits(registry, recipient_balance_after); + + // Update internal balance tracking + registry.investors.borrow_mut(sender).balance = sender_balance_after; + registry.investors.borrow_mut(recipient).balance = recipient_balance_after; + + request.approve(TransferApproval()); +} + +/// Validates accreditation and holding limits for a clawback, +/// updates internal balance tracking, then stamps the request. +public(package) fun approve_clawback( + registry: &mut InvestorRegistry, + request: &mut Request>>, +) { + let investor = request.data().owner(); + let amount = request.data().funds().value(); + + assert_eligible(registry, investor); + + // Allow burning to zero, otherwise check minimum + let new_balance = registry.investors.borrow(investor).balance - amount; + if (new_balance > 0 && registry.min_balance > 0) { + assert!(new_balance >= registry.min_balance, EBelowMinBalance); + }; + + registry.investors.borrow_mut(investor).balance = new_balance; + request.approve(ClawbackApproval()); +} + +/// Validates accreditation and holding limits for minting, +/// updates internal balance tracking. +public(package) fun validate_mint( + registry: &mut InvestorRegistry, + _cap: &RegistryCap, + investor: address, + amount: u64, +) { + assert_eligible(registry, investor); + + let new_balance = registry.investors.borrow(investor).balance + amount; + assert_within_limits(registry, new_balance); + + registry.investors.borrow_mut(investor).balance = new_balance; +} + +// ==== Permits ==== + +/// Permit for TransferApproval (only this module can create it). +public(package) fun transfer_approval_permit(): internal::Permit { + internal::permit() +} + +/// Permit for ClawbackApproval (only this module can create it). +public(package) fun clawback_approval_permit(): internal::Permit { + internal::permit() +} + +// ==== Admin ==== + +/// Register or update an investor's accreditation status. +public fun set_investor( + registry: &mut InvestorRegistry, + _cap: &RegistryCap, + investor: address, + accredited: bool, +) { + if (registry.investors.contains(investor)) { + registry.investors.borrow_mut(investor).accredited = accredited; + } else { + registry + .investors + .add( + investor, + InvestorProfile { + accredited, + balance: 0, + }, + ); + } +} + +/// Configure holding limits. +public fun set_holding_limits( + registry: &mut InvestorRegistry, + _cap: &RegistryCap, + min_balance: u64, + max_balance: u64, +) { + registry.min_balance = min_balance; + registry.max_balance = max_balance; +} + +// ==== Internal ==== + +/// Assert investor is registered and accredited. +fun assert_eligible(registry: &InvestorRegistry, investor: address) { + assert!(registry.investors.contains(investor), ENotWhitelisted); + assert!(registry.investors.borrow(investor).accredited, ENotAccredited); +} + +/// Assert balance is within configured min/max limits. +fun assert_within_limits(registry: &InvestorRegistry, balance: u64) { + if (registry.min_balance > 0) { + assert!(balance >= registry.min_balance, EBelowMinBalance); + }; + if (registry.max_balance > 0) { + assert!(balance <= registry.max_balance, EAboveMaxBalance); + }; +} diff --git a/packages/testing/acme_compliance/sources/acme_token.move b/packages/testing/acme_compliance/sources/acme_token.move new file mode 100644 index 0000000..33853b7 --- /dev/null +++ b/packages/testing/acme_compliance/sources/acme_token.move @@ -0,0 +1,22 @@ +/// ACME_TOKEN currency type definition and creation. +module acme_compliance::acme_token; + +use sui::coin_registry; + +public struct ACME_TOKEN has drop {} + +fun init(otw: ACME_TOKEN, ctx: &mut TxContext) { + let (initializer, cap) = coin_registry::new_currency_with_otw( + otw, + 6, + b"ACME".to_string(), + b"Acme Token".to_string(), + b"Acme security token".to_string(), + b"https://acme.example".to_string(), + ctx, + ); + let metadata = initializer.finalize(ctx); + + transfer::public_transfer(cap, ctx.sender()); + transfer::public_transfer(metadata, ctx.sender()); +} diff --git a/packages/testing/acme_compliance/sources/acme_treasury.move b/packages/testing/acme_compliance/sources/acme_treasury.move new file mode 100644 index 0000000..2425bdf --- /dev/null +++ b/packages/testing/acme_compliance/sources/acme_treasury.move @@ -0,0 +1,96 @@ +/// Treasury operations for ACME_TOKEN. +/// +/// Handles minting (deposit into Chest) and burning (clawback from Chest), +/// enforcing the same compliance rules as transfers: accreditation and +/// holding limits. +module acme_compliance::acme_treasury; + +use acme_compliance::acme_compliance::{ + Self, + InvestorRegistry, + RegistryCap, + TransferApproval, + ClawbackApproval +}; +use acme_compliance::acme_token::ACME_TOKEN; +use pas::chest::Chest; +use pas::clawback_funds::{Self, ClawbackFunds}; +use pas::namespace::Namespace; +use pas::policy::{Self, Policy}; +use pas::request::Request; +use pas::templates::Templates; +use ptb::ptb; +use std::type_name; +use sui::balance::Balance; +use sui::coin::TreasuryCap; + +// ==== Setup ==== + +/// One-time setup: PAS policy + compliance template + holding limits. +/// Call after publishing (TreasuryCap is created in `acme_token::init`). +#[allow(lint(self_transfer), unused_mut_parameter)] +entry fun setup( + namespace: &mut Namespace, + templates: &mut Templates, + registry: &mut InvestorRegistry, + treasury_cap: &mut TreasuryCap, + registry_cap: &RegistryCap, + 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(), + "acme_compliance", + "approve_transfer", + vector[ptb::ext_input("pas:request"), ptb::object_by_type()], + vector[(*type_name.as_string()).to_string()], + ); + + templates.set_template_command(acme_compliance::transfer_approval_permit(), cmd); + + policy.share(); + transfer::public_transfer(policy_cap, ctx.sender()); + + // 4. Configure holding limits: min 100 tokens, max 1M tokens (6 decimals) + registry.set_holding_limits(registry_cap, 100_000_000, 1_000_000_000_000); +} + +// ==== Mint & Burn ==== + +/// Mint tokens and deposit into an investor's Chest. +entry fun mint( + registry: &mut InvestorRegistry, + to_chest: &Chest, + cap: &mut TreasuryCap, + registry_cap: &RegistryCap, + amount: u64, +) { + registry.validate_mint(registry_cap, to_chest.owner(), amount); + to_chest.deposit_balance(cap.mint_balance(amount)); +} + +/// Burn tokens from an investor's Chest via clawback. +entry fun burn( + registry: &mut InvestorRegistry, + policy: &Policy>, + cap: &mut TreasuryCap, + mut request: Request>>, + ctx: &mut TxContext, +) { + registry.approve_clawback(&mut request); + let balance = clawback_funds::resolve(request, policy); + cap.burn(balance.into_coin(ctx)); +} From 0377c0964887a271c7c98e8e109f059afd889af9 Mon Sep 17 00:00:00 2001 From: Haris Katimertzis Date: Mon, 9 Mar 2026 19:00:12 +0200 Subject: [PATCH 4/4] fix: remove example --- packages/testing/acme_compliance/Move.lock | 35 --- packages/testing/acme_compliance/Move.toml | 7 - .../sources/acme_compliance.move | 202 ------------------ .../acme_compliance/sources/acme_token.move | 22 -- .../sources/acme_treasury.move | 96 --------- 5 files changed, 362 deletions(-) delete mode 100644 packages/testing/acme_compliance/Move.lock delete mode 100644 packages/testing/acme_compliance/Move.toml delete mode 100644 packages/testing/acme_compliance/sources/acme_compliance.move delete mode 100644 packages/testing/acme_compliance/sources/acme_token.move delete mode 100644 packages/testing/acme_compliance/sources/acme_treasury.move diff --git a/packages/testing/acme_compliance/Move.lock b/packages/testing/acme_compliance/Move.lock deleted file mode 100644 index ce7cda3..0000000 --- a/packages/testing/acme_compliance/Move.lock +++ /dev/null @@ -1,35 +0,0 @@ -# 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.acme_compliance] -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/testing/acme_compliance/Move.toml b/packages/testing/acme_compliance/Move.toml deleted file mode 100644 index 7e6e63b..0000000 --- a/packages/testing/acme_compliance/Move.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "acme_compliance" -edition = "2024.beta" - -[dependencies] -pas = { local = "../../pas" } -ptb = { local = "../../ptb" } diff --git a/packages/testing/acme_compliance/sources/acme_compliance.move b/packages/testing/acme_compliance/sources/acme_compliance.move deleted file mode 100644 index 142a637..0000000 --- a/packages/testing/acme_compliance/sources/acme_compliance.move +++ /dev/null @@ -1,202 +0,0 @@ -/// Example: Securities compliance with PAS. -/// -/// Demonstrates two common compliance rules for tokenized securities: -/// 1. Accredited investor checks (both sender and recipient must be accredited) -/// 2. Holding limits (min/max balance per investor) -/// -/// Balance tracking is maintained internally because PAS stores balances inside -/// Chests using Sui's address balances, which the compliance contract cannot -/// read directly during approval. -module acme_compliance::acme_compliance; - -use pas::clawback_funds::ClawbackFunds; -use pas::request::Request; -use pas::send_funds::SendFunds; -use sui::balance::Balance; -use sui::table::Table; -use acme_compliance::acme_token::ACME_TOKEN; - -// ==== Error Codes ==== - -#[error] -const ENotWhitelisted: vector = b"Address is not in the investor registry"; -#[error] -const ENotAccredited: vector = b"Investor is not accredited"; -#[error] -const EBelowMinBalance: vector = b"Balance would fall below minimum holding requirement"; -#[error] -const EAboveMaxBalance: vector = b"Balance would exceed maximum holding limit"; - -// ==== Witness ==== - -/// Witness stamp for approved transfers. -public struct TransferApproval() has drop; - -/// Witness stamp for approved clawbacks (burn). -public struct ClawbackApproval() has drop; - -// ==== Structs ==== - -/// On-chain registry of investor status and balances. -public struct InvestorRegistry has key { - id: UID, - investors: Table, - /// Minimum balance per investor (0 = no minimum) - min_balance: u64, - /// Maximum balance per investor (0 = no maximum) - max_balance: u64, -} - -public struct InvestorProfile has drop, store { - accredited: bool, - balance: u64, -} - -/// Admin capability for managing the registry -public struct RegistryCap has key, store { id: UID } - -// ==== Init ==== - -fun init(ctx: &mut TxContext) { - transfer::share_object(InvestorRegistry { - id: object::new(ctx), - investors: sui::table::new(ctx), - min_balance: 0, - max_balance: 0, - }); - transfer::transfer(RegistryCap { id: object::new(ctx) }, ctx.sender()); -} - -// ==== Approval Functions ==== - -/// Validates accreditation and holding limits for both parties, -/// updates internal balance tracking, then stamps the request. -public fun approve_transfer( - registry: &mut InvestorRegistry, - request: &mut Request>>, -) { - let sender = request.data().sender(); - let recipient = request.data().recipient(); - let amount = request.data().funds().value(); - - assert_eligible(registry, sender); - assert_eligible(registry, recipient); - - // Check sender's remaining balance meets minimum (if non-zero after transfer) - let sender_balance_after = registry.investors.borrow(sender).balance - amount; - if (sender_balance_after > 0 && registry.min_balance > 0) { - assert!(sender_balance_after >= registry.min_balance, EBelowMinBalance); - }; - - // Check recipient's new balance within limits - let recipient_balance_after = registry.investors.borrow(recipient).balance + amount; - assert_within_limits(registry, recipient_balance_after); - - // Update internal balance tracking - registry.investors.borrow_mut(sender).balance = sender_balance_after; - registry.investors.borrow_mut(recipient).balance = recipient_balance_after; - - request.approve(TransferApproval()); -} - -/// Validates accreditation and holding limits for a clawback, -/// updates internal balance tracking, then stamps the request. -public(package) fun approve_clawback( - registry: &mut InvestorRegistry, - request: &mut Request>>, -) { - let investor = request.data().owner(); - let amount = request.data().funds().value(); - - assert_eligible(registry, investor); - - // Allow burning to zero, otherwise check minimum - let new_balance = registry.investors.borrow(investor).balance - amount; - if (new_balance > 0 && registry.min_balance > 0) { - assert!(new_balance >= registry.min_balance, EBelowMinBalance); - }; - - registry.investors.borrow_mut(investor).balance = new_balance; - request.approve(ClawbackApproval()); -} - -/// Validates accreditation and holding limits for minting, -/// updates internal balance tracking. -public(package) fun validate_mint( - registry: &mut InvestorRegistry, - _cap: &RegistryCap, - investor: address, - amount: u64, -) { - assert_eligible(registry, investor); - - let new_balance = registry.investors.borrow(investor).balance + amount; - assert_within_limits(registry, new_balance); - - registry.investors.borrow_mut(investor).balance = new_balance; -} - -// ==== Permits ==== - -/// Permit for TransferApproval (only this module can create it). -public(package) fun transfer_approval_permit(): internal::Permit { - internal::permit() -} - -/// Permit for ClawbackApproval (only this module can create it). -public(package) fun clawback_approval_permit(): internal::Permit { - internal::permit() -} - -// ==== Admin ==== - -/// Register or update an investor's accreditation status. -public fun set_investor( - registry: &mut InvestorRegistry, - _cap: &RegistryCap, - investor: address, - accredited: bool, -) { - if (registry.investors.contains(investor)) { - registry.investors.borrow_mut(investor).accredited = accredited; - } else { - registry - .investors - .add( - investor, - InvestorProfile { - accredited, - balance: 0, - }, - ); - } -} - -/// Configure holding limits. -public fun set_holding_limits( - registry: &mut InvestorRegistry, - _cap: &RegistryCap, - min_balance: u64, - max_balance: u64, -) { - registry.min_balance = min_balance; - registry.max_balance = max_balance; -} - -// ==== Internal ==== - -/// Assert investor is registered and accredited. -fun assert_eligible(registry: &InvestorRegistry, investor: address) { - assert!(registry.investors.contains(investor), ENotWhitelisted); - assert!(registry.investors.borrow(investor).accredited, ENotAccredited); -} - -/// Assert balance is within configured min/max limits. -fun assert_within_limits(registry: &InvestorRegistry, balance: u64) { - if (registry.min_balance > 0) { - assert!(balance >= registry.min_balance, EBelowMinBalance); - }; - if (registry.max_balance > 0) { - assert!(balance <= registry.max_balance, EAboveMaxBalance); - }; -} diff --git a/packages/testing/acme_compliance/sources/acme_token.move b/packages/testing/acme_compliance/sources/acme_token.move deleted file mode 100644 index 33853b7..0000000 --- a/packages/testing/acme_compliance/sources/acme_token.move +++ /dev/null @@ -1,22 +0,0 @@ -/// ACME_TOKEN currency type definition and creation. -module acme_compliance::acme_token; - -use sui::coin_registry; - -public struct ACME_TOKEN has drop {} - -fun init(otw: ACME_TOKEN, ctx: &mut TxContext) { - let (initializer, cap) = coin_registry::new_currency_with_otw( - otw, - 6, - b"ACME".to_string(), - b"Acme Token".to_string(), - b"Acme security token".to_string(), - b"https://acme.example".to_string(), - ctx, - ); - let metadata = initializer.finalize(ctx); - - transfer::public_transfer(cap, ctx.sender()); - transfer::public_transfer(metadata, ctx.sender()); -} diff --git a/packages/testing/acme_compliance/sources/acme_treasury.move b/packages/testing/acme_compliance/sources/acme_treasury.move deleted file mode 100644 index 2425bdf..0000000 --- a/packages/testing/acme_compliance/sources/acme_treasury.move +++ /dev/null @@ -1,96 +0,0 @@ -/// Treasury operations for ACME_TOKEN. -/// -/// Handles minting (deposit into Chest) and burning (clawback from Chest), -/// enforcing the same compliance rules as transfers: accreditation and -/// holding limits. -module acme_compliance::acme_treasury; - -use acme_compliance::acme_compliance::{ - Self, - InvestorRegistry, - RegistryCap, - TransferApproval, - ClawbackApproval -}; -use acme_compliance::acme_token::ACME_TOKEN; -use pas::chest::Chest; -use pas::clawback_funds::{Self, ClawbackFunds}; -use pas::namespace::Namespace; -use pas::policy::{Self, Policy}; -use pas::request::Request; -use pas::templates::Templates; -use ptb::ptb; -use std::type_name; -use sui::balance::Balance; -use sui::coin::TreasuryCap; - -// ==== Setup ==== - -/// One-time setup: PAS policy + compliance template + holding limits. -/// Call after publishing (TreasuryCap is created in `acme_token::init`). -#[allow(lint(self_transfer), unused_mut_parameter)] -entry fun setup( - namespace: &mut Namespace, - templates: &mut Templates, - registry: &mut InvestorRegistry, - treasury_cap: &mut TreasuryCap, - registry_cap: &RegistryCap, - 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(), - "acme_compliance", - "approve_transfer", - vector[ptb::ext_input("pas:request"), ptb::object_by_type()], - vector[(*type_name.as_string()).to_string()], - ); - - templates.set_template_command(acme_compliance::transfer_approval_permit(), cmd); - - policy.share(); - transfer::public_transfer(policy_cap, ctx.sender()); - - // 4. Configure holding limits: min 100 tokens, max 1M tokens (6 decimals) - registry.set_holding_limits(registry_cap, 100_000_000, 1_000_000_000_000); -} - -// ==== Mint & Burn ==== - -/// Mint tokens and deposit into an investor's Chest. -entry fun mint( - registry: &mut InvestorRegistry, - to_chest: &Chest, - cap: &mut TreasuryCap, - registry_cap: &RegistryCap, - amount: u64, -) { - registry.validate_mint(registry_cap, to_chest.owner(), amount); - to_chest.deposit_balance(cap.mint_balance(amount)); -} - -/// Burn tokens from an investor's Chest via clawback. -entry fun burn( - registry: &mut InvestorRegistry, - policy: &Policy>, - cap: &mut TreasuryCap, - mut request: Request>>, - ctx: &mut TxContext, -) { - registry.approve_clawback(&mut request); - let balance = clawback_funds::resolve(request, policy); - cap.burn(balance.into_coin(ctx)); -}