Skip to content
Closed
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
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
3 changes: 2 additions & 1 deletion packages/pas/sources/requests/unlock_funds_request.move
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module pas::unlock_funds_request;

use pas::namespace::Namespace;
use pas::{namespace::Namespace, versioning::breaking_version};
use sui::balance::Balance;

#[error(code = 0)]
Expand Down Expand Up @@ -40,6 +40,7 @@ public fun resolve_unrestricted<T>(
namespace: &Namespace,
): Balance<T> {
assert!(!namespace.rule_exists<T>(), ECannotResolveManagedAssets);
namespace.versioning().assert_is_valid_version();
request.resolve()
}

Expand Down
25 changes: 23 additions & 2 deletions packages/pas/sources/rule.move
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use pas::{
namespace::Namespace,
transfer_funds_request::TransferFundsRequest,
unlock_funds_request::UnlockFundsRequest,
vault::Vault
vault::Vault,
versioning::Versioning
};
use ptb::ptb::Command;
use std::{string::String, type_name::{Self, TypeName}};
Expand Down Expand Up @@ -41,6 +42,8 @@ public struct Rule<phantom T> has key {
/// Initially, this only means it approves "transfers", "clawbacks" and "mints (managed scenario)".
/// In the future, there might be NFT version of these rules.
auth_witness: TypeName,
/// Block versions to break backwards compatibility -- only used in case of emergency.
versioning: Versioning,
}

/// This is the key under which we save a DF that stores the resolution info.
Expand All @@ -64,9 +67,13 @@ public fun new<T, U: drop>(
): Rule<T> {
assert!(!namespace.rule_exists<T>(), ERuleAlreadyExists);

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

let mut rule = Rule<T> {
id: derived_object::claim(namespace.uid_mut(), keys::rule_key<T>()),
auth_witness: type_name::with_defining_ids<U>(),
versioning,
};

dynamic_field::add<_, VecMap<String, Command>>(
Expand All @@ -90,6 +97,7 @@ public fun enable_funds_management<T>(
clawback_allowed: bool,
) {
assert!(!rule.is_fund_management_enabled(), EFundManagementAlreadyEnabled);
rule.versioning.assert_is_valid_version();
dynamic_field::add(&mut rule.id, FundsClawbackState(), clawback_allowed);
}

Expand All @@ -101,6 +109,7 @@ public fun resolve_unlock_funds<T, U: drop>(
): Balance<T> {
rule.assert_is_valid_issuer_proof!<_, U>();
rule.assert_is_fund_management_enabled!();
rule.versioning.assert_is_valid_version();
request.resolve()
}

Expand All @@ -114,6 +123,7 @@ public fun resolve_transfer_funds<T, U: drop>(
rule.assert_is_valid_issuer_proof!<_, U>();
rule.assert_is_fund_management_enabled!();
// destructuring the request to finalize the transfer.
rule.versioning.assert_is_valid_version();
request.resolve();
}

Expand All @@ -129,6 +139,7 @@ public fun clawback_funds<T, U: drop>(
): Balance<T> {
assert!(rule.is_fund_clawback_allowed(), EClawbackNotAllowed);
rule.assert_is_valid_issuer_proof!<_, U>();
rule.versioning.assert_is_valid_version();

from.withdraw<T>(amount)
}
Expand All @@ -137,13 +148,15 @@ public fun clawback_funds<T, U: drop>(
/// Aborts early if the management for funds has not been enabled for `T`.
public fun is_fund_clawback_allowed<T>(rule: &Rule<T>): bool {
rule.assert_is_fund_management_enabled!();
rule.versioning.assert_is_valid_version();
*dynamic_field::borrow(&rule.id, FundsClawbackState())
}

/// Set the move command for a specific action type.
/// NOTE: If the action type already exists, it will be replaced.
public fun set_action_command<T, U: drop, A>(rule: &mut Rule<T>, command: Command, _stamp: U) {
rule.assert_is_valid_issuer_proof!<_, U>();
rule.versioning.assert_is_valid_version();
let action_type = type_name::with_defining_ids<A>();

let action_type_str = (*action_type.as_string()).to_string();
Expand All @@ -161,12 +174,20 @@ public fun set_action_command<T, U: drop, A>(rule: &mut Rule<T>, command: Comman
info_map.insert(action_type_str, command);
}

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

public fun auth_witness<T>(rule: &Rule<T>): TypeName { rule.auth_witness }

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

public fun auth_witness<T>(rule: &Rule<T>): TypeName { rule.auth_witness }
public(package) fun versioning<T>(rule: &Rule<T>): Versioning { rule.versioning }

macro fun assert_is_fund_management_enabled<$T>($rule: &Rule<$T>) {
let rule = $rule;
Expand Down
23 changes: 22 additions & 1 deletion packages/pas/sources/vault.move
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use pas::{
keys,
namespace::{Self, Namespace},
transfer_funds_request::{Self, TransferFundsRequest},
unlock_funds_request::{Self, UnlockFundsRequest}
unlock_funds_request::{Self, UnlockFundsRequest},
versioning::Versioning
};
use sui::{balance::{Self, Balance}, derived_object};

Expand All @@ -28,6 +29,8 @@ public struct Vault has key {
/// There's ONLY ONE namespace in the system, but this helps us avoid having
/// `&Namespace` inputs in all functions that need to derive the IDs.
namespace_id: ID,
/// Block versions to break backwards compatibility -- only used in case of emergency.
versioning: Versioning,
}

/// A proof that address has authenticated. This allows for uniform access control between both
Expand All @@ -38,10 +41,14 @@ public struct Auth(address) has drop;
public fun create(namespace: &mut Namespace, owner: address): Vault {
assert!(!namespace.vault_exists(owner), EVaultAlreadyExists);

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

Vault {
id: derived_object::claim(namespace.uid_mut(), keys::vault_key(owner)),
owner,
namespace_id: object::id(namespace),
versioning,
}
}

Expand All @@ -66,6 +73,7 @@ public fun unlock_funds<T>(
_ctx: &mut TxContext,
): UnlockFundsRequest<T> {
auth.assert_is_valid_for_vault!(vault);
vault.versioning.assert_is_valid_version();
unlock_funds_request::new(vault.owner, vault.id.to_inner(), vault.withdraw(amount))
}

Expand All @@ -78,6 +86,7 @@ public fun transfer_funds<T>(
_ctx: &mut TxContext,
): TransferFundsRequest<T> {
auth.assert_is_valid_for_vault!(from);
from.versioning.assert_is_valid_version();
from.internal_transfer_funds<T>(to.owner, amount)
}

Expand All @@ -94,6 +103,7 @@ public fun unsafe_transfer_funds<T>(
_ctx: &mut TxContext,
): TransferFundsRequest<T> {
auth.assert_is_valid_for_vault!(from);
from.versioning.assert_is_valid_version();
from.internal_transfer_funds<T>(recipient_address, amount)
}

Expand All @@ -112,13 +122,24 @@ public fun owner(vault: &Vault): address {
}

public fun deposit_funds<T>(vault: &Vault, balance: Balance<T>) {
vault.versioning.assert_is_valid_version();
balance::send_funds(balance, object::id(vault).to_address());
}

/// Permissionless operation to bring versioning up-to-date with the namespace.
public fun sync_versioning(vault: &mut Vault, namespace: &Namespace) {
vault.versioning = namespace.versioning();
}

public(package) fun withdraw<T>(vault: &mut Vault, amount: u64): Balance<T> {
vault.versioning.assert_is_valid_version();
balance::redeem_funds(vault.id.withdraw_funds_from_object(amount))
}

public(package) fun versioning(vault: &Vault): Versioning {
vault.versioning
}

/// Verify that the ownership proof matches the vaults owner.
macro fun assert_is_valid_for_vault($proof: &Auth, $vault: &Vault) {
let proof = $proof;
Expand Down
47 changes: 47 additions & 0 deletions packages/pas/sources/versioning.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/// Versioning module.
///
/// This module is responsible for managing the versioning of the package.
///
/// It allows for blocking specific versions of the package in case of emergency, or to slowly deprecate an earlier feature.
module pas::versioning;

use sui::vec_set::{Self, VecSet};

#[error(code = 0)]
const EInvalidVersion: vector<u8> =
b"This version of the core package (pas) is no longer supported. Please use the latest version of the package.";

public struct Versioning has copy, drop, store {
blocked_versions: VecSet<u64>,
}

public(package) fun new(): Versioning {
Versioning {
blocked_versions: vec_set::empty(),
}
}

public(package) fun block_version(versioning: &mut Versioning, version: u64) {
versioning.blocked_versions.insert(version);
}

public(package) fun unblock_version(versioning: &mut Versioning, version: u64) {
versioning.blocked_versions.remove(&version);
}

/// Verify that a version is not part of the blocked version list.
public fun is_valid_version(versioning: &Versioning, version: u64): bool {
!versioning.blocked_versions.contains(&version)
}

public fun assert_is_valid_version(versioning: &Versioning) {
assert!(versioning.is_valid_version(breaking_version!()), EInvalidVersion);
}

/// The current package's breaking version.
///
/// A breaking version is not equal to the released version. It acts as a marker to allow
/// disabling specific packages.
///
/// This is bumped only in case of emergency, or to slowly deprecate an earlier feature.
public macro fun breaking_version(): u64 { 1 }
Loading
Loading