From cd26dc0585b7bf47bca0908f9a4ed3a4c0e1959a Mon Sep 17 00:00:00 2001 From: Manos Liolios Date: Mon, 9 Feb 2026 13:46:54 +0200 Subject: [PATCH 1/2] Proposed versioning story (tests wip) --- packages/pas/sources/namespace.move | 65 ++++++++++++++++++++++++- packages/pas/sources/rule.move | 22 ++++++++- packages/pas/sources/vault.move | 19 +++++++- packages/pas/sources/versioning.move | 47 ++++++++++++++++++ packages/pas/tests/e2e.move | 73 +++++++++++++++++++++++++++- 5 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 packages/pas/sources/versioning.move diff --git a/packages/pas/sources/namespace.move b/packages/pas/sources/namespace.move index 3c9a217..35087d8 100644 --- a/packages/pas/sources/namespace.move +++ b/packages/pas/sources/namespace.move @@ -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 = b"The upgrade cap is already set for this namespace."; +#[error(code = 1)] +const EUpgradeCapPackageMismatch: vector = + b"The upgrade cap package does not match the package."; +#[error(code = 2)] +const EUpgradeCapNotSet: vector = + 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, + /// 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().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` exists in the namespace public fun rule_exists(namespace: &Namespace): bool { derived_object::exists(&namespace.id, keys::rule_key()) @@ -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); @@ -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(), } } diff --git a/packages/pas/sources/rule.move b/packages/pas/sources/rule.move index 5e0e7f6..8c42c17 100644 --- a/packages/pas/sources/rule.move +++ b/packages/pas/sources/rule.move @@ -16,6 +16,7 @@ use sui::{ dynamic_field, vec_map::{Self, VecMap} }; +use pas::versioning::Versioning; #[error(code = 0)] const EInvalidProof: vector = @@ -41,6 +42,9 @@ public struct Rule 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. @@ -64,9 +68,13 @@ public fun new( ): Rule { assert!(!namespace.rule_exists(), ERuleAlreadyExists); + let versioning = namespace.versioning(); + versioning.assert_is_valid_version(); + let mut rule = Rule { id: derived_object::claim(namespace.uid_mut(), keys::rule_key()), auth_witness: type_name::with_defining_ids(), + versioning, }; dynamic_field::add<_, VecMap>( @@ -90,6 +98,7 @@ public fun enable_funds_management( 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); } @@ -101,6 +110,7 @@ public fun resolve_unlock_funds( ): Balance { rule.assert_is_valid_issuer_proof!<_, U>(); rule.assert_is_fund_management_enabled!(); + rule.versioning.assert_is_valid_version(); request.resolve() } @@ -114,6 +124,7 @@ public fun resolve_transfer_funds( 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(); } @@ -129,6 +140,7 @@ public fun clawback_funds( ): Balance { assert!(rule.is_fund_clawback_allowed(), EClawbackNotAllowed); rule.assert_is_valid_issuer_proof!<_, U>(); + rule.versioning.assert_is_valid_version(); from.withdraw(amount) } @@ -137,6 +149,7 @@ public fun clawback_funds( /// Aborts early if the management for funds has not been enabled for `T`. public fun is_fund_clawback_allowed(rule: &Rule): bool { rule.assert_is_fund_management_enabled!(); + rule.versioning.assert_is_valid_version(); *dynamic_field::borrow(&rule.id, FundsClawbackState()) } @@ -144,6 +157,7 @@ public fun is_fund_clawback_allowed(rule: &Rule): bool { /// NOTE: If the action type already exists, it will be replaced. public fun set_action_command(rule: &mut Rule, 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(); let action_type_str = (*action_type.as_string()).to_string(); @@ -161,13 +175,19 @@ public fun set_action_command(rule: &mut Rule, 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(rule: &mut Rule, namespace: &Namespace) { + rule.versioning = namespace.versioning(); +} + /// Check if fund management is enabled for a given `T`. public(package) fun is_fund_management_enabled(rule: &Rule): bool { dynamic_field::exists_(&rule.id, FundsClawbackState()) } public fun auth_witness(rule: &Rule): TypeName { rule.auth_witness } - + macro fun assert_is_fund_management_enabled<$T>($rule: &Rule<$T>) { let rule = $rule; assert!(rule.is_fund_management_enabled(), EFundManagementNotEnabled); diff --git a/packages/pas/sources/vault.move b/packages/pas/sources/vault.move index 1839c2f..76be406 100644 --- a/packages/pas/sources/vault.move +++ b/packages/pas/sources/vault.move @@ -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}; @@ -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 @@ -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, } } @@ -66,6 +73,7 @@ public fun unlock_funds( _ctx: &mut TxContext, ): UnlockFundsRequest { 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)) } @@ -78,6 +86,7 @@ public fun transfer_funds( _ctx: &mut TxContext, ): TransferFundsRequest { auth.assert_is_valid_for_vault!(from); + from.versioning.assert_is_valid_version(); from.internal_transfer_funds(to.owner, amount) } @@ -94,6 +103,7 @@ public fun unsafe_transfer_funds( _ctx: &mut TxContext, ): TransferFundsRequest { auth.assert_is_valid_for_vault!(from); + from.versioning.assert_is_valid_version(); from.internal_transfer_funds(recipient_address, amount) } @@ -112,10 +122,17 @@ public fun owner(vault: &Vault): address { } public fun deposit_funds(vault: &Vault, balance: Balance) { + 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(vault: &mut Vault, amount: u64): Balance { + vault.versioning.assert_is_valid_version(); balance::redeem_funds(vault.id.withdraw_funds_from_object(amount)) } diff --git a/packages/pas/sources/versioning.move b/packages/pas/sources/versioning.move new file mode 100644 index 0000000..a627b70 --- /dev/null +++ b/packages/pas/sources/versioning.move @@ -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 = + 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, +} + +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 } diff --git a/packages/pas/tests/e2e.move b/packages/pas/tests/e2e.move index a5120d0..2a1cb84 100644 --- a/packages/pas/tests/e2e.move +++ b/packages/pas/tests/e2e.move @@ -1,7 +1,8 @@ #[test_only, allow(unused_variable, unused_mut_ref, dead_code)] module pas::e2e; -use pas::{rule, vault::{Self, Vault}}; +use pas::{namespace::{Self, Namespace}, rule, vault::{Self, Vault}}; +use ptb::ptb::Command; use std::unit_test::{assert_eq, destroy}; use sui::{balance::{Self, send_funds}, sui::SUI, test_scenario::return_shared}; @@ -404,6 +405,70 @@ fun try_to_create_duplicate_rule() { }); } +#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapAlreadySet)] +fun tries_to_setup_namespace_twice() { + let mut scenario = sui::test_scenario::begin(@0x0); + namespace::init_for_testing(scenario.ctx()); + scenario.next_tx(@0x0); + + let mut namespace = scenario.take_shared(); + + let package_id = package_id(); + + let upgrade_cap = sui::package::test_publish(package_id, scenario.ctx()); + namespace.setup(&upgrade_cap); + namespace.setup(&upgrade_cap); + + abort +} + +#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] +fun tries_to_setup_namespace_with_invalid_upgrade_cap() { + let mut scenario = sui::test_scenario::begin(@0x0); + namespace::init_for_testing(scenario.ctx()); + scenario.next_tx(@0x0); + + let mut namespace = scenario.take_shared(); + + // create the upgrade cap from a type coming from a dependency. + let package_id = package_id(); + + let upgrade_cap = sui::package::test_publish(package_id, scenario.ctx()); + namespace.setup(&upgrade_cap); + + abort +} + +#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] +fun tries_to_block_version_with_invalid_upgrade_cap() { + test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { + scenario.next_tx(@0x1); + + let upgrade_cap = sui::package::test_publish(package_id(), scenario.ctx()); + namespace.block_version(&upgrade_cap, 1); + + abort + }); +} + +#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] +fun tries_to_unblock_version_with_invalid_upgrade_cap() { + test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { + scenario.next_tx(@0x1); + + let upgrade_cap = sui::package::test_publish(package_id(), scenario.ctx()); + namespace.unblock_version(&upgrade_cap, 1); + + abort + }); +} + +fun package_id(): ID { + sui::address::from_ascii_bytes(std::type_name::with_defining_ids() + .address_string() + .as_bytes()).to_id() +} + /// A test_tx already set up for convenience. public macro fun test_tx( $admin: address, @@ -422,6 +487,12 @@ public macro fun test_tx( let mut namespace = scenario.take_shared(); + let package_id = package_id(); + + let upgrade_cap = sui::package::test_publish(package_id, scenario.ctx()); + namespace.setup(&upgrade_cap); + std::unit_test::destroy(upgrade_cap); + let mut rule_a = pas::rule::new(&mut namespace, internal::permit(), AWitness()); let mut cap_a = sui::coin::create_treasury_cap_for_testing(scenario.ctx()); From af79893a4500ebc95d3ad8aaff3dde6224cb782a Mon Sep 17 00:00:00 2001 From: Manos Liolios Date: Wed, 11 Feb 2026 22:22:52 +0200 Subject: [PATCH 2/2] more tests on versioning revision --- .../requests/unlock_funds_request.move | 3 +- packages/pas/sources/rule.move | 13 +- packages/pas/sources/vault.move | 4 + packages/pas/tests/e2e.move | 77 +++-------- packages/pas/tests/versioning_tests.move | 129 ++++++++++++++++++ 5 files changed, 159 insertions(+), 67 deletions(-) create mode 100644 packages/pas/tests/versioning_tests.move diff --git a/packages/pas/sources/requests/unlock_funds_request.move b/packages/pas/sources/requests/unlock_funds_request.move index b83eccb..3835788 100644 --- a/packages/pas/sources/requests/unlock_funds_request.move +++ b/packages/pas/sources/requests/unlock_funds_request.move @@ -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)] @@ -40,6 +40,7 @@ public fun resolve_unrestricted( namespace: &Namespace, ): Balance { assert!(!namespace.rule_exists(), ECannotResolveManagedAssets); + namespace.versioning().assert_is_valid_version(); request.resolve() } diff --git a/packages/pas/sources/rule.move b/packages/pas/sources/rule.move index 8c42c17..c86f80b 100644 --- a/packages/pas/sources/rule.move +++ b/packages/pas/sources/rule.move @@ -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}}; @@ -16,7 +17,6 @@ use sui::{ dynamic_field, vec_map::{Self, VecMap} }; -use pas::versioning::Versioning; #[error(code = 0)] const EInvalidProof: vector = @@ -42,7 +42,6 @@ public struct Rule 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, } @@ -176,18 +175,20 @@ public fun set_action_command(rule: &mut Rule, command: Comman } /// Allows syncing the versioning of a rule to the namespace's versioning. -/// This is permission-less and can be done +/// This is permission-less and can be done public fun sync_versioning(rule: &mut Rule, namespace: &Namespace) { rule.versioning = namespace.versioning(); } +public fun auth_witness(rule: &Rule): TypeName { rule.auth_witness } + /// Check if fund management is enabled for a given `T`. public(package) fun is_fund_management_enabled(rule: &Rule): bool { dynamic_field::exists_(&rule.id, FundsClawbackState()) } -public fun auth_witness(rule: &Rule): TypeName { rule.auth_witness } - +public(package) fun versioning(rule: &Rule): Versioning { rule.versioning } + macro fun assert_is_fund_management_enabled<$T>($rule: &Rule<$T>) { let rule = $rule; assert!(rule.is_fund_management_enabled(), EFundManagementNotEnabled); diff --git a/packages/pas/sources/vault.move b/packages/pas/sources/vault.move index 76be406..bc82525 100644 --- a/packages/pas/sources/vault.move +++ b/packages/pas/sources/vault.move @@ -136,6 +136,10 @@ public(package) fun withdraw(vault: &mut Vault, amount: u64): Balance { 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; diff --git a/packages/pas/tests/e2e.move b/packages/pas/tests/e2e.move index 2a1cb84..f18ecea 100644 --- a/packages/pas/tests/e2e.move +++ b/packages/pas/tests/e2e.move @@ -1,8 +1,7 @@ #[test_only, allow(unused_variable, unused_mut_ref, dead_code)] module pas::e2e; -use pas::{namespace::{Self, Namespace}, rule, vault::{Self, Vault}}; -use ptb::ptb::Command; +use pas::{rule, vault::{Self, Vault}}; use std::unit_test::{assert_eq, destroy}; use sui::{balance::{Self, send_funds}, sui::SUI, test_scenario::return_shared}; @@ -405,68 +404,26 @@ fun try_to_create_duplicate_rule() { }); } -#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapAlreadySet)] -fun tries_to_setup_namespace_twice() { - let mut scenario = sui::test_scenario::begin(@0x0); - namespace::init_for_testing(scenario.ctx()); - scenario.next_tx(@0x0); - - let mut namespace = scenario.take_shared(); - - let package_id = package_id(); - - let upgrade_cap = sui::package::test_publish(package_id, scenario.ctx()); - namespace.setup(&upgrade_cap); - namespace.setup(&upgrade_cap); - - abort +public fun package_id(): ID { + sui::address::from_ascii_bytes(std::type_name::with_defining_ids() + .address_string() + .as_bytes()).to_id() } -#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] -fun tries_to_setup_namespace_with_invalid_upgrade_cap() { - let mut scenario = sui::test_scenario::begin(@0x0); - namespace::init_for_testing(scenario.ctx()); - scenario.next_tx(@0x0); - - let mut namespace = scenario.take_shared(); - - // create the upgrade cap from a type coming from a dependency. - let package_id = package_id(); - - let upgrade_cap = sui::package::test_publish(package_id, scenario.ctx()); - namespace.setup(&upgrade_cap); - - abort +public fun a_permit(): internal::Permit { + internal::permit() } -#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] -fun tries_to_block_version_with_invalid_upgrade_cap() { - test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { - scenario.next_tx(@0x1); - - let upgrade_cap = sui::package::test_publish(package_id(), scenario.ctx()); - namespace.block_version(&upgrade_cap, 1); - - abort - }); +public fun b_permit(): internal::Permit { + internal::permit() } -#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] -fun tries_to_unblock_version_with_invalid_upgrade_cap() { - test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { - scenario.next_tx(@0x1); - - let upgrade_cap = sui::package::test_publish(package_id(), scenario.ctx()); - namespace.unblock_version(&upgrade_cap, 1); - - abort - }); +public fun a_witness(): AWitness { + AWitness() } -fun package_id(): ID { - sui::address::from_ascii_bytes(std::type_name::with_defining_ids() - .address_string() - .as_bytes()).to_id() +public fun b_witness(): BWitness { + BWitness() } /// A test_tx already set up for convenience. @@ -487,20 +444,20 @@ public macro fun test_tx( let mut namespace = scenario.take_shared(); - let package_id = package_id(); + let package_id = pas::e2e::package_id(); let upgrade_cap = sui::package::test_publish(package_id, scenario.ctx()); namespace.setup(&upgrade_cap); - std::unit_test::destroy(upgrade_cap); + sui::transfer::public_transfer(upgrade_cap, $admin); - let mut rule_a = pas::rule::new(&mut namespace, internal::permit(), AWitness()); + let mut rule_a = pas::rule::new(&mut namespace, pas::e2e::a_permit(), pas::e2e::a_witness()); let mut cap_a = sui::coin::create_treasury_cap_for_testing(scenario.ctx()); rule_a.enable_funds_management(&mut cap_a, true); std::unit_test::destroy(cap_a); rule_a.share(); - let mut rule_b = pas::rule::new(&mut namespace, internal::permit(), BWitness()); + let mut rule_b = pas::rule::new(&mut namespace, pas::e2e::b_permit(), pas::e2e::b_witness()); let mut cap_b = sui::coin::create_treasury_cap_for_testing(scenario.ctx()); rule_b.enable_funds_management(&mut cap_b, false); std::unit_test::destroy(cap_b); diff --git a/packages/pas/tests/versioning_tests.move b/packages/pas/tests/versioning_tests.move new file mode 100644 index 0000000..b1e1b2c --- /dev/null +++ b/packages/pas/tests/versioning_tests.move @@ -0,0 +1,129 @@ +#[test_only, allow(unused_variable, unused_mut_ref, dead_code)] +module pas::versioning_tests; + +use pas::{ + e2e::{package_id, test_tx, A}, + namespace::{Self, Namespace}, + vault, + versioning::breaking_version +}; +use ptb::ptb::Command; +use std::unit_test::assert_eq; +use sui::{package::UpgradeCap, test_scenario::Scenario}; + +#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapAlreadySet)] +fun tries_to_setup_namespace_twice() { + let mut scenario = sui::test_scenario::begin(@0x0); + namespace::init_for_testing(scenario.ctx()); + scenario.next_tx(@0x0); + + let mut namespace = scenario.take_shared(); + + let package_id = package_id(); + + let upgrade_cap = sui::package::test_publish(package_id, scenario.ctx()); + namespace.setup(&upgrade_cap); + namespace.setup(&upgrade_cap); + + abort +} + +#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] +fun tries_to_setup_namespace_with_invalid_upgrade_cap() { + let mut scenario = sui::test_scenario::begin(@0x0); + namespace::init_for_testing(scenario.ctx()); + scenario.next_tx(@0x0); + + let mut namespace = scenario.take_shared(); + + // create the upgrade cap from a type coming from a dependency. + let package_id = package_id(); + + let upgrade_cap = sui::package::test_publish(package_id, scenario.ctx()); + namespace.setup(&upgrade_cap); + + abort +} + +#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] +fun tries_to_block_version_with_invalid_upgrade_cap() { + test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { + scenario.next_tx(@0x1); + + let upgrade_cap = sui::package::test_publish(package_id(), scenario.ctx()); + namespace.block_version(&upgrade_cap, 1); + + abort + }); +} + +#[test, expected_failure(abort_code = ::pas::namespace::EUpgradeCapPackageMismatch)] +fun tries_to_unblock_version_with_invalid_upgrade_cap() { + test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { + scenario.next_tx(@0x1); + + let upgrade_cap = sui::package::test_publish(package_id(), scenario.ctx()); + namespace.unblock_version(&upgrade_cap, 1); + + abort + }); +} + +#[test] +fun block_unblock_versions_and_sync_with_vaults_and_rules() { + test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { + scenario.next_tx(@0x1); + let upgrade_cap = scenario.take_from_sender(); + + let mut vault = vault::create(namespace, @0x1); + + namespace.block_version(&upgrade_cap, 1); + assert!(!namespace.versioning().is_valid_version(1)); + vault.sync_versioning(namespace); + managed_rule.sync_versioning(namespace); + assert_eq!(vault.versioning(), namespace.versioning()); + assert!(!vault.versioning().is_valid_version(1)); + assert!(!managed_rule.versioning().is_valid_version(1)); + + namespace.unblock_version(&upgrade_cap, 1); + vault.sync_versioning(namespace); + managed_rule.sync_versioning(namespace); + assert!(namespace.versioning().is_valid_version(1)); + assert!(vault.versioning().is_valid_version(1)); + assert!(managed_rule.versioning().is_valid_version(1)); + + vault.share(); + scenario.return_to_sender(upgrade_cap); + }); +} + +#[test, expected_failure(abort_code = ::pas::versioning::EInvalidVersion)] +fun try_to_create_vault_with_invalid_version() { + test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { + namespace.block_current_version(scenario); + + let _vault = vault::create(namespace, @0x1); + abort + }); +} + +#[test, expected_failure(abort_code = ::pas::versioning::EInvalidVersion)] +fun try_unlock_funds_invalid_version_on_vault() { + test_tx!(@0x1, |namespace, managed_rule, _unmanaged_rule, scenario| { + let mut vault = vault::create(namespace, @0x1); + + namespace.block_current_version(scenario); + vault.sync_versioning(namespace); + let auth = vault::new_auth(scenario.ctx()); + let req = vault.unlock_funds(&auth, 50, scenario.ctx()); + abort + }); +} + +use fun block_current_version as Namespace.block_current_version; + +fun block_current_version(namespace: &mut Namespace, scenario: &Scenario) { + let upgrade_cap = scenario.take_from_sender(); + namespace.block_version(&upgrade_cap, breaking_version!()); + scenario.return_to_sender(upgrade_cap); +}