From d35acf2130b912114577e491dbfe0add65610e1c Mon Sep 17 00:00:00 2001 From: Dreamland1 Date: Wed, 29 Jul 2026 12:17:14 +0100 Subject: [PATCH 1/3] feat(admin): extract storage module with Role enum --- contracts/admin/src/lib.rs | 60 ++-------------------------------- contracts/admin/src/storage.rs | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 57 deletions(-) create mode 100644 contracts/admin/src/storage.rs diff --git a/contracts/admin/src/lib.rs b/contracts/admin/src/lib.rs index b008713e..2ca947e4 100644 --- a/contracts/admin/src/lib.rs +++ b/contracts/admin/src/lib.rs @@ -127,6 +127,9 @@ #![no_std] mod events; +pub mod storage; + +pub use storage::*; use bc_forge_ttl as ttl; use soroban_sdk::{contracterror, contracttype, vec, Address, Env, String, Vec}; @@ -151,64 +154,7 @@ pub enum AdminError { AlreadyInitialized = 6, } -/// Storage keys for the access-control layer. -/// -/// `#[contracttype]` derives a distinct ledger key for every variant (and, -/// for `Role(Role, Address)`, for every `(Role, Address)` pair), so entries -/// never collide with each other or with the other variants below. -#[derive(Clone)] -#[contracttype] -pub enum AdminKey { - /// The singular contract admin address, set via `set_admin`. - Admin, - /// Maps a `(Role, Address)` pair to `true` when `address` holds `role`. - /// This is the Role-to-Address mapping storage structure: membership is - /// looked up directly by key rather than by scanning a list, and each - /// pair occupies its own ledger entry so grants/revokes for one address - /// never touch another's. - Role(Role, Address), - /// Multi-sig admin pool addresses, set via `set_admin_pool`. - AdminPool, - /// Multi-sig approval threshold, set alongside the pool. - Threshold, - /// Governance proposal data, keyed by proposal ID. - Proposal(u64), - /// Auto-incrementing counter for proposal IDs. - ProposalIdCounter, - /// Super-admin mapping populated by `migrate_admin` for legacy contracts. - SuperAdmin(Address), -} -/// Roles recognized by the access-control layer. -/// -/// New variants must be appended, never inserted, so that previously -/// persisted `AdminKey::Role(Role, Address)` entries keep decoding to the -/// same variant they were written with. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[contracttype] -pub enum Role { - /// Full administrative control granted via `set_admin`. - Admin, - /// Permission to mint new tokens. - Minter, - /// Highest-privilege role, reserved for owner-level operations. - SuperAdmin, - /// Role allowing emergency pause and unpause operations. - Pauser, -} - -/// The SuperAdmin role constant — can be imported as `SUPER_ADMIN_ROLE` for -/// use in access-control gating without qualifying the full `Role` enum. -pub const SUPER_ADMIN_ROLE: Role = Role::SuperAdmin; - -#[derive(Clone, Debug, PartialEq)] -#[contracttype] -pub struct Proposal { - pub creator: Address, - pub description: String, - pub approvals: Vec
, - pub executed: bool, -} /// Strkey of the well-known Stellar "null" account: an ed25519 public key /// whose 32-byte payload is all zeros. No private key can ever produce a diff --git a/contracts/admin/src/storage.rs b/contracts/admin/src/storage.rs new file mode 100644 index 00000000..5cf584ef --- /dev/null +++ b/contracts/admin/src/storage.rs @@ -0,0 +1,60 @@ +use soroban_sdk::{contracttype, Address, String, Vec}; + +/// Storage keys for the access-control layer. +/// +/// `#[contracttype]` derives a distinct ledger key for every variant (and, +/// for `Role(Role, Address)`, for every `(Role, Address)` pair), so entries +/// never collide with each other or with the other variants below. +#[derive(Clone)] +#[contracttype] +pub enum AdminKey { + /// The singular contract admin address, set via `set_admin`. + Admin, + /// Maps a `(Role, Address)` pair to `true` when `address` holds `role`. + /// This is the Role-to-Address mapping storage structure: membership is + /// looked up directly by key rather than by scanning a list, and each + /// pair occupies its own ledger entry so grants/revokes for one address + /// never touch another's. + Role(Role, Address), + /// Multi-sig admin pool addresses, set via `set_admin_pool`. + AdminPool, + /// Multi-sig approval threshold, set alongside the pool. + Threshold, + /// Governance proposal data, keyed by proposal ID. + Proposal(u64), + /// Auto-incrementing counter for proposal IDs. + ProposalIdCounter, + /// Super-admin mapping populated by `migrate_admin` for legacy contracts. + SuperAdmin(Address), +} + +/// Roles recognized by the access-control layer. +/// +/// New variants must be appended, never inserted, so that previously +/// persisted `AdminKey::Role(Role, Address)` entries keep decoding to the +/// same variant they were written with. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[contracttype] +pub enum Role { + /// Full administrative control granted via `set_admin`. + Admin, + /// Permission to mint new tokens. + Minter, + /// Highest-privilege role, reserved for owner-level operations. + SuperAdmin, + /// Role allowing emergency pause and unpause operations. + Pauser, +} + +/// The SuperAdmin role constant — can be imported as `SUPER_ADMIN_ROLE` for +/// use in access-control gating without qualifying the full `Role` enum. +pub const SUPER_ADMIN_ROLE: Role = Role::SuperAdmin; + +#[derive(Clone, Debug, PartialEq)] +#[contracttype] +pub struct Proposal { + pub creator: Address, + pub description: String, + pub approvals: Vec
, + pub executed: bool, +} From 58816b48222806a4a9b1c158598697ee58060e57 Mon Sep 17 00:00:00 2001 From: Dreamland1 Date: Wed, 29 Jul 2026 12:43:29 +0100 Subject: [PATCH 2/3] Define error RoleAlreadyGranted --- contracts/admin/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/admin/src/lib.rs b/contracts/admin/src/lib.rs index 2ca947e4..a490886b 100644 --- a/contracts/admin/src/lib.rs +++ b/contracts/admin/src/lib.rs @@ -152,6 +152,8 @@ pub enum AdminError { /// The contract has already been initialized; calling `init_storage` again /// is not allowed. AlreadyInitialized = 6, + /// An operation was attempted to grant a role that the address already holds. + RoleAlreadyGranted = 7, } @@ -279,6 +281,9 @@ pub fn grant_role(env: &Env, caller: &Address, role: Role, address: &Address) { fn _grant_role(env: &Env, admin: &Address, role: Role, address: &Address) { require_non_zero_address(env, address); + if has_role(env, role, address) { + soroban_sdk::panic_with_error!(env, AdminError::RoleAlreadyGranted); + } env.storage() .persistent() .set(&AdminKey::Role(role, address.clone()), &true); From 5f82928c56c8fe35c799de2fccc5de18dadbb8f4 Mon Sep 17 00:00:00 2001 From: Dreamland1 Date: Wed, 29 Jul 2026 13:00:43 +0100 Subject: [PATCH 3/3] docs: standardize error naming conventions and add NatSpec docstrings --- contracts/admin/src/lib.rs | 2 -- contracts/split/src/lib.rs | 6 ++++++ contracts/token/src/lib.rs | 11 +++++++++++ contracts/vesting/src/lib.rs | 9 +++++++++ contracts/wrapper/src/lib.rs | 7 +++++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/contracts/admin/src/lib.rs b/contracts/admin/src/lib.rs index a490886b..9438d3dc 100644 --- a/contracts/admin/src/lib.rs +++ b/contracts/admin/src/lib.rs @@ -156,8 +156,6 @@ pub enum AdminError { RoleAlreadyGranted = 7, } - - /// Strkey of the well-known Stellar "null" account: an ed25519 public key /// whose 32-byte payload is all zeros. No private key can ever produce a /// signature for it, so it is used as the canonical zero-address sentinel diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index 03e89e43..d728f8c4 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -45,14 +45,20 @@ pub struct Invoice { pub created_at: u32, } +/// Errors returned by the split contract. #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[contracterror] #[repr(u32)] pub enum SplitError { + /// The specified invoice was not found. InvoiceNotFound = 1, + /// The recipient address is invalid. InvalidRecipient = 2, + /// Insufficient balance to perform the operation. InsufficientBalance = 3, + /// The invoice has already been completed. InvoiceAlreadyCompleted = 4, + /// The specified failed payout was not found. FailedPayoutNotFound = 5, } diff --git a/contracts/token/src/lib.rs b/contracts/token/src/lib.rs index 82a79d3b..ab0adc73 100644 --- a/contracts/token/src/lib.rs +++ b/contracts/token/src/lib.rs @@ -79,19 +79,30 @@ struct AllowanceData { expiration_ledger: u32, } +/// Errors returned by the token contract. #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[contracterror] #[repr(u32)] pub enum TokenError { + /// The contract has already been initialized. AlreadyInitialized = 1, + /// The contract has not been initialized. NotInitialized = 2, + /// An invalid amount was provided. InvalidAmount = 3, + /// Insufficient balance to perform the transfer. InsufficientBalance = 4, + /// Insufficient allowance to perform the transfer on behalf of another user. InsufficientAllowance = 5, + /// The contract is currently paused. ContractPaused = 6, + /// A required fee is not configured. FeeNotConfigured = 7, + /// Insufficient balance to pay the fee. InsufficientFeeBalance = 8, + /// A fee exemption could not be found. FeeExemptionNotFound = 9, + /// The maximum token supply has been exceeded. MaxSupplyExceeded = 10, } diff --git a/contracts/vesting/src/lib.rs b/contracts/vesting/src/lib.rs index 48af9b7a..bdda5a6d 100644 --- a/contracts/vesting/src/lib.rs +++ b/contracts/vesting/src/lib.rs @@ -56,17 +56,26 @@ pub struct VestingInfo { pub revoked: bool, } +/// Errors returned by the vesting contract. #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[contracterror] #[repr(u32)] pub enum VestingError { + /// The contract has already been initialized. AlreadyInitialized = 1, + /// The contract has not been initialized. NotInitialized = 2, + /// An invalid amount was provided. InvalidAmount = 3, + /// An invalid vesting duration was specified. InvalidDuration = 4, + /// The specified cliff occurs after the vesting end. CliffAfterEnd = 5, + /// The vesting schedule was not found. ScheduleNotFound = 6, + /// The vesting schedule is not revocable. NotRevocable = 7, + /// The vesting schedule has already been revoked. AlreadyRevoked = 8, } diff --git a/contracts/wrapper/src/lib.rs b/contracts/wrapper/src/lib.rs index 01579427..f9c7f800 100644 --- a/contracts/wrapper/src/lib.rs +++ b/contracts/wrapper/src/lib.rs @@ -56,15 +56,22 @@ pub enum DataKey { // ─── Errors ────────────────────────────────────────────────────────────────── +/// Errors returned by the wrapper contract. #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[contracterror] #[repr(u32)] pub enum WrapperError { + /// The contract has already been initialized. AlreadyInitialized = 1, + /// The contract has not been initialized. NotInitialized = 2, + /// An invalid amount was provided. InvalidAmount = 3, + /// Insufficient balance to perform the operation. InsufficientBalance = 4, + /// Insufficient allowance to perform the operation. InsufficientAllowance = 5, + /// The contract is currently paused. ContractPaused = 6, /// Reentrant call detected. Reentrant = 7,