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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The P-Assets Standard is a framework for issuing and managing permissioned balan
1. Each address has a single vault (derived address, with easy discoverability). Objects can own vaults as well. This enables with account abstractions / defi protocols implementations
2. Vault uses address (object) balances, so RPCs work out of the box (wallet just treats the vault address like a normal one). Wallets/explorers needs to query for the derived vault address to get balances.
3. Balances can only move from vault to vault (either by safe vault-to-vault deposits, or deriving the recipient with `unsafe_` calls)
4. When a transfer is initiated, a `TransferFundsRequest` is issued, which can be resolved, on the PTB layer, calling the `Command` that is specified by the issuer. The issuer can "approve" it in their own package by presenting a witness. Any custom logic (KYC, checks) can be implemented there.
4. When a transfer is initiated, a `TransferFunds` is issued, which can be resolved, on the PTB layer, calling the `Command` that is specified by the issuer. The issuer can "approve" it in their own package by presenting a witness. Any custom logic (KYC, checks) can be implemented there.
5. Clawback is available (vaults are shared and a clawback can be initiated using the issuer's witness).

(To be added: Issuers can attach "metadata" to user's Vaults (such as `KYC` stamps or AML stamps they issue), which they can then check on their transfer functions to restrict movement. Since vaults are shared, issuers can revoke these stamps at any moment).
Expand Down
30 changes: 30 additions & 0 deletions packages/pas/sources/keys.move
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
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 `Vault` from the namespace
public struct VaultKey(address) has copy, drop, store;

/// Key for deriving `Templates` from the namespace
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 vault_key(owner: address): VaultKey { VaultKey(owner) }

public(package) fun template_key(): TemplateKey { TemplateKey() }

const TRANSFER_FUNDS_ACTION_TYPE: vector<u8> = b"transfer_funds";
const UNLOCK_FUNDS_ACTION_TYPE: vector<u8> = b"unlock_funds";
const CLAWBACK_FUNDS_ACTION_TYPE: vector<u8> = b"clawback_funds";

public fun transfer_funds_action(): String { TRANSFER_FUNDS_ACTION_TYPE.to_string() }

public fun unlock_funds_action(): String { UNLOCK_FUNDS_ACTION_TYPE.to_string() }

public fun clawback_funds_action(): String { CLAWBACK_FUNDS_ACTION_TYPE.to_string() }

public fun actions(): VecSet<String> {
vec_set::from_keys(vector[
TRANSFER_FUNDS_ACTION_TYPE.to_string(),
UNLOCK_FUNDS_ACTION_TYPE.to_string(),
CLAWBACK_FUNDS_ACTION_TYPE.to_string(),
])
}

public fun is_valid_action(action: String): bool {
actions().contains(&action)
}
65 changes: 63 additions & 2 deletions packages/pas/sources/namespace.move
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,69 @@
/// ... any other module we might add in the future
module pas::namespace;

use pas::keys;
use sui::derived_object;
use pas::{keys, versioning::{Self, Versioning}};
use std::type_name;
use sui::{derived_object, package::UpgradeCap};

#[error(code = 0)]
const EUpgradeCapAlreadySet: vector<u8> = b"The upgrade cap is already set for this namespace.";
#[error(code = 1)]
const EUpgradeCapPackageMismatch: vector<u8> =
b"The upgrade cap package does not match the package.";
#[error(code = 2)]
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 vaults, rules, etc.
///
/// Namespace is a singleton -- there's one global version for it.
public struct Namespace has key {
id: UID,
/// The UpgradeCap of the package, used as the "ownership" capability, mainly to
/// block versions of the package in case of emergency.
upgrade_cap_id: Option<ID>,
/// Enables "blocking" versions of the package
versioning: Versioning,
}

// We publish the Namespace in the `init` function, since it's "singleton".
fun init(ctx: &mut TxContext) {
transfer::share_object(Namespace {
id: object::new(ctx),
upgrade_cap_id: option::none(),
versioning: versioning::new(),
});
}

/// Setup the namespace (links the `UpgradeCap`) once after publishing. This makes the UpgradeCap the "admin" capability
/// (which can set the blocked versions of a package).
entry fun setup(namespace: &mut Namespace, cap: &UpgradeCap) {
// setup is already done for upgrade cap
assert!(namespace.upgrade_cap_id.is_none(), EUpgradeCapAlreadySet);

// Verify the `UpgradeCap` is correct for this package.
assert!(
type_name::with_defining_ids<Namespace>().address_string() == cap.package().to_address().to_ascii_string(),
EUpgradeCapPackageMismatch,
);

namespace.upgrade_cap_id = option::some(object::id(cap));
}

/// Allows the package admin to block a version of the package.
///
/// This is only used in case of emergency (e.g. security consideration), or if there is a breaking change
public fun block_version(namespace: &mut Namespace, cap: &UpgradeCap, version: u64) {
assert!(namespace.is_valid_upgrade_cap(cap), EUpgradeCapPackageMismatch);
namespace.versioning.block_version(version);
}

/// Allows the package admin to unblock a version of the package.
public fun unblock_version(namespace: &mut Namespace, cap: &UpgradeCap, version: u64) {
assert!(namespace.is_valid_upgrade_cap(cap), EUpgradeCapPackageMismatch);
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>())
Expand All @@ -43,11 +92,21 @@ public(package) fun vault_address_from_id(namespace_id: ID, owner: address): add
derived_object::derive_address(namespace_id, keys::vault_key(owner))
}

public(package) fun versioning(namespace: &Namespace): Versioning {
namespace.versioning
}

/// Expose `uid_mut` so we can claim derived objects from other modules.
public(package) fun uid_mut(namespace: &mut Namespace): &mut UID {
// We can only do it after we have set the upgrade cap (to prevent usage of the system before it has been set up).
assert!(namespace.upgrade_cap_id.is_some(), EUpgradeCapNotSet);
&mut namespace.id
}

fun is_valid_upgrade_cap(namespace: &Namespace, cap: &UpgradeCap): bool {
namespace.upgrade_cap_id.is_some_and!(|id| id == object::id(cap))
}

#[test_only]
public fun init_for_testing(ctx: &mut TxContext) {
init(ctx);
Expand All @@ -57,6 +116,8 @@ public fun init_for_testing(ctx: &mut TxContext) {
public fun create_for_testing(ctx: &mut TxContext): Namespace {
Namespace {
id: object::new(ctx),
upgrade_cap_id: option::none(),
versioning: versioning::new(),
}
}

Expand Down
48 changes: 48 additions & 0 deletions packages/pas/sources/requests/clawback_funds.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module pas::clawback_funds;

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

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

public struct ClawbackFunds<phantom T> {
/// `owner` is the wallet OR object address, NOT the vault address
owner: address,
/// The ID of the vault the funds are coming from
vault_id: ID,
/// The balance that is being clawed back.
balance: Balance<T>,
}

public fun owner<T>(request: &ClawbackFunds<T>): address { request.owner }

public fun vault_id<T>(request: &ClawbackFunds<T>): ID { request.vault_id }

public fun amount<T>(request: &ClawbackFunds<T>): u64 { request.balance.value() }

public(package) fun new<T>(
owner: address,
vault_id: ID,
balance: Balance<T>,
): Request<ClawbackFunds<T>> {
request::new(ClawbackFunds {
owner,
vault_id,
balance,
})
}

/// 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()));

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

use std::type_name::{Self, TypeName};
use sui::vec_set::{Self, VecSet};

#[error(code = 0)]
const EInsufficientApprovals: vector<u8> =
b"Cannot resolve request: insufficient approvals received.";

/// A base request type.
/// Examples:
/// `Request<TransferFunds<T>>`
/// `Request<UnlockFunds<T>>`
public struct Request<K> {
/// The collected approvals for this request
approvals: VecSet<TypeName>,
data: K,
}

/// Adds an approval to a request. Can be called to resolve rules
public fun approve<K, U: drop>(request: &mut Request<K>, _approval: U) {
request.approvals.insert(type_name::with_defining_ids<U>());
}

public fun data<K>(request: &Request<K>): &K {
&request.data
}

public fun approvals<K>(request: &Request<K>): VecSet<TypeName> {
request.approvals
}

public(package) fun new<K>(data: K): Request<K> {
Request {
approvals: vec_set::empty(),
data,
}
}

/// An internal function to resolve a request.
public(package) fun resolve<K>(request: Request<K>, required_approvals: VecSet<TypeName>): K {
required_approvals.keys().do_ref!(|approval| {
assert!(request.approvals.contains(approval), EInsufficientApprovals);
});
let Request { data, .. } = request;
data
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module pas::transfer_funds_request;
module pas::transfer_funds;

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

/// A transfer request that is generated once a Permissioned Funds Transfer is initiated.
Expand All @@ -16,7 +17,7 @@ use sui::balance::{Self, Balance};
/// - Emit regulatory events
/// - Handle dividends/distributions
/// - Implement any jurisdiction-specific rules
public struct TransferFundsRequest<phantom T> {
public struct TransferFunds<phantom T> {
/// `sender` is the wallet OR object address, NOT the vault address
sender: address,
/// `recipient` is the wallet OR object address, NOT the vault address
Expand All @@ -31,39 +32,41 @@ public struct TransferFundsRequest<phantom T> {
balance: Balance<T>,
}

public fun sender<T>(request: &TransferFundsRequest<T>): address { request.sender }
public fun sender<T>(request: &TransferFunds<T>): address { request.sender }

public fun recipient<T>(request: &TransferFundsRequest<T>): address { request.recipient }
public fun recipient<T>(request: &TransferFunds<T>): address { request.recipient }

public fun sender_vault_id<T>(request: &TransferFundsRequest<T>): ID { request.sender_vault_id }
public fun sender_vault_id<T>(request: &TransferFunds<T>): ID { request.sender_vault_id }

public fun recipient_vault_id<T>(request: &TransferFundsRequest<T>): ID {
public fun recipient_vault_id<T>(request: &TransferFunds<T>): ID {
request.recipient_vault_id
}

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

public(package) fun new<T>(
sender: address,
recipient: address,
sender_vault_id: ID,
recipient_vault_id: ID,
balance: Balance<T>,
): TransferFundsRequest<T> {
TransferFundsRequest {
): Request<TransferFunds<T>> {
request::new(TransferFunds {
sender,
recipient,
sender_vault_id,
recipient_vault_id,
amount: balance.value(),
balance,
}
})
}

/// Internal function to resolve a transfer request.
/// WARNING: This must only be called by `rule.move` after verifying the witness,
/// it should never become public.
public(package) fun resolve<T>(request: TransferFundsRequest<T>) {
let TransferFundsRequest { balance, recipient_vault_id, .. } = request;
/// 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()));

let TransferFunds { balance, recipient_vault_id, .. } = data;
balance::send_funds(balance, recipient_vault_id.to_address());
}
Loading