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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@ The P-Assets Standard is a framework for issuing and managing permissioned balan

- **Permissioned Transfers**: All transfers must go through chests and be approved by custom transfer rules
- **Chest-Based Architecture**: Tokens can only be held in chests, with automatic balance tracking
- **Flexible Rules System**: Each token type has associated rules that govern transfers with jurisdiction-specific compliance
- **Flexible Policies System**: Each token type has associated rules that govern transfers with jurisdiction-specific compliance
- **Optional Clawback**: Regulatory compliance feature that allows token recovery when legally required

## How It Works

1. **Setup**: Registry is created as a shared object, token issuers register their tokens with rules
2. **Chest Creation**: Chests are derived for each address that needs to hold tokens
3. **Transfers**: Initiated from source chest, creating a transfer request that must be resolved by the rule
3. **Transfers**: Initiated from source chest, creating a transfer request that must be resolved by the policy
4. **Resolution**: Token-specific smart contracts validate and approve transfers based on compliance rules

## Wallet & SDK Integration

### Simple Discovery
The standard uses derived objects for predictable addresses:
- **Single chest per user** which holds the balances of the user
- **No indexing required** - chest and rule addresses are deterministically computable
- **No indexing required** - chest and policy addresses are deterministically computable
- **One query** to see all user balances via dynamic fields on their chest

### Easy Resolution

Each rule contains `Command` instructions that tell SDKs exactly how to resolve transfers - no need to understand complex on-chain logic. SDKs simply read the command and construct the appropriate transaction.
Each policy contains `Command` instructions that tell SDKs exactly how to resolve transfers - no need to understand complex on-chain logic. SDKs simply read the command and construct the appropriate transaction.

## Security Features

Expand Down
4 changes: 2 additions & 2 deletions packages/pas/sources/chest.move
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public fun create_and_share(namespace: &mut Namespace, owner: address) {
}

/// Enables a fund unlock flow.
/// This is useful for assets that are not managed by a Rule within the system, or
/// This is useful for assets that are not managed by a Policy within the system, or
/// if there's a special case where an issuer allows balances to flow out of the system.
public fun unlock_funds<T>(
chest: &mut Chest,
Expand Down Expand Up @@ -95,7 +95,7 @@ public fun transfer_funds<T>(
/// Initiate a clawback request for an amount of funds.
/// This takes no `Auth`, as it's an admin action.
///
/// This can only ever finalize if clawback is enabled in the rule.
/// This can only ever finalize if clawback is enabled in the policy.
public fun clawback_funds<T>(
from: &mut Chest,
amount: u64,
Expand Down
6 changes: 3 additions & 3 deletions packages/pas/sources/keys.move
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module pas::keys;
use std::string::String;
use sui::vec_set::{Self, VecSet};

/// Key for deriving `Rule<T>` from the namespace
public struct RuleKey<phantom T>() has copy, drop, store;
/// Key for deriving `Policy<T>` from the namespace
public struct PolicyKey<phantom T>() has copy, drop, store;

/// Key for deriving `Chest` from the namespace
public struct ChestKey(address) has copy, drop, store;
Expand All @@ -13,7 +13,7 @@ public struct ChestKey(address) has copy, drop, store;
public struct TemplateKey() has copy, drop, store;

/// WARNING: these should only be used internally.
public(package) fun rule_key<T>(): RuleKey<T> { RuleKey<T>() }
public(package) fun policy_key<T>(): PolicyKey<T> { PolicyKey<T>() }

public(package) fun chest_key(owner: address): ChestKey { ChestKey(owner) }

Expand Down
16 changes: 8 additions & 8 deletions packages/pas/sources/namespace.move
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
///
/// Namespace is responsible for creating objects that are easy to query & find:
/// 1. Chests
/// 2. Rules
/// 2. Policies
/// ... any other module we might add in the future
module pas::namespace;

Expand All @@ -19,7 +19,7 @@ const EUpgradeCapPackageMismatch: vector<u8> =
const EUpgradeCapNotSet: vector<u8> =
b"The upgrade cap is not set for this namespace, making it unusable.";

/// The namespace is only used for address derivation of chests, rules, etc.
/// The namespace is only used for address derivation of chests, policies, etc.
///
/// Namespace is a singleton -- there's one global version for it.
public struct Namespace has key {
Expand Down Expand Up @@ -69,14 +69,14 @@ public fun unblock_version(namespace: &mut Namespace, cap: &UpgradeCap, version:
namespace.versioning.unblock_version(version);
}

/// Check if `Rule<T>` exists in the namespace
public fun rule_exists<T>(namespace: &Namespace): bool {
derived_object::exists(&namespace.id, keys::rule_key<T>())
/// Check if `Policy<T>` exists in the namespace
public fun policy_exists<T>(namespace: &Namespace): bool {
derived_object::exists(&namespace.id, keys::policy_key<T>())
}

/// The derived address for `Rule<T>`
public fun rule_address<T>(namespace: &Namespace): address {
derived_object::derive_address(namespace.id.to_inner(), keys::rule_key<T>())
/// The derived address for `Policy<T>`
public fun policy_address<T>(namespace: &Namespace): address {
derived_object::derive_address(namespace.id.to_inner(), keys::policy_key<T>())
}

public fun chest_exists(namespace: &Namespace, owner: address): bool {
Expand Down
155 changes: 155 additions & 0 deletions packages/pas/sources/policy.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
module pas::policy;

use pas::{keys, namespace::Namespace, versioning::Versioning};
use std::{string::String, type_name::{Self, TypeName}};
use sui::{
coin::TreasuryCap,
derived_object,
dynamic_field,
vec_map::{Self, VecMap},
vec_set::{Self, VecSet}
};

#[error(code = 1)]
const EPolicyAlreadyExists: vector<u8> = b"A policy for this token type already exists.";
#[error(code = 2)]
const EFundManagementNotEnabled: vector<u8> = b"Fund management is not enabled for this policy.";
#[error(code = 3)]
const EFundManagementAlreadyEnabled: vector<u8> =
b"Fund management is already enabled for this policy.";
#[error(code = 4)]
const EInvalidAction: vector<u8> = b"Invalid action type.";
#[error(code = 5)]
const ENotSupportedAction: vector<u8> =
b"The requested action type is not supported by the issuer.";

/// A policy is set by the owner of `T`, and points to a `TypeName` that needs
/// to be verified by the entity's contract.
///
/// This is derived from `namespace, TypeName<T>`
public struct Policy<phantom T> has key {
id: UID,
/// The required approvals per request type.
/// The key must be one of the request types (e.g. `transfer_funds`, `unlock_funds` or `clawback_funds`).
///
/// The value is a vector of approvals that need to be gather to resolve the request.
required_approvals: VecMap<String, VecSet<TypeName>>,
/// Block versions to break backwards compatibility -- only used in case of emergency.
versioning: Versioning,
}

/// Capability for managing a `Policy<T>`. It's 1:1.
public struct PolicyCap<phantom T> has key, store {
id: UID,
}

/// Key that is used to derive the PolicyCap ID from `Policy<T>`
public struct PolicyCapKey() has copy, drop, store;

/// A flag saved as <FundsClawbackState(), bool> to check if claw-backs are enabled
/// for a given asset.
public struct FundsClawbackState() has copy, drop, store;

/// Create a new `Policy` for `T`.
/// We use `Permit<T>` as the proof of ownership for `T`.
public fun new<T>(namespace: &mut Namespace, _: internal::Permit<T>): (Policy<T>, PolicyCap<T>) {
assert!(!namespace.policy_exists<T>(), EPolicyAlreadyExists);

let versioning = namespace.versioning();
versioning.assert_is_valid_version();

let mut policy = Policy<T> {
id: derived_object::claim(namespace.uid_mut(), keys::policy_key<T>()),
required_approvals: vec_map::empty(),
versioning,
};

let policy_cap = PolicyCap<T> {
id: derived_object::claim(&mut policy.id, PolicyCapKey()),
};

(policy, policy_cap)
}

public fun share<T>(policy: Policy<T>) {
transfer::share_object(policy);
}

/// Enables funds management for a given `T`, adding a DF that tracks the clawback status (true/false).
/// This can only be called once. After calling it, the clawback status can never change!
public fun enable_funds_management<T>(
policy: &mut Policy<T>,
_: &mut TreasuryCap<T>,
clawback_allowed: bool,
) {
assert!(!policy.is_fund_management_enabled(), EFundManagementAlreadyEnabled);
policy.versioning.assert_is_valid_version();
dynamic_field::add(&mut policy.id, FundsClawbackState(), clawback_allowed);
}

/// Get the set of required approvals for a given action.
public fun required_approvals<T>(policy: &Policy<T>, action_type: String): VecSet<TypeName> {
assert!(policy.required_approvals.contains(&action_type), ENotSupportedAction);
*policy.required_approvals.get(&action_type)
}

/// For a set of actions, set the approvals required to conclude the action.
///
/// Supported actions: ["transfer_funds", "unlock_funds", "clawback_funds"]
public(package) fun set_required_approvals<T>(
policy: &mut Policy<T>,
_: &PolicyCap<T>,
action: String,
approvals: VecSet<TypeName>,
) {
policy.versioning.assert_is_valid_version();
assert!(keys::is_valid_action(action), EInvalidAction);

if (policy.required_approvals.contains(&action)) {
policy.required_approvals.remove(&action);
};
policy.required_approvals.insert(action, approvals);
}

public fun set_required_approval<T, A: drop>(
policy: &mut Policy<T>,
cap: &PolicyCap<T>,
action: String,
) {
policy.set_required_approvals(
cap,
action,
vec_set::singleton(type_name::with_defining_ids<A>()),
);
}

/// Remove the action approval for a given action (this will make all requests not resolve).
public fun remove_action_approval<T>(policy: &mut Policy<T>, _: &PolicyCap<T>, action: String) {
policy.versioning.assert_is_valid_version();
policy.required_approvals.remove(&action);
}

/// Check if clawback is allowed or not.
/// Aborts early if the management for funds has not been enabled for `T`.
public fun is_fund_clawback_allowed<T>(policy: &Policy<T>): bool {
policy.assert_is_fund_management_enabled();
policy.versioning.assert_is_valid_version();
*dynamic_field::borrow(&policy.id, FundsClawbackState())
}

/// Allows syncing the versioning of a policy to the namespace's versioning.
/// This is permission-less and can be done
public fun sync_versioning<T>(policy: &mut Policy<T>, namespace: &Namespace) {
policy.versioning = namespace.versioning();
}

/// Check if fund management is enabled for a given `T`.
public(package) fun is_fund_management_enabled<T>(policy: &Policy<T>): bool {
dynamic_field::exists_(&policy.id, FundsClawbackState())
}

public(package) fun versioning<T>(policy: &Policy<T>): Versioning { policy.versioning }

public fun assert_is_fund_management_enabled<T>(policy: &Policy<T>) {
assert!(policy.is_fund_management_enabled(), EFundManagementNotEnabled);
}
18 changes: 9 additions & 9 deletions packages/pas/sources/requests/clawback_funds.move
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
module pas::clawback_funds;

use pas::{keys::clawback_funds_action, request::{Self, Request}, rule::Rule};
use pas::{keys::clawback_funds_action, policy::Policy, request::{Self, Request}};
use sui::balance::Balance;

#[error(code = 1)]
const EClawbackNotAllowed: vector<u8> =
b"Attempted to clawback tokens when clawback is not enabled for this rule.";
b"Attempted to clawback tokens when clawback is not enabled for this policy.";

public struct ClawbackFunds<phantom T> {
/// `owner` is the wallet OR object address, NOT the chest address
Expand Down Expand Up @@ -35,13 +35,13 @@ public(package) fun new<T>(
}

/// Resolve a clawback funds request by:
/// 1. Verify rule is valid
/// 2. Verify rule has clawback enabled
/// 3. Make sure rule has enabled clawback resolution
public fun resolve<T>(request: Request<ClawbackFunds<T>>, rule: &Rule<T>): Balance<T> {
rule.versioning().assert_is_valid_version();
assert!(rule.is_fund_clawback_allowed(), EClawbackNotAllowed);
let data = request.resolve(rule.required_approvals(clawback_funds_action()));
/// 1. Verify policy is valid
/// 2. Verify policy has clawback enabled
/// 3. Make sure policy has enabled clawback resolution
public fun resolve<T>(request: Request<ClawbackFunds<T>>, policy: &Policy<T>): Balance<T> {
policy.versioning().assert_is_valid_version();
assert!(policy.is_fund_clawback_allowed(), EClawbackNotAllowed);
let data = request.resolve(policy.required_approvals(clawback_funds_action()));

let ClawbackFunds { balance, .. } = data;
balance
Expand Down
12 changes: 6 additions & 6 deletions packages/pas/sources/requests/transfer_funds.move
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module pas::transfer_funds;

use pas::{keys::transfer_funds_action, request::{Self, Request}, rule::Rule};
use pas::{keys::transfer_funds_action, policy::Policy, request::{Self, Request}};
use sui::balance::{Self, Balance};

/// A transfer request that is generated once a Permissioned Funds Transfer is initiated.
///
/// A hot potato that is issued when a transfer is initiated.
/// It can only be resolved by presenting a witness `U` that is the witness of `Rule<T>`
/// It can only be resolved by presenting a witness `U` that is the witness of `Policy<T>`
///
/// This enables the `resolve` function of each smart contract to
/// be flexible and implement its own mechanisms for validation.
Expand Down Expand Up @@ -62,10 +62,10 @@ public(package) fun new<T>(
}

/// resolve a transfer request, if funds management is enabled & there are enough approvals.
public fun resolve<T>(request: Request<TransferFunds<T>>, rule: &Rule<T>) {
rule.versioning().assert_is_valid_version();
rule.assert_is_fund_management_enabled();
let data = request.resolve(rule.required_approvals(transfer_funds_action()));
public fun resolve<T>(request: Request<TransferFunds<T>>, policy: &Policy<T>) {
policy.versioning().assert_is_valid_version();
policy.assert_is_fund_management_enabled();
let data = request.resolve(policy.required_approvals(transfer_funds_action()));

let TransferFunds { balance, recipient_chest_id, .. } = data;
balance::send_funds(balance, recipient_chest_id.to_address());
Expand Down
25 changes: 15 additions & 10 deletions packages/pas/sources/requests/unlock_funds.move
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
module pas::unlock_funds;

use pas::{keys::unlock_funds_action, namespace::Namespace, request::{Self, Request}, rule::Rule};
use pas::{
keys::unlock_funds_action,
namespace::Namespace,
policy::Policy,
request::{Self, Request}
};
use sui::{balance::Balance, vec_set};

#[error(code = 0)]
Expand All @@ -10,8 +15,8 @@ const ECannotResolveManagedAssets: vector<u8> =
/// An unlock funds request that is generated once a Permissioned Funds Transfer is initiated.
///
/// This can be resolved in two ways:
/// 1. If the asset is `permissioned` (there's a `Rule<T>` for that asset), it can only be resolved by the creator
/// by calling `rule::resolve_unlock_funds`
/// 1. If the asset is `permissioned` (there's a `Policy<T>` for that asset), it can only be resolved by the creator
/// by calling `policy::resolve_unlock_funds`
/// 2. If the asset is not permissioned, it can be resolved by any address by calling `unlock_funds::resolve_unrestricted`
public struct UnlockFunds<phantom T> {
/// `from` is the wallet OR object address, NOT the chest address
Expand All @@ -30,16 +35,16 @@ public fun chest_id<T>(request: &UnlockFunds<T>): ID { request.chest_id }

public fun amount<T>(request: &UnlockFunds<T>): u64 { request.amount }

/// This enables unlocking assets that are not managed by a Rule within the system.
/// If a `Rule<T>` exists, they can only be resolved from within the system.
/// This enables unlocking assets that are not managed by a Policy within the system.
/// If a `Policy<T>` exists, they can only be resolved from within the system.
///
/// For example, `SUI` will never be a managed asset, so the owner needs to be able
/// to withdraw if anyone transfers some to their chest.
public fun resolve_unrestricted<T>(
request: Request<UnlockFunds<T>>,
namespace: &Namespace,
): Balance<T> {
assert!(!namespace.rule_exists<T>(), ECannotResolveManagedAssets);
assert!(!namespace.policy_exists<T>(), ECannotResolveManagedAssets);
namespace.versioning().assert_is_valid_version();
let data = request.resolve(vec_set::empty());
let UnlockFunds { balance, .. } = data;
Expand All @@ -61,10 +66,10 @@ public(package) fun new<T>(

/// Resolve an unlock funds request as long as funds management is enabled and
/// there are enough valid approvals.
public fun resolve<T>(request: Request<UnlockFunds<T>>, rule: &Rule<T>): Balance<T> {
rule.versioning().assert_is_valid_version();
rule.assert_is_fund_management_enabled();
let data = request.resolve(rule.required_approvals(unlock_funds_action()));
public fun resolve<T>(request: Request<UnlockFunds<T>>, policy: &Policy<T>): Balance<T> {
policy.versioning().assert_is_valid_version();
policy.assert_is_fund_management_enabled();
let data = request.resolve(policy.required_approvals(unlock_funds_action()));

let UnlockFunds { balance, .. } = data;
balance
Expand Down
Loading