From 1260c46bdaed4814df380d8a991e026bc0b2f820 Mon Sep 17 00:00:00 2001 From: Anadudev Date: Wed, 30 Sep 2026 02:41:32 +0100 Subject: [PATCH] fix(vault): drop unimplemented capability bits, add entrypoint-mapping tests, update docs (#1116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #1116. The capability bitmap previously advertised six bits for features that have no vault entrypoint today: Bit 6 CAP_OFFERING_METADATA – no entrypoint Bit 7 CAP_PRICE_REGISTRY – no entrypoint Bit 12 CAP_REVENUE_POOL – no entrypoint Bit 13 CAP_RATE_LIMIT – no entrypoint Bit 14 CAP_ADMIN_BROADCAST – no entrypoint Bit 16 CAP_SLIPPAGE_GUARD – no entrypoint Changes: * capabilities.rs - Reserved-bit constants (CAP_OFFERING_METADATA, CAP_PRICE_REGISTRY, CAP_REVENUE_POOL, CAP_RATE_LIMIT, CAP_ADMIN_BROADCAST, CAP_SLIPPAGE_GUARD) are retained as named sentinels so their bit positions can never be accidentally reused, but ALL_CAPABILITIES no longer ORs them in. - ALL_CAPABILITIES is now 0x0000_0000_0002_8F3F, covering only the 12 bits (0-5, 8-11, 15, 17) that have callable vault entrypoints. - Updated all doc-comments to reflect reserved-and-cleared semantics. * test_capabilities.rs (new, 29 tests) - Asserts the exact hex mask (EXPECTED_EXACT_MASK = 0x0000_0000_0002_8F3F). - Asserts each of the 12 supported bits IS set. - Asserts each of the 6 reserved bits IS cleared. - every_set_capability_maps_to_callable_vault_entrypoint: calls the real client method for every set bit, proving no bit is orphaned. - reserved_bits_are_zero / all_reserved_bits_are_cleared_in_all_capabilities: double-check the reserved set is always 0 in ALL_CAPABILITIES. - bit_positions_are_stable: locks down all 18 constant hex values so accidental re-numbering is caught immediately. * lib.rs - Added mod test_capabilities; declaration so the new test module is compiled and run under cargo test. * test_value_conservation.rs - Added missing Events as _ import (unused-import warning fix). * docs/CAPABILITIES.md - Updated active mask from stale value to 0x0000_0000_0002_8F3F. - Marked bits 6, 7, 12, 13, 14, 16 as reserved (cleared) in the bit-registry table and TypeScript constant block. - Added Stability guarantee section documenting the reserved-bit contract. All 29 capability tests pass (cargo test -p callora-vault capabilities). --- contracts/vault/src/capabilities.rs | 50 ++-- contracts/vault/src/lib.rs | 3 + contracts/vault/src/test_capabilities.rs | 283 ++++++++++++++---- .../vault/src/test_value_conservation.rs | 2 +- docs/CAPABILITIES.md | 43 +-- 5 files changed, 270 insertions(+), 111 deletions(-) diff --git a/contracts/vault/src/capabilities.rs b/contracts/vault/src/capabilities.rs index 9830ba91..0403431f 100644 --- a/contracts/vault/src/capabilities.rs +++ b/contracts/vault/src/capabilities.rs @@ -43,14 +43,12 @@ pub const CAP_PAUSE: u64 = 1 << 4; /// Introduced: v1.0.0 pub const CAP_AUTHORIZED_CALLER: u64 = 1 << 5; -/// Bit 6 — Offering metadata: per-offering metadata stored and queried on-chain. -/// Managed via `set_metadata()`, `update_metadata()`, `remove_metadata()`. -/// Introduced: v1.0.0 +/// Bit 6 — Reserved (formerly offering metadata; entrypoints not implemented). +/// Position is permanently reserved and cleared (always 0) in `capabilities()`. pub const CAP_OFFERING_METADATA: u64 = 1 << 6; -/// Bit 7 — Price registry: per-offering prices stored on-chain. -/// Managed via `set_price()`, `get_price()`, `remove_price()`, `list_prices()`. -/// Introduced: v1.0.0 +/// Bit 7 — Reserved (formerly price registry; entrypoints not implemented). +/// Position is permanently reserved and cleared (always 0) in `capabilities()`. pub const CAP_PRICE_REGISTRY: u64 = 1 << 7; /// Bit 8 — Request idempotency: `deduct` and `batch_deduct` accept an optional @@ -64,7 +62,7 @@ pub const CAP_REQUEST_IDEMPOTENCY: u64 = 1 << 8; pub const CAP_TWO_STEP_OWNERSHIP: u64 = 1 << 9; /// Bit 10 — Two-step admin transfer: the admin role moves via `set_admin()` / -/// `accept_admin()` / `cancel_admin_transfer()`. +/// `accept_admin()`. /// Introduced: v1.0.0 pub const CAP_TWO_STEP_ADMIN: u64 = 1 << 10; @@ -73,40 +71,38 @@ pub const CAP_TWO_STEP_ADMIN: u64 = 1 << 10; /// Introduced: v1.0.0 pub const CAP_SETTLEMENT: u64 = 1 << 11; -/// Bit 12 — Revenue pool: an optional revenue pool address is configurable via a -/// two-step `propose_revenue_pool()` / `accept_revenue_pool()` pattern. -/// Introduced: v1.0.0 +/// Bit 12 — Reserved (formerly revenue pool propose/accept; entrypoints not implemented). +/// Position is permanently reserved and cleared (always 0) in `capabilities()`. pub const CAP_REVENUE_POOL: u64 = 1 << 12; -/// Bit 13 — Developer rate limiting: per-developer token-bucket rate limits are -/// enforced on deduct operations. Configured via `set_developer_rate_limit()`. -/// Introduced: v1.0.0 +/// Bit 13 — Reserved (formerly developer rate limiting; entrypoints not implemented). +/// Position is permanently reserved and cleared (always 0) in `capabilities()`. pub const CAP_RATE_LIMIT: u64 = 1 << 13; -/// Bit 14 — Admin broadcast: admin can emit signed on-chain messages with severity -/// levels via `broadcast()`. -/// Introduced: v1.0.0 +/// Bit 14 — Reserved (formerly admin broadcast; entrypoints not implemented). +/// Position is permanently reserved and cleared (always 0) in `capabilities()`. pub const CAP_ADMIN_BROADCAST: u64 = 1 << 14; /// Bit 15 — Depositor allowlist: owner restricts deposits to approved addresses. -/// Managed via `add_address()`, `set_allowed_depositor()`, `clear_all()`, -/// `get_allowlist()`. +/// Managed via `add_address()`, `clear_all()`, `get_allowlist()`, `is_authorized_depositor()`. /// Introduced: v1.0.0 pub const CAP_DEPOSITOR_ALLOWLIST: u64 = 1 << 15; -/// Bit 16 — Slippage guard: `deduct` enforces a caller-supplied `max_fee_bps` cap -/// expressed as basis points of the current vault balance. -/// Introduced: v1.0.0 +/// Bit 16 — Reserved (formerly slippage guard on deduct; entrypoints not implemented). +/// Position is permanently reserved and cleared (always 0) in `capabilities()`. pub const CAP_SLIPPAGE_GUARD: u64 = 1 << 16; -/// Bit 17 — Contract upgrade: admin can replace the WASM via `upgrade()`. +/// Bit 17 — Contract upgrade: admin can replace the WASM via `propose_upgrade()` / +/// `execute_upgrade()`. /// Introduced: v1.0.0 pub const CAP_UPGRADE: u64 = 1 << 17; -// Bits 18–63 are reserved for future capabilities and are always zero. +// Bits 6, 7, 12, 13, 14, 16, and 18–63 are reserved and are always zero in capabilities(). /// Bitmask of all capabilities exposed by this contract version. /// +/// Reserved bits (6, 7, 12, 13, 14, 16, and 18–63) are excluded and remain 0. +/// /// Combine individual `CAP_*` constants with `&` to test for a specific feature: /// ```ignore /// assert!(caps & CAP_DEPOSIT != 0); @@ -117,24 +113,18 @@ pub const ALL_CAPABILITIES: u64 = CAP_DEPOSIT | CAP_BATCH_DEDUCT | CAP_PAUSE | CAP_AUTHORIZED_CALLER - | CAP_OFFERING_METADATA - | CAP_PRICE_REGISTRY | CAP_REQUEST_IDEMPOTENCY | CAP_TWO_STEP_OWNERSHIP | CAP_TWO_STEP_ADMIN | CAP_SETTLEMENT - | CAP_REVENUE_POOL - | CAP_RATE_LIMIT - | CAP_ADMIN_BROADCAST | CAP_DEPOSITOR_ALLOWLIST - | CAP_SLIPPAGE_GUARD | CAP_UPGRADE; /// Return the capability bitmap for this contract. /// /// Each set bit signals a supported feature. Bits are stable across upgrades — /// once assigned a bit position is never reused for a different feature. -/// Reserved bits (18–63) are always zero. +/// Reserved bits (6, 7, 12, 13, 14, 16, and 18–63) are always zero. /// /// No authentication is required; this is a pure view function. pub fn capabilities(_env: &Env) -> u64 { diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index f5f1a483..03dde92c 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -2462,6 +2462,9 @@ mod test_recovery_idempotency; #[cfg(test)] mod test_event_schema; +#[cfg(test)] +mod test_capabilities; + // #[cfg(test)] // mod test_gas_budget; // #[cfg(test)] diff --git a/contracts/vault/src/test_capabilities.rs b/contracts/vault/src/test_capabilities.rs index e5f05bcc..a7be3fa4 100644 --- a/contracts/vault/src/test_capabilities.rs +++ b/contracts/vault/src/test_capabilities.rs @@ -1,6 +1,6 @@ extern crate std; -use soroban_sdk::{testutils::Address as _, Address, Env}; +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, Symbol, Vec}; use crate::{ capabilities::{ @@ -13,167 +13,192 @@ use crate::{ CalloraVault, CalloraVaultClient, }; +/// Exact expected capability mask: bits 0..5, 8..11, 15, 17. +/// (Bits 6, 7, 12, 13, 14, 16 and 18..63 are reserved/cleared). +const EXPECTED_EXACT_MASK: u64 = 0x0000_0000_0002_8F3F; + fn create_usdc(env: &Env, admin: &Address) -> Address { let ca = env.register_stellar_asset_contract_v2(admin.clone()); ca.address() } -fn setup(env: &Env) -> CalloraVaultClient<'_> { +fn setup(env: &Env) -> (CalloraVaultClient<'_>, Address) { let owner = Address::generate(env); let vault_addr = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(env, &vault_addr); let usdc = create_usdc(env, &owner); env.mock_all_auths(); - client.init(&owner, &usdc, &Some(0), &Some(owner.clone()), &Some(1), &None, &None); - client + client.init( + &owner, + &usdc, + &Some(0), + &Some(owner.clone()), + &Some(1), + &None, + &Some(10_000), + &None, + ); + (client, owner) } // --------------------------------------------------------------------------- -// Basic return value +// Basic return value & exact mask assertions // --------------------------------------------------------------------------- #[test] fn capabilities_returns_nonzero() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities(), 0); } #[test] fn capabilities_equals_all_capabilities_constant() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_eq!(client.capabilities(), ALL_CAPABILITIES); } +#[test] +fn capabilities_equals_exact_expected_mask() { + let env = Env::default(); + let (client, _) = setup(&env); + assert_eq!(client.capabilities(), EXPECTED_EXACT_MASK); + assert_eq!(ALL_CAPABILITIES, EXPECTED_EXACT_MASK); +} + // --------------------------------------------------------------------------- -// Each individual capability bit is set +// Each supported individual capability bit is set // --------------------------------------------------------------------------- #[test] fn cap_deposit_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_DEPOSIT, 0); } #[test] fn cap_withdraw_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_WITHDRAW, 0); } #[test] fn cap_deduct_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_DEDUCT, 0); } #[test] fn cap_batch_deduct_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_BATCH_DEDUCT, 0); } #[test] fn cap_pause_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_PAUSE, 0); } #[test] fn cap_authorized_caller_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_AUTHORIZED_CALLER, 0); } -#[test] -fn cap_offering_metadata_is_set() { - let env = Env::default(); - let client = setup(&env); - assert_ne!(client.capabilities() & CAP_OFFERING_METADATA, 0); -} - -#[test] -fn cap_price_registry_is_set() { - let env = Env::default(); - let client = setup(&env); - assert_ne!(client.capabilities() & CAP_PRICE_REGISTRY, 0); -} - #[test] fn cap_request_idempotency_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_REQUEST_IDEMPOTENCY, 0); } #[test] fn cap_two_step_ownership_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_TWO_STEP_OWNERSHIP, 0); } #[test] fn cap_two_step_admin_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_TWO_STEP_ADMIN, 0); } #[test] fn cap_settlement_is_set() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_ne!(client.capabilities() & CAP_SETTLEMENT, 0); } #[test] -fn cap_revenue_pool_is_set() { +fn cap_depositor_allowlist_is_set() { let env = Env::default(); - let client = setup(&env); - assert_ne!(client.capabilities() & CAP_REVENUE_POOL, 0); + let (client, _) = setup(&env); + assert_ne!(client.capabilities() & CAP_DEPOSITOR_ALLOWLIST, 0); } #[test] -fn cap_rate_limit_is_set() { +fn cap_upgrade_is_set() { let env = Env::default(); - let client = setup(&env); - assert_ne!(client.capabilities() & CAP_RATE_LIMIT, 0); + let (client, _) = setup(&env); + assert_ne!(client.capabilities() & CAP_UPGRADE, 0); } +// --------------------------------------------------------------------------- +// Unimplemented feature bits are cleared (0) and reserved +// --------------------------------------------------------------------------- + #[test] -fn cap_admin_broadcast_is_set() { +fn cap_offering_metadata_is_cleared() { let env = Env::default(); - let client = setup(&env); - assert_ne!(client.capabilities() & CAP_ADMIN_BROADCAST, 0); + let (client, _) = setup(&env); + assert_eq!(client.capabilities() & CAP_OFFERING_METADATA, 0); } #[test] -fn cap_depositor_allowlist_is_set() { +fn cap_price_registry_is_cleared() { let env = Env::default(); - let client = setup(&env); - assert_ne!(client.capabilities() & CAP_DEPOSITOR_ALLOWLIST, 0); + let (client, _) = setup(&env); + assert_eq!(client.capabilities() & CAP_PRICE_REGISTRY, 0); } #[test] -fn cap_slippage_guard_is_set() { +fn cap_revenue_pool_is_cleared() { let env = Env::default(); - let client = setup(&env); - assert_ne!(client.capabilities() & CAP_SLIPPAGE_GUARD, 0); + let (client, _) = setup(&env); + assert_eq!(client.capabilities() & CAP_REVENUE_POOL, 0); } #[test] -fn cap_upgrade_is_set() { +fn cap_rate_limit_is_cleared() { let env = Env::default(); - let client = setup(&env); - assert_ne!(client.capabilities() & CAP_UPGRADE, 0); + let (client, _) = setup(&env); + assert_eq!(client.capabilities() & CAP_RATE_LIMIT, 0); +} + +#[test] +fn cap_admin_broadcast_is_cleared() { + let env = Env::default(); + let (client, _) = setup(&env); + assert_eq!(client.capabilities() & CAP_ADMIN_BROADCAST, 0); +} + +#[test] +fn cap_slippage_guard_is_cleared() { + let env = Env::default(); + let (client, _) = setup(&env); + assert_eq!(client.capabilities() & CAP_SLIPPAGE_GUARD, 0); } // --------------------------------------------------------------------------- @@ -183,10 +208,13 @@ fn cap_upgrade_is_set() { #[test] fn reserved_bits_are_zero() { let env = Env::default(); - let client = setup(&env); - // Bits 18–63 must always be zero. - let reserved_mask: u64 = !((1u64 << 18) - 1); + let (client, _) = setup(&env); + let reserved_mask: u64 = !ALL_CAPABILITIES; assert_eq!(client.capabilities() & reserved_mask, 0); + + // Specifically verify all bits 18–63 are zero. + let upper_reserved_mask: u64 = !((1u64 << 18) - 1); + assert_eq!(client.capabilities() & upper_reserved_mask, 0); } // --------------------------------------------------------------------------- @@ -216,13 +244,13 @@ fn bit_positions_are_stable() { } // --------------------------------------------------------------------------- -// Edge cases +// Edge cases & mask decomposition // --------------------------------------------------------------------------- #[test] fn capabilities_is_idempotent() { let env = Env::default(); - let client = setup(&env); + let (client, _) = setup(&env); assert_eq!(client.capabilities(), client.capabilities()); } @@ -237,16 +265,50 @@ fn capabilities_available_before_init() { } #[test] -fn all_capabilities_constant_has_no_gaps() { - // Every bit from 0 through 17 must be present in ALL_CAPABILITIES. - for bit in 0u64..18 { - let mask = 1u64 << bit; +fn all_reserved_bits_are_cleared_in_all_capabilities() { + let reserved_bits: &[u64] = &[ + CAP_OFFERING_METADATA, + CAP_PRICE_REGISTRY, + CAP_REVENUE_POOL, + CAP_RATE_LIMIT, + CAP_ADMIN_BROADCAST, + CAP_SLIPPAGE_GUARD, + ]; + for &bit in reserved_bits { + assert_eq!( + ALL_CAPABILITIES & bit, + 0, + "reserved bit {bit:#x} must be cleared in ALL_CAPABILITIES" + ); + } +} + +#[test] +fn all_supported_bits_match_all_capabilities_decomposition() { + let supported_bits: &[u64] = &[ + CAP_DEPOSIT, + CAP_WITHDRAW, + CAP_DEDUCT, + CAP_BATCH_DEDUCT, + CAP_PAUSE, + CAP_AUTHORIZED_CALLER, + CAP_REQUEST_IDEMPOTENCY, + CAP_TWO_STEP_OWNERSHIP, + CAP_TWO_STEP_ADMIN, + CAP_SETTLEMENT, + CAP_DEPOSITOR_ALLOWLIST, + CAP_UPGRADE, + ]; + let mut expected_mask = 0u64; + for &bit in supported_bits { assert_ne!( - ALL_CAPABILITIES & mask, + ALL_CAPABILITIES & bit, 0, - "bit {bit} is missing from ALL_CAPABILITIES" + "supported bit {bit:#x} must be set in ALL_CAPABILITIES" ); + expected_mask |= bit; } + assert_eq!(ALL_CAPABILITIES, expected_mask); } #[test] @@ -279,3 +341,102 @@ fn all_capabilities_bits_are_power_of_two_distinct() { seen |= cap; } } + +// --------------------------------------------------------------------------- +// Mapping: every set capability bit corresponds to a callable client entrypoint +// --------------------------------------------------------------------------- + +#[test] +fn every_set_capability_maps_to_callable_vault_entrypoint() { + let env = Env::default(); + let (client, owner) = setup(&env); + let caps = client.capabilities(); + + // 1. CAP_DEPOSIT (bit 0) -> deposit + assert_ne!(caps & CAP_DEPOSIT, 0); + let _ = client.try_deposit(&owner, &100); + + // 2. CAP_WITHDRAW (bit 1) -> withdraw, withdraw_to + assert_ne!(caps & CAP_WITHDRAW, 0); + let _ = client.try_withdraw(&0); + let _ = client.try_withdraw_to(&owner, &0); + + // 3. CAP_DEDUCT (bit 2) -> deduct, simulate_deduct + assert_ne!(caps & CAP_DEDUCT, 0); + let _ = client.try_deduct(&owner, &0, &1); + + // 4. CAP_BATCH_DEDUCT (bit 3) -> batch_deduct, simulate_batch_deduct + assert_ne!(caps & CAP_BATCH_DEDUCT, 0); + let _ = client.try_batch_deduct(&owner, &Vec::new(&env)); + + // 5. CAP_PAUSE (bit 4) -> pause, unpause, is_paused + assert_ne!(caps & CAP_PAUSE, 0); + assert!(!client.is_paused()); + client.pause(&owner); + assert!(client.is_paused()); + client.unpause(&owner); + assert!(!client.is_paused()); + + // 6. CAP_AUTHORIZED_CALLER (bit 5) -> set_authorized_caller + assert_ne!(caps & CAP_AUTHORIZED_CALLER, 0); + let caller = Address::generate(&env); + client.set_authorized_caller(&Some(caller.clone()), &0); + + // 7. CAP_REQUEST_IDEMPOTENCY (bit 8) -> is_request_processed, prune_processed_requests + assert_ne!(caps & CAP_REQUEST_IDEMPOTENCY, 0); + let req_id = Symbol::new(&env, "req_1"); + assert!(!client.is_request_processed(&req_id)); + let _ = client.prune_processed_requests(&owner, &Vec::new(&env)); + + // 8. CAP_TWO_STEP_OWNERSHIP (bit 9) -> transfer_ownership, accept_ownership, get_owner + assert_ne!(caps & CAP_TWO_STEP_OWNERSHIP, 0); + assert_eq!(client.get_owner(), owner); + let new_owner = Address::generate(&env); + client.transfer_ownership(&owner, &new_owner); + client.accept_ownership(); + assert_eq!(client.get_owner(), new_owner); + + // 9. CAP_TWO_STEP_ADMIN (bit 10) -> set_admin, accept_admin, get_admin + // The vault was initialized with `owner` as admin; after ownership transfer + // `owner` is no longer the owner but is still the admin. set_admin + // requires the *current admin* as caller. + assert_ne!(caps & CAP_TWO_STEP_ADMIN, 0); + let new_admin = Address::generate(&env); + client.set_admin(&owner, &new_admin); // owner == current admin + client.accept_admin(); + assert_eq!(client.get_admin(), new_admin); + + // 10. CAP_SETTLEMENT (bit 11) -> set_settlement, get_settlement + assert_ne!(caps & CAP_SETTLEMENT, 0); + let settlement = Address::generate(&env); + client.set_settlement(&new_owner, &settlement); + assert_eq!(client.get_settlement(), settlement); + + // 11. CAP_DEPOSITOR_ALLOWLIST (bit 15) -> add_address, clear_all, get_allowlist, is_authorized_depositor + // NOTE: add_address / get_allowlist / clear_all store a Vec
under + // StorageKey::AllowedDepositors, whereas is_authorized_depositor reads a + // per-address bool under DataKey::Depositor — they use different storage + // keys and do not interact with each other. We verify each entrypoint is + // callable and produces the correct observable effect for its own storage. + assert_ne!(caps & CAP_DEPOSITOR_ALLOWLIST, 0); + let depositor = Address::generate(&env); + // is_authorized_depositor: callable entrypoint — returns false for unknown addr. + assert!(!client.is_authorized_depositor(&depositor)); + // add_address: entrypoint callable — depositor appears in get_allowlist(). + client.add_address(&new_owner, &depositor); + let list = client.get_allowlist(); + assert!(list.contains(depositor.clone()), "depositor must be in allowlist after add_address"); + // clear_all: entrypoint callable — allowlist is empty afterward. + client.clear_all(&new_owner); + let list_after = client.get_allowlist(); + assert!(list_after.is_empty(), "allowlist must be empty after clear_all"); + + // 12. CAP_UPGRADE (bit 17) -> propose_upgrade, execute_upgrade, cancel_upgrade, get_pending_upgrade + assert_ne!(caps & CAP_UPGRADE, 0); + assert_eq!(client.get_pending_upgrade(), None); + let wasm_hash = BytesN::from_array(&env, &[1u8; 32]); + client.propose_upgrade(&new_admin, &wasm_hash); + assert!(client.get_pending_upgrade().is_some()); + client.cancel_upgrade(&new_admin); + assert_eq!(client.get_pending_upgrade(), None); +} diff --git a/contracts/vault/src/test_value_conservation.rs b/contracts/vault/src/test_value_conservation.rs index d4eb4287..700329f6 100644 --- a/contracts/vault/src/test_value_conservation.rs +++ b/contracts/vault/src/test_value_conservation.rs @@ -22,7 +22,7 @@ extern crate std; -use soroban_sdk::testutils::{Address as _, Ledger as _}; +use soroban_sdk::testutils::{Address as _, Events as _, Ledger as _}; use soroban_sdk::{token, Address, Env, Error, InvokeError, Vec}; use super::*; diff --git a/docs/CAPABILITIES.md b/docs/CAPABILITIES.md index 5f547f30..d2c50495 100644 --- a/docs/CAPABILITIES.md +++ b/docs/CAPABILITIES.md @@ -5,6 +5,8 @@ Each set bit indicates a feature that the current contract version supports. Clients can use bit operations to detect available capabilities without complex version-string parsing. +The active capability mask for this contract version is `0x0000_0000_0002_8F3F` (`0x28F3F`). + ## Querying capabilities ```typescript @@ -19,31 +21,31 @@ if (caps & CAP_BATCH_DEDUCT) { ```rust // Inside another Soroban contract let caps: u64 = VaultClient::new(&env, &vault).capabilities(); -let has_rate_limit = caps & CAP_RATE_LIMIT != 0; +let has_batch_deduct = caps & CAP_BATCH_DEDUCT != 0; ``` ## Bit registry -| Bit | Hex value | Constant | Feature | Introduced | -|-----|-----------|----------|---------|-----------| +| Bit | Hex value | Constant | Feature | Status / Introduced | +|-----|-----------|----------|---------|---------------------| | 0 | `0x00001` | `CAP_DEPOSIT` | `deposit()` — accept USDC from allowlisted callers | v1.0.0 | | 1 | `0x00002` | `CAP_WITHDRAW` | `withdraw()` / `withdraw_to()` — owner-initiated withdrawal | v1.0.0 | | 2 | `0x00004` | `CAP_DEDUCT` | `deduct()` — authorized caller deducts to settlement | v1.0.0 | | 3 | `0x00008` | `CAP_BATCH_DEDUCT` | `batch_deduct()` — atomic multi-item deduct | v1.0.0 | | 4 | `0x00010` | `CAP_PAUSE` | `pause()` / `unpause()` — circuit-breaker (admin or owner) | v1.0.0 | | 5 | `0x00020` | `CAP_AUTHORIZED_CALLER` | `set_authorized_caller()` — delegate deduct permission | v1.0.0 | -| 6 | `0x00040` | `CAP_OFFERING_METADATA` | `set_metadata()` / `update_metadata()` / `remove_metadata()` | v1.0.0 | -| 7 | `0x00080` | `CAP_PRICE_REGISTRY` | `set_price()` / `get_price()` / `list_prices()` / `remove_price()` | v1.0.0 | +| 6 | `0x00040` | `CAP_OFFERING_METADATA` | *(reserved)* — previously offering metadata; no entrypoint implemented | reserved (cleared) | +| 7 | `0x00080` | `CAP_PRICE_REGISTRY` | *(reserved)* — previously price registry; no entrypoint implemented | reserved (cleared) | | 8 | `0x00100` | `CAP_REQUEST_IDEMPOTENCY` | Optional `request_id` on `deduct` / `batch_deduct` for at-least-once retry | v1.0.0 | | 9 | `0x00200` | `CAP_TWO_STEP_OWNERSHIP` | `transfer_ownership()` / `accept_ownership()` | v1.0.0 | -| 10 | `0x00400` | `CAP_TWO_STEP_ADMIN` | `set_admin()` / `accept_admin()` / `cancel_admin_transfer()` | v1.0.0 | +| 10 | `0x00400` | `CAP_TWO_STEP_ADMIN` | `set_admin()` / `accept_admin()` | v1.0.0 | | 11 | `0x00800` | `CAP_SETTLEMENT` | Settlement contract integration via `set_settlement()` | v1.0.0 | -| 12 | `0x01000` | `CAP_REVENUE_POOL` | `propose_revenue_pool()` / `accept_revenue_pool()` / `cancel_revenue_pool()` | v1.0.0 | -| 13 | `0x02000` | `CAP_RATE_LIMIT` | Per-developer token-bucket rate limits via `set_developer_rate_limit()` | v1.0.0 | -| 14 | `0x04000` | `CAP_ADMIN_BROADCAST` | `broadcast()` — admin-signed on-chain messages with severity | v1.0.0 | -| 15 | `0x08000` | `CAP_DEPOSITOR_ALLOWLIST` | `add_address()` / `set_allowed_depositor()` / `clear_all()` | v1.0.0 | -| 16 | `0x10000` | `CAP_SLIPPAGE_GUARD` | `max_fee_bps` parameter on `deduct()` | v1.0.0 | -| 17 | `0x20000` | `CAP_UPGRADE` | `upgrade()` — admin-gated WASM replacement | v1.0.0 | +| 12 | `0x01000` | `CAP_REVENUE_POOL` | *(reserved)* — previously revenue pool propose/accept; no entrypoint implemented | reserved (cleared) | +| 13 | `0x02000` | `CAP_RATE_LIMIT` | *(reserved)* — previously developer rate limits; no entrypoint implemented | reserved (cleared) | +| 14 | `0x04000` | `CAP_ADMIN_BROADCAST` | *(reserved)* — previously admin broadcast; no entrypoint implemented | reserved (cleared) | +| 15 | `0x08000` | `CAP_DEPOSITOR_ALLOWLIST` | `add_address()` / `clear_all()` / `get_allowlist()` | v1.0.0 | +| 16 | `0x10000` | `CAP_SLIPPAGE_GUARD` | *(reserved)* — previously slippage guard; no entrypoint implemented | reserved (cleared) | +| 17 | `0x20000` | `CAP_UPGRADE` | `propose_upgrade()` / `execute_upgrade()` — admin-gated WASM replacement | v1.0.0 | | 18–63 | — | *(reserved)* | Always zero; reserved for future capabilities | — | ## Stability guarantee @@ -51,7 +53,7 @@ let has_rate_limit = caps & CAP_RATE_LIMIT != 0; - A bit position is assigned once and never reused for a different feature. - Removed features keep their bit **cleared** in future versions; the position stays reserved. - New features always occupy the lowest available bit index. -- Reserved bits (18–63) are always `0` in the current version. +- Reserved bits (6, 7, 12, 13, 14, 16, and 18–63) are always `0` in the current version. ## Integration checklist @@ -69,16 +71,19 @@ export const CAP_DEDUCT = 0x00004n; export const CAP_BATCH_DEDUCT = 0x00008n; export const CAP_PAUSE = 0x00010n; export const CAP_AUTHORIZED_CALLER = 0x00020n; -export const CAP_OFFERING_METADATA = 0x00040n; -export const CAP_PRICE_REGISTRY = 0x00080n; +export const CAP_OFFERING_METADATA = 0x00040n; // Reserved (cleared) +export const CAP_PRICE_REGISTRY = 0x00080n; // Reserved (cleared) export const CAP_REQUEST_IDEMPOTENCY= 0x00100n; export const CAP_TWO_STEP_OWNERSHIP = 0x00200n; export const CAP_TWO_STEP_ADMIN = 0x00400n; export const CAP_SETTLEMENT = 0x00800n; -export const CAP_REVENUE_POOL = 0x01000n; -export const CAP_RATE_LIMIT = 0x02000n; -export const CAP_ADMIN_BROADCAST = 0x04000n; +export const CAP_REVENUE_POOL = 0x01000n; // Reserved (cleared) +export const CAP_RATE_LIMIT = 0x02000n; // Reserved (cleared) +export const CAP_ADMIN_BROADCAST = 0x04000n; // Reserved (cleared) export const CAP_DEPOSITOR_ALLOWLIST= 0x08000n; -export const CAP_SLIPPAGE_GUARD = 0x10000n; +export const CAP_SLIPPAGE_GUARD = 0x10000n; // Reserved (cleared) export const CAP_UPGRADE = 0x20000n; + +// Exact mask of all supported capabilities in this version: +export const ALL_CAPABILITIES = 0x28F3Fn; ```