diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs index 18a8bb61..2029675a 100644 --- a/contracts/revenue_pool/src/lib.rs +++ b/contracts/revenue_pool/src/lib.rs @@ -235,7 +235,13 @@ impl RevenuePool { ); } - /// Return the pending admin address, or `None` if no transfer is in progress. + /// Return the pending admin address, or `None` if no two-step admin transfer is in progress. + /// + /// Integrators can poll this to detect an in-flight admin handover + /// before `accept_admin` or `claim_admin` is called. + /// + /// # Returns + /// `Some(Address)` of the nominated admin, or `None` when no transfer is pending. pub fn get_pending_admin(env: Env) -> Option
{ env.storage() .instance() @@ -732,3 +738,6 @@ mod test_invariant; #[cfg(test)] mod test_proptest; + +#[cfg(test)] +mod test_error_codes; diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index 6ddbb445..571dd3fc 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -335,6 +335,19 @@ fn create_usdc<'a>( assert_eq!(usdc_client.balance(&developer), 100); } + #[test] + fn get_pending_admin_returns_none_before_nomination() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let (_, client) = create_pool(&env); + let (usdc_address, _, _) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + + assert_eq!(client.get_pending_admin(), None); + } + #[test] fn set_admin_two_step_transfers_control_accept_admin() { let env = Env::default(); @@ -345,7 +358,6 @@ fn create_usdc<'a>( let (usdc_address, _, _) = create_usdc(&env, &admin); client.init(&admin, &usdc_address); - assert_eq!(client.get_pending_admin(), None); client.set_admin(&admin, &new_admin); assert_eq!(client.get_pending_admin(), Some(new_admin.clone())); diff --git a/contracts/revenue_pool/src/test_error_codes.rs b/contracts/revenue_pool/src/test_error_codes.rs new file mode 100644 index 00000000..1fb316fd --- /dev/null +++ b/contracts/revenue_pool/src/test_error_codes.rs @@ -0,0 +1,36 @@ +extern crate std; + +use crate::RevenuePoolError; +use std::collections::BTreeSet; + +#[test] +fn revenue_pool_error_codes_are_stable_and_unique() { + let mappings = [ + (1_u32, RevenuePoolError::BatchEmpty), + (2, RevenuePoolError::BatchTooLarge), + ]; + + let mut seen = BTreeSet::new(); + for (expected_code, variant) in mappings { + assert_eq!(variant as u32, expected_code); + assert!( + seen.insert(expected_code), + "duplicate revenue-pool error code {expected_code}" + ); + } + + assert_eq!(seen.len(), 2); +} + +#[test] +fn error_code_docs_list_every_revenue_pool_code() { + let docs = include_str!("../../../docs/ERROR_CODES.md"); + let expected_lines = [ + "| 1 | `BatchEmpty` | Revenue Pool | `batch_distribute` received an empty `payments` vector |", + "| 2 | `BatchTooLarge` | Revenue Pool | `batch_distribute` exceeded `MAX_BATCH_SIZE` |", + ]; + + for line in expected_lines { + assert!(docs.contains(line), "missing revenue-pool docs line: {line}"); + } +} diff --git a/contracts/settlement/src/errors.rs b/contracts/settlement/src/errors.rs new file mode 100644 index 00000000..25707fd5 --- /dev/null +++ b/contracts/settlement/src/errors.rs @@ -0,0 +1,45 @@ +use soroban_sdk::contracterror; + +/// Stable, machine-readable error codes for the settlement contract. +/// +/// The numeric discriminants in this enum are part of the contract interface and +/// must remain stable over time. Callers and indexers may branch on these `u32` +/// codes instead of parsing panic strings. +/// +/// | Code | Variant | Meaning | +/// |------|------------------------------|------------------------------------------------------| +/// | 1 | NotInitialized | A function was called before `init` | +/// | 2 | AlreadyInitialized | `init` was called more than once | +/// | 3 | Unauthorized | Caller is not the vault or current admin | +/// | 4 | AmountNotPositive | Amount must be greater than zero | +/// | 5 | DeveloperRequired | `to_pool=false` requires a developer address | +/// | 6 | DeveloperMustBeNone | `to_pool=true` forbids a developer address | +/// | 7 | PoolOverflow | Global pool credit would overflow `i128` | +/// | 8 | DeveloperOverflow | Developer balance credit would overflow `i128` | +/// | 9 | UsdcTokenNotConfigured | USDC token address is not configured | +/// | 10 | InsufficientDeveloperBalance | Developer balance is lower than the withdrawal | +/// | 11 | DeveloperBalanceUnderflow | Developer balance debit would underflow | +/// | 12 | InsufficientContractBalance | Contract USDC balance is lower than requested amount | +/// | 13 | DailyWithdrawCapExceeded | Daily developer withdrawal cap would be exceeded | +/// | 14 | GasExhaustionRisk | Full scan is too large; use paginated access | +/// | 15 | ReasonTooLong | Reason `Symbol` exceeds the allowed length | +#[contracterror] +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u32)] +pub enum SettlementError { + NotInitialized = 1, + AlreadyInitialized = 2, + Unauthorized = 3, + AmountNotPositive = 4, + DeveloperRequired = 5, + DeveloperMustBeNone = 6, + PoolOverflow = 7, + DeveloperOverflow = 8, + UsdcTokenNotConfigured = 9, + InsufficientDeveloperBalance = 10, + DeveloperBalanceUnderflow = 11, + InsufficientContractBalance = 12, + DailyWithdrawCapExceeded = 13, + GasExhaustionRisk = 14, + ReasonTooLong = 15, +} diff --git a/contracts/settlement/src/lib.rs b/contracts/settlement/src/lib.rs index 37b6e035..e6f3caae 100644 --- a/contracts/settlement/src/lib.rs +++ b/contracts/settlement/src/lib.rs @@ -1,6 +1,9 @@ #![no_std] -use soroban_sdk::{contract, contracterror, contractimpl, contracttype, token, Address, BytesN, Env, Symbol, Vec}; +use soroban_sdk::{contract, contractimpl, contracttype, token, Address, BytesN, Env, Symbol, Vec}; + +mod errors; +pub use errors::SettlementError; /// Maximum number of items allowed in a single `batch_receive_payment` call. pub const MAX_BATCH_SIZE: u32 = 50; @@ -8,50 +11,6 @@ pub const MAX_BATCH_SIZE: u32 = 50; /// Maximum number of developer balances returned per page in paginated queries. pub const MAX_DEVELOPER_BALANCES_PAGE_SIZE: u32 = 100; -/// Typed errors for the settlement contract. -/// -/// Using `#[contracterror]` encodes each variant as a stable `u32` code. -/// Callers and indexers can match on the code rather than parsing raw panic strings, -/// and the WASM binary shrinks because no error string literals are embedded. -/// -/// | Code | Variant | When | -/// |------|------------------------------|---------------------------------------------------| -/// | 1 | NotInitialized | A function is called before `init` | -/// | 2 | AlreadyInitialized | `init` is called more than once | -/// | 3 | Unauthorized | Caller is not the vault or admin | -/// | 4 | AmountNotPositive | `amount` is zero or negative | -/// | 5 | DeveloperRequired | `to_pool=false` but no developer address supplied | -/// | 6 | DeveloperMustBeNone | `to_pool=true` but a developer address was given | -/// | 7 | PoolOverflow | Global pool `i128` addition would overflow | -/// | 8 | DeveloperOverflow | Developer balance `i128` addition would overflow | -/// | 9 | UsdcTokenNotConfigured | USDC token address not configured for withdrawals | -/// | 10 | InsufficientDeveloperBalance | Developer balance is less than withdrawal amount | -/// | 11 | DeveloperBalanceUnderflow | Developer balance subtraction would overflow | -/// | 12 | InsufficientContractBalance | Settlement contract lacks on-ledger USDC | -/// | 13 | DailyWithdrawCapExceeded | Developer's daily withdrawal cap would be exceeded| -/// | 14 | GasExhaustionRisk | Index too large for safe full scan; use pagination| -/// | 15 | ReasonTooLong | Reason Symbol exceeds maximum allowed length | -#[contracterror] -#[derive(Clone, Copy, Debug, PartialEq)] -#[repr(u32)] -pub enum SettlementError { - NotInitialized = 1, - AlreadyInitialized = 2, - Unauthorized = 3, - AmountNotPositive = 4, - DeveloperRequired = 5, - DeveloperMustBeNone = 6, - PoolOverflow = 7, - DeveloperOverflow = 8, - UsdcTokenNotConfigured = 9, - InsufficientDeveloperBalance = 10, - DeveloperBalanceUnderflow = 11, - InsufficientContractBalance = 12, - DailyWithdrawCapExceeded = 13, - GasExhaustionRisk = 14, - ReasonTooLong = 15, -} - /// Persistent storage keys for settlement contract #[contracttype] #[derive(Clone, Debug, PartialEq)] @@ -958,9 +917,9 @@ pub fn withdraw_developer_balance( (result, next_cursor) } - /// Return the pending admin address, or `None` if no transfer is in progress. + /// Return the pending admin address, or `None` if no two-step admin transfer is in progress. /// - /// Integrators can poll this to detect an in-flight two-step admin handover + /// Integrators can poll this to detect an in-flight admin handover /// before `accept_admin` is called. /// /// # Returns @@ -1241,3 +1200,6 @@ mod test_views; #[cfg(test)] mod test_invariant; + +#[cfg(test)] +mod test_error_codes; diff --git a/contracts/settlement/src/test_error_codes.rs b/contracts/settlement/src/test_error_codes.rs new file mode 100644 index 00000000..ecf3b44c --- /dev/null +++ b/contracts/settlement/src/test_error_codes.rs @@ -0,0 +1,62 @@ +extern crate std; + +use crate::SettlementError; +use std::collections::BTreeSet; + +#[test] +fn settlement_error_codes_are_stable_and_unique() { + let mappings = [ + (1_u32, SettlementError::NotInitialized), + (2, SettlementError::AlreadyInitialized), + (3, SettlementError::Unauthorized), + (4, SettlementError::AmountNotPositive), + (5, SettlementError::DeveloperRequired), + (6, SettlementError::DeveloperMustBeNone), + (7, SettlementError::PoolOverflow), + (8, SettlementError::DeveloperOverflow), + (9, SettlementError::UsdcTokenNotConfigured), + (10, SettlementError::InsufficientDeveloperBalance), + (11, SettlementError::DeveloperBalanceUnderflow), + (12, SettlementError::InsufficientContractBalance), + (13, SettlementError::DailyWithdrawCapExceeded), + (14, SettlementError::GasExhaustionRisk), + (15, SettlementError::ReasonTooLong), + ]; + + let mut seen = BTreeSet::new(); + for (expected_code, variant) in mappings { + assert_eq!(variant as u32, expected_code); + assert!( + seen.insert(expected_code), + "duplicate settlement error code {expected_code}" + ); + } + + assert_eq!(seen.len(), 15); +} + +#[test] +fn error_code_docs_list_every_settlement_code() { + let docs = include_str!("../../../docs/ERROR_CODES.md"); + let expected_lines = [ + "| 1 | `NotInitialized` | Settlement | A function was called before `init` |", + "| 2 | `AlreadyInitialized` | Settlement | `init` was called more than once |", + "| 3 | `Unauthorized` | Settlement | Caller is not the vault or current admin |", + "| 4 | `AmountNotPositive` | Settlement | Amount must be greater than zero |", + "| 5 | `DeveloperRequired` | Settlement | `to_pool=false` requires a developer address |", + "| 6 | `DeveloperMustBeNone` | Settlement | `to_pool=true` forbids a developer address |", + "| 7 | `PoolOverflow` | Settlement | Global pool credit would overflow `i128` |", + "| 8 | `DeveloperOverflow` | Settlement | Developer balance credit would overflow `i128` |", + "| 9 | `UsdcTokenNotConfigured` | Settlement | USDC token address is not configured |", + "| 10 | `InsufficientDeveloperBalance` | Settlement | Developer balance is lower than the withdrawal |", + "| 11 | `DeveloperBalanceUnderflow` | Settlement | Developer balance debit would underflow |", + "| 12 | `InsufficientContractBalance` | Settlement | Contract USDC balance is lower than requested amount |", + "| 13 | `DailyWithdrawCapExceeded` | Settlement | Daily developer withdrawal cap would be exceeded |", + "| 14 | `GasExhaustionRisk` | Settlement | Full scan is too large; use paginated access |", + "| 15 | `ReasonTooLong` | Settlement | Reason `Symbol` exceeds the allowed length |", + ]; + + for line in expected_lines { + assert!(docs.contains(line), "missing settlement docs line: {line}"); + } +} diff --git a/contracts/vault/src/errors.rs b/contracts/vault/src/errors.rs new file mode 100644 index 00000000..ca448169 --- /dev/null +++ b/contracts/vault/src/errors.rs @@ -0,0 +1,117 @@ +use soroban_sdk::contracterror; + +/// Stable, machine-readable error codes for the Callora Vault contract. +/// +/// The numeric discriminants in this enum are part of the contract interface and +/// must remain stable over time. Callers may branch on these `u32` codes instead +/// of parsing panic strings. +/// +/// | Code | Variant | Meaning | +/// |------|--------------------------------|----------------------------------------------------------| +/// | 1 | NotInitialized | Vault has not been initialized | +/// | 2 | AlreadyInitialized | `init` was called more than once | +/// | 3 | Unauthorized | Caller is not authorized for the operation | +/// | 4 | Paused | State-changing action is blocked while paused | +/// | 5 | InsufficientBalance | Vault balance is too low for the requested operation | +/// | 6 | AmountNotPositive | Amount must be greater than zero | +/// | 7 | ExceedsMaxDeduct | Deduct amount exceeds the configured cap | +/// | 8 | BelowMinDeposit | Deposit amount is below the configured minimum | +/// | 9 | Overflow | Arithmetic overflow was detected | +/// | 10 | InitialBalanceNegative | Initial balance must be non-negative | +/// | 11 | MinDepositNotPositive | Minimum deposit must be greater than zero | +/// | 12 | MaxDeductNotPositive | Maximum deduct must be greater than zero | +/// | 13 | MinDepositExceedsMaxDeduct | Minimum deposit cannot exceed maximum deduct | +/// | 14 | UsdcTokenCannotBeVault | USDC token address cannot be the vault contract | +/// | 15 | RevenuePoolCannotBeVault | Revenue pool address cannot be the vault contract | +/// | 16 | AuthorizedCallerCannotBeVault | Authorized caller cannot be the vault contract | +/// | 17 | InitialBalanceExceedsOnLedger | Initial tracked balance exceeds on-ledger USDC | +/// | 18 | AlreadyPaused | Contract is already paused | +/// | 19 | NotPaused | Contract is not paused | +/// | 20 | SettlementNotSet | Settlement address has not been configured | +/// | 21 | BatchEmpty | Batch deduct received no items | +/// | 22 | BatchTooLarge | Batch deduct exceeds the maximum allowed size | +/// | 23 | NewOwnerSameAsCurrent | Proposed owner matches the current owner | +/// | 24 | NoOwnershipTransferPending | No ownership transfer is pending | +/// | 25 | NoAdminTransferPending | No admin transfer is pending | +/// | 26 | OfferingIdTooLong | Offering ID exceeds the maximum length | +/// | 27 | MetadataTooLong | Metadata exceeds the maximum length | +/// | 28 | PriceParseError | Price is invalid or non-positive | +/// | 29 | DuplicateRequestId | Request ID has already been processed | +/// | 30 | OfferingIdInvalid | Offering ID is empty or contains invalid characters | +/// | 31 | MetadataInvalid | Metadata is empty or contains invalid characters | +/// | 32 | StaleNonce | Rotation nonce does not match the stored current nonce | +/// | 33 | NewRevenuePoolSameAsCurrent | Proposed revenue pool matches the current revenue pool | +/// | 34 | NoRevenuePoolTransferPending | No revenue-pool transfer is pending | +#[contracterror] +#[repr(u32)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub enum VaultError { + /// Vault has not been initialized yet (code 1). + NotInitialized = 1, + /// Vault has already been initialized (code 2). + AlreadyInitialized = 2, + /// Caller is not authorized for this operation (code 3). + Unauthorized = 3, + /// Vault is currently paused (code 4). + Paused = 4, + /// Insufficient balance for the requested operation (code 5). + InsufficientBalance = 5, + /// Amount must be positive (code 6). + AmountNotPositive = 6, + /// Deduct amount exceeds the configured maximum (code 7). + ExceedsMaxDeduct = 7, + /// Deposit amount is below the configured minimum (code 8). + BelowMinDeposit = 8, + /// Arithmetic overflow detected (code 9). + Overflow = 9, + /// Initial balance must be non-negative (code 10). + InitialBalanceNegative = 10, + /// Min deposit must be positive (code 11). + MinDepositNotPositive = 11, + /// Max deduct must be positive (code 12). + MaxDeductNotPositive = 12, + /// Min deposit cannot exceed max deduct (code 13). + MinDepositExceedsMaxDeduct = 13, + /// USDC token address cannot be the vault address (code 14). + UsdcTokenCannotBeVault = 14, + /// Revenue pool address cannot be the vault address (code 15). + RevenuePoolCannotBeVault = 15, + /// Authorized caller address cannot be the vault address (code 16). + AuthorizedCallerCannotBeVault = 16, + /// Initial balance exceeds on-ledger USDC balance (code 17). + InitialBalanceExceedsOnLedger = 17, + /// Vault is already paused (code 18). + AlreadyPaused = 18, + /// Vault is not paused (code 19). + NotPaused = 19, + /// Settlement address has not been configured (code 20). + SettlementNotSet = 20, + /// Batch deduct requires at least one item (code 21). + BatchEmpty = 21, + /// Batch size exceeds maximum allowed (code 22). + BatchTooLarge = 22, + /// New owner must be different from current owner (code 23). + NewOwnerSameAsCurrent = 23, + /// No ownership transfer is pending (code 24). + NoOwnershipTransferPending = 24, + /// No admin transfer is pending (code 25). + NoAdminTransferPending = 25, + /// Offering ID exceeds maximum length (code 26). + OfferingIdTooLong = 26, + /// Metadata exceeds maximum length (code 27). + MetadataTooLong = 27, + /// Price parsing error or non-positive price (code 28). + PriceParseError = 28, + /// Duplicate request ID detected (code 29). + DuplicateRequestId = 29, + /// Offering ID is empty or contains invalid characters (code 30). + OfferingIdInvalid = 30, + /// Metadata string is empty or contains invalid characters (code 31). + MetadataInvalid = 31, + /// Supplied nonce does not match the stored authorized-caller rotation nonce (code 32). + StaleNonce = 32, + /// New revenue pool must be different from current revenue pool (code 33). + NewRevenuePoolSameAsCurrent = 33, + /// No revenue pool transfer is pending (code 34). + NoRevenuePoolTransferPending = 34, +} diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index dfcfc4ab..2e169c8b 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -31,87 +31,12 @@ /// persistent, they do not silently archive. To prevent state bloat, an owner /// can explicitly prune old markers using `prune_processed_requests`. use soroban_sdk::{ - contract, contractclient, contracterror, contractimpl, contracttype, token, Address, BytesN, - Env, String, Symbol, Vec, + contract, contractclient, contractimpl, contracttype, token, Address, BytesN, Env, String, + Symbol, Vec, }; -/// Typed error codes for the Callora Vault contract. -/// -/// These error codes are returned instead of string panics to enable -/// machine-readable error handling by integrators using @stellar/stellar-sdk. -#[contracterror] -#[repr(u32)] -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -pub enum VaultError { - /// Vault has not been initialized yet (code 1). - NotInitialized = 1, - /// Vault has already been initialized (code 2). - AlreadyInitialized = 2, - /// Caller is not authorized for this operation (code 3). - Unauthorized = 3, - /// Vault is currently paused (code 4). - Paused = 4, - /// Insufficient balance for the requested operation (code 5). - InsufficientBalance = 5, - /// Amount must be positive (code 6). - AmountNotPositive = 6, - /// Deduct amount exceeds the configured maximum (code 7). - ExceedsMaxDeduct = 7, - /// Deposit amount is below the configured minimum (code 8). - BelowMinDeposit = 8, - /// Arithmetic overflow detected (code 9). - Overflow = 9, - /// Initial balance must be non-negative (code 10). - InitialBalanceNegative = 10, - /// Min deposit must be positive (code 11). - MinDepositNotPositive = 11, - /// Max deduct must be positive (code 12). - MaxDeductNotPositive = 12, - /// Min deposit cannot exceed max deduct (code 13). - MinDepositExceedsMaxDeduct = 13, - /// USDC token address cannot be the vault address (code 14). - UsdcTokenCannotBeVault = 14, - /// Revenue pool address cannot be the vault address (code 15). - RevenuePoolCannotBeVault = 15, - /// Authorized caller address cannot be the vault address (code 16). - AuthorizedCallerCannotBeVault = 16, - /// Initial balance exceeds on-ledger USDC balance (code 17). - InitialBalanceExceedsOnLedger = 17, - /// Vault is already paused (code 18). - AlreadyPaused = 18, - /// Vault is not paused (code 19). - NotPaused = 19, - /// Settlement address has not been configured (code 20). - SettlementNotSet = 20, - /// Batch deduct requires at least one item (code 21). - BatchEmpty = 21, - /// Batch size exceeds maximum allowed (code 22). - BatchTooLarge = 22, - /// New owner must be different from current owner (code 23). - NewOwnerSameAsCurrent = 23, - /// No ownership transfer is pending (code 24). - NoOwnershipTransferPending = 24, - /// No admin transfer is pending (code 25). - NoAdminTransferPending = 25, - /// Offering ID exceeds maximum length (code 26). - OfferingIdTooLong = 26, - /// Metadata exceeds maximum length (code 27). - MetadataTooLong = 27, - /// Price parsing error or non‑positive price (code 28). - PriceParseError = 28, - /// Duplicate request ID detected (code 29). - DuplicateRequestId = 29, - /// Offering ID is empty or contains invalid characters (code 30). - OfferingIdInvalid = 30, - /// Metadata string is empty or contains invalid characters (code 31). - MetadataInvalid = 31, - /// Supplied nonce does not match the stored authorized-caller rotation nonce (code 30). - StaleNonce = 32, - /// New revenue pool must be different from current revenue pool (code 33). - NewRevenuePoolSameAsCurrent = 33, - /// No revenue pool transfer is pending (code 34). - NoRevenuePoolTransferPending = 34, -} +mod errors; +pub use errors::VaultError; #[contracttype] #[derive(Clone)] @@ -358,7 +283,13 @@ impl CalloraVault { env.storage().instance().get(&StorageKey::PendingOwner) } - /// Return the pending admin address, or `None` if no admin transfer is in progress. + /// Return the pending admin address, or `None` if no two-step admin transfer is in progress. + /// + /// Integrators can poll this to detect an in-flight admin handover + /// before `accept_admin` is called. + /// + /// # Returns + /// `Some(Address)` of the nominated admin, or `None` when no transfer is pending. pub fn get_pending_admin(env: Env) -> Option
{ env.storage().instance().get(&StorageKey::PendingAdmin) } @@ -1460,7 +1391,7 @@ impl CalloraVault { .publish((Symbol::new(&env, "request_id_pruned"), caller.clone()), id.clone()); } } - + Ok(()) } @@ -1606,6 +1537,9 @@ mod test_views; #[cfg(test)] mod test_idempotency; +#[cfg(test)] +mod test_error_codes; + #[cfg(test)] mod test_reentrancy; diff --git a/contracts/vault/src/test_error_codes.rs b/contracts/vault/src/test_error_codes.rs new file mode 100644 index 00000000..4f10e68e --- /dev/null +++ b/contracts/vault/src/test_error_codes.rs @@ -0,0 +1,97 @@ +extern crate std; + +use crate::VaultError; +use std::collections::BTreeSet; + +#[test] +fn vault_error_codes_are_stable_and_unique() { + let mappings = [ + (1_u32, VaultError::NotInitialized), + (2, VaultError::AlreadyInitialized), + (3, VaultError::Unauthorized), + (4, VaultError::Paused), + (5, VaultError::InsufficientBalance), + (6, VaultError::AmountNotPositive), + (7, VaultError::ExceedsMaxDeduct), + (8, VaultError::BelowMinDeposit), + (9, VaultError::Overflow), + (10, VaultError::InitialBalanceNegative), + (11, VaultError::MinDepositNotPositive), + (12, VaultError::MaxDeductNotPositive), + (13, VaultError::MinDepositExceedsMaxDeduct), + (14, VaultError::UsdcTokenCannotBeVault), + (15, VaultError::RevenuePoolCannotBeVault), + (16, VaultError::AuthorizedCallerCannotBeVault), + (17, VaultError::InitialBalanceExceedsOnLedger), + (18, VaultError::AlreadyPaused), + (19, VaultError::NotPaused), + (20, VaultError::SettlementNotSet), + (21, VaultError::BatchEmpty), + (22, VaultError::BatchTooLarge), + (23, VaultError::NewOwnerSameAsCurrent), + (24, VaultError::NoOwnershipTransferPending), + (25, VaultError::NoAdminTransferPending), + (26, VaultError::OfferingIdTooLong), + (27, VaultError::MetadataTooLong), + (28, VaultError::PriceParseError), + (29, VaultError::DuplicateRequestId), + (30, VaultError::OfferingIdInvalid), + (31, VaultError::MetadataInvalid), + (32, VaultError::StaleNonce), + (33, VaultError::NewRevenuePoolSameAsCurrent), + (34, VaultError::NoRevenuePoolTransferPending), + ]; + + let mut seen = BTreeSet::new(); + for (expected_code, variant) in mappings { + assert_eq!(variant as u32, expected_code); + assert!(seen.insert(expected_code), "duplicate vault error code {expected_code}"); + } + + assert_eq!(seen.len(), 34); +} + +#[test] +fn error_code_docs_list_every_vault_code() { + let docs = include_str!("../../../docs/ERROR_CODES.md"); + let expected_lines = [ + "| 1 | `NotInitialized` | Vault | Vault has not been initialized |", + "| 2 | `AlreadyInitialized` | Vault | `init` was called more than once |", + "| 3 | `Unauthorized` | Vault | Caller is not authorized for the operation |", + "| 4 | `Paused` | Vault | State-changing action is blocked while paused |", + "| 5 | `InsufficientBalance` | Vault | Vault balance is too low for the requested operation |", + "| 6 | `AmountNotPositive` | Vault | Amount must be greater than zero |", + "| 7 | `ExceedsMaxDeduct` | Vault | Deduct amount exceeds the configured cap |", + "| 8 | `BelowMinDeposit` | Vault | Deposit amount is below the configured minimum |", + "| 9 | `Overflow` | Vault | Arithmetic overflow was detected |", + "| 10 | `InitialBalanceNegative` | Vault | Initial balance must be non-negative |", + "| 11 | `MinDepositNotPositive` | Vault | Minimum deposit must be greater than zero |", + "| 12 | `MaxDeductNotPositive` | Vault | Maximum deduct must be greater than zero |", + "| 13 | `MinDepositExceedsMaxDeduct` | Vault | Minimum deposit cannot exceed maximum deduct |", + "| 14 | `UsdcTokenCannotBeVault` | Vault | USDC token address cannot be the vault contract |", + "| 15 | `RevenuePoolCannotBeVault` | Vault | Revenue pool address cannot be the vault contract |", + "| 16 | `AuthorizedCallerCannotBeVault` | Vault | Authorized caller cannot be the vault contract |", + "| 17 | `InitialBalanceExceedsOnLedger` | Vault | Initial tracked balance exceeds on-ledger USDC |", + "| 18 | `AlreadyPaused` | Vault | Contract is already paused |", + "| 19 | `NotPaused` | Vault | Contract is not paused |", + "| 20 | `SettlementNotSet` | Vault | Settlement address has not been configured |", + "| 21 | `BatchEmpty` | Vault | Batch deduct received no items |", + "| 22 | `BatchTooLarge` | Vault | Batch deduct exceeds the maximum allowed size |", + "| 23 | `NewOwnerSameAsCurrent` | Vault | Proposed owner matches the current owner |", + "| 24 | `NoOwnershipTransferPending` | Vault | No ownership transfer is pending |", + "| 25 | `NoAdminTransferPending` | Vault | No admin transfer is pending |", + "| 26 | `OfferingIdTooLong` | Vault | Offering ID exceeds the maximum length |", + "| 27 | `MetadataTooLong` | Vault | Metadata exceeds the maximum length |", + "| 28 | `PriceParseError` | Vault | Price is invalid or non-positive |", + "| 29 | `DuplicateRequestId` | Vault | Request ID has already been processed |", + "| 30 | `OfferingIdInvalid` | Vault | Offering ID is empty or contains invalid characters |", + "| 31 | `MetadataInvalid` | Vault | Metadata is empty or contains invalid characters |", + "| 32 | `StaleNonce` | Vault | Rotation nonce does not match the stored current nonce |", + "| 33 | `NewRevenuePoolSameAsCurrent` | Vault | Proposed revenue pool matches the current revenue pool |", + "| 34 | `NoRevenuePoolTransferPending` | Vault | No revenue-pool transfer is pending |", + ]; + + for line in expected_lines { + assert!(docs.contains(line), "missing vault docs line: {line}"); + } +} diff --git a/docs/ERROR_CODES.md b/docs/ERROR_CODES.md new file mode 100644 index 00000000..16785f6c --- /dev/null +++ b/docs/ERROR_CODES.md @@ -0,0 +1,78 @@ +# Contract Error Codes + +Stable, semantic `u32` error codes used by the GrantFox smart contracts. +These numeric discriminants are part of each contract's public interface and +must not be reassigned once released. + +## Stability rules + +- Preserve every existing numeric code for its current semantic meaning. +- Add new variants only with new, previously unused codes in that contract. +- Do not reuse a removed code for a different error. +- `cargo test --workspace` enforces code stability and duplicate-code checks. + +## Vault + +| Code | Variant | Contract | Meaning | +|------|---------|----------|---------| +| 1 | `NotInitialized` | Vault | Vault has not been initialized | +| 2 | `AlreadyInitialized` | Vault | `init` was called more than once | +| 3 | `Unauthorized` | Vault | Caller is not authorized for the operation | +| 4 | `Paused` | Vault | State-changing action is blocked while paused | +| 5 | `InsufficientBalance` | Vault | Vault balance is too low for the requested operation | +| 6 | `AmountNotPositive` | Vault | Amount must be greater than zero | +| 7 | `ExceedsMaxDeduct` | Vault | Deduct amount exceeds the configured cap | +| 8 | `BelowMinDeposit` | Vault | Deposit amount is below the configured minimum | +| 9 | `Overflow` | Vault | Arithmetic overflow was detected | +| 10 | `InitialBalanceNegative` | Vault | Initial balance must be non-negative | +| 11 | `MinDepositNotPositive` | Vault | Minimum deposit must be greater than zero | +| 12 | `MaxDeductNotPositive` | Vault | Maximum deduct must be greater than zero | +| 13 | `MinDepositExceedsMaxDeduct` | Vault | Minimum deposit cannot exceed maximum deduct | +| 14 | `UsdcTokenCannotBeVault` | Vault | USDC token address cannot be the vault contract | +| 15 | `RevenuePoolCannotBeVault` | Vault | Revenue pool address cannot be the vault contract | +| 16 | `AuthorizedCallerCannotBeVault` | Vault | Authorized caller cannot be the vault contract | +| 17 | `InitialBalanceExceedsOnLedger` | Vault | Initial tracked balance exceeds on-ledger USDC | +| 18 | `AlreadyPaused` | Vault | Contract is already paused | +| 19 | `NotPaused` | Vault | Contract is not paused | +| 20 | `SettlementNotSet` | Vault | Settlement address has not been configured | +| 21 | `BatchEmpty` | Vault | Batch deduct received no items | +| 22 | `BatchTooLarge` | Vault | Batch deduct exceeds the maximum allowed size | +| 23 | `NewOwnerSameAsCurrent` | Vault | Proposed owner matches the current owner | +| 24 | `NoOwnershipTransferPending` | Vault | No ownership transfer is pending | +| 25 | `NoAdminTransferPending` | Vault | No admin transfer is pending | +| 26 | `OfferingIdTooLong` | Vault | Offering ID exceeds the maximum length | +| 27 | `MetadataTooLong` | Vault | Metadata exceeds the maximum length | +| 28 | `PriceParseError` | Vault | Price is invalid or non-positive | +| 29 | `DuplicateRequestId` | Vault | Request ID has already been processed | +| 30 | `OfferingIdInvalid` | Vault | Offering ID is empty or contains invalid characters | +| 31 | `MetadataInvalid` | Vault | Metadata is empty or contains invalid characters | +| 32 | `StaleNonce` | Vault | Rotation nonce does not match the stored current nonce | +| 33 | `NewRevenuePoolSameAsCurrent` | Vault | Proposed revenue pool matches the current revenue pool | +| 34 | `NoRevenuePoolTransferPending` | Vault | No revenue-pool transfer is pending | + +## Settlement + +| Code | Variant | Contract | Meaning | +|------|---------|----------|---------| +| 1 | `NotInitialized` | Settlement | A function was called before `init` | +| 2 | `AlreadyInitialized` | Settlement | `init` was called more than once | +| 3 | `Unauthorized` | Settlement | Caller is not the vault or current admin | +| 4 | `AmountNotPositive` | Settlement | Amount must be greater than zero | +| 5 | `DeveloperRequired` | Settlement | `to_pool=false` requires a developer address | +| 6 | `DeveloperMustBeNone` | Settlement | `to_pool=true` forbids a developer address | +| 7 | `PoolOverflow` | Settlement | Global pool credit would overflow `i128` | +| 8 | `DeveloperOverflow` | Settlement | Developer balance credit would overflow `i128` | +| 9 | `UsdcTokenNotConfigured` | Settlement | USDC token address is not configured | +| 10 | `InsufficientDeveloperBalance` | Settlement | Developer balance is lower than the withdrawal | +| 11 | `DeveloperBalanceUnderflow` | Settlement | Developer balance debit would underflow | +| 12 | `InsufficientContractBalance` | Settlement | Contract USDC balance is lower than requested amount | +| 13 | `DailyWithdrawCapExceeded` | Settlement | Daily developer withdrawal cap would be exceeded | +| 14 | `GasExhaustionRisk` | Settlement | Full scan is too large; use paginated access | +| 15 | `ReasonTooLong` | Settlement | Reason `Symbol` exceeds the allowed length | + +## Revenue Pool + +| Code | Variant | Contract | Meaning | +|------|---------|----------|---------| +| 1 | `BatchEmpty` | Revenue Pool | `batch_distribute` received an empty `payments` vector | +| 2 | `BatchTooLarge` | Revenue Pool | `batch_distribute` exceeded `MAX_BATCH_SIZE` | diff --git a/docs/interfaces/revenue_pool.json b/docs/interfaces/revenue_pool.json index 0a6a0082..40ceabcc 100644 --- a/docs/interfaces/revenue_pool.json +++ b/docs/interfaces/revenue_pool.json @@ -13,8 +13,18 @@ "description": "Initialize the revenue pool with an admin and the USDC token address. Can only be called once. The admin must authorize the call.", "access": "admin (must sign)", "params": [ - { "name": "admin", "type": "Address", "optional": false, "description": "Address that may call distribute, batch_distribute, receive_payment, and set_admin." }, - { "name": "usdc_token", "type": "Address", "optional": false, "description": "Stellar USDC (or wrapped USDC) token contract address." } + { + "name": "admin", + "type": "Address", + "optional": false, + "description": "Address that may call distribute, batch_distribute, receive_payment, and set_admin." + }, + { + "name": "usdc_token", + "type": "Address", + "optional": false, + "description": "Stellar USDC (or wrapped USDC) token contract address." + } ], "returns": "void", "panics": [ @@ -30,7 +40,12 @@ "description": "Activate the circuit-breaker. Blocks distribute and batch_distribute until unpause is called. Admin rotation (set_admin / claim_admin) remains available while paused.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + } ], "returns": "void", "panics": [ @@ -47,7 +62,12 @@ "description": "Deactivate the circuit-breaker, restoring distribute and batch_distribute.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + } ], "returns": "void", "panics": [ @@ -74,9 +94,24 @@ "description": "Transfer USDC from this contract to a developer wallet. Admin only. Blocked while paused.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." }, - { "name": "to", "type": "Address", "optional": false, "description": "Developer address to receive USDC." }, - { "name": "amount", "type": "i128", "optional": false, "description": "Amount in USDC base units; must be > 0." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + }, + { + "name": "to", + "type": "Address", + "optional": false, + "description": "Developer address to receive USDC." + }, + { + "name": "amount", + "type": "i128", + "optional": false, + "description": "Amount in USDC base units; must be > 0." + } ], "returns": "void", "panics": [ @@ -96,8 +131,18 @@ "description": "Atomically distribute USDC to multiple developer wallets. Validates the total against the available balance before any transfer. One event is emitted per payment. Admin only. Blocked while paused.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." }, - { "name": "payments", "type": "Vec<(Address, i128)>", "optional": false, "description": "Ordered list of (developer_address, amount) pairs. All amounts must be > 0." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + }, + { + "name": "payments", + "type": "Vec<(Address, i128)>", + "optional": false, + "description": "Ordered list of (developer_address, amount) pairs. All amounts must be > 0." + } ], "returns": "void", "panics": [ @@ -109,7 +154,11 @@ "\"insufficient USDC balance\" — total amount exceeds contract balance." ], "events": [ - { "note": "One event per payment entry.", "topics": ["\"batch_distribute\"", "to"], "data": "amount (i128)" } + { + "note": "One event per payment entry.", + "topics": ["\"batch_distribute\"", "to"], + "data": "amount (i128)" + } ] }, @@ -118,16 +167,34 @@ "description": "Event-only helper — does NOT move tokens. Emits a receive_payment event for indexer alignment when the backend wants to log a payment credited from the vault. USDC is received passively when any address transfers tokens to this contract; no explicit call is required for token receipt.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." }, - { "name": "amount", "type": "i128", "optional": false, "description": "Amount received (for event logging only; no tokens are moved)." }, - { "name": "from_vault", "type": "bool", "optional": false, "description": "True if the source was the vault contract." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + }, + { + "name": "amount", + "type": "i128", + "optional": false, + "description": "Amount received (for event logging only; no tokens are moved)." + }, + { + "name": "from_vault", + "type": "bool", + "optional": false, + "description": "True if the source was the vault contract." + } ], "returns": "void", "panics": [ "\"unauthorized: caller is not admin\" — caller != current admin." ], "events": [ - { "topics": ["\"receive_payment\"", "caller"], "data": "(amount, from_vault) tuple" } + { + "topics": ["\"receive_payment\"", "caller"], + "data": "(amount, from_vault) tuple" + } ] }, @@ -137,9 +204,7 @@ "access": "any", "params": [], "returns": "i128", - "panics": [ - "\"revenue pool not initialized\" — called before init." - ], + "panics": ["\"revenue pool not initialized\" — called before init."], "events": [] }, @@ -149,9 +214,17 @@ "access": "any", "params": [], "returns": "Address", - "panics": [ - "\"revenue pool not initialized\" — called before init." - ], + "panics": ["\"revenue pool not initialized\" — called before init."], + "events": [] + }, + + { + "name": "get_pending_admin", + "description": "Return the pending admin address, or null if no two-step transfer is in progress. Integrators can poll this to detect an in-flight admin handover before accept_admin or claim_admin is called.", + "access": "any", + "params": [], + "returns": "Address | null", + "panics": [], "events": [] }, @@ -160,15 +233,28 @@ "description": "Initiate replacement of the current admin. The proposed new admin must call claim_admin to complete the transfer.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." }, - { "name": "new_admin", "type": "Address", "optional": false, "description": "Address of the proposed new admin." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + }, + { + "name": "new_admin", + "type": "Address", + "optional": false, + "description": "Address of the proposed new admin." + } ], "returns": "void", "panics": [ "\"unauthorized: caller is not admin\" — caller != current admin." ], "events": [ - { "topics": ["\"admin_transfer_started\"", "current_admin"], "data": "new_admin (Address)" } + { + "topics": ["\"admin_transfer_started\"", "current_admin"], + "data": "new_admin (Address)" + } ] }, @@ -177,7 +263,12 @@ "description": "Complete the admin transfer. Must be called by the pending admin set via set_admin.", "access": "pending admin (must sign)", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the pending admin; must authorize." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the pending admin; must authorize." + } ], "returns": "void", "panics": [ @@ -185,7 +276,10 @@ "\"unauthorized: caller is not pending admin\" — caller is not the pending admin." ], "events": [ - { "topics": ["\"admin_transfer_completed\"", "new_admin"], "data": "void" } + { + "topics": ["\"admin_transfer_completed\"", "new_admin"], + "data": "void" + } ] } ] diff --git a/docs/interfaces/settlement.json b/docs/interfaces/settlement.json index 08b79ef9..ca2ff55b 100644 --- a/docs/interfaces/settlement.json +++ b/docs/interfaces/settlement.json @@ -8,19 +8,71 @@ "errors": { "description": "Typed error codes emitted via env.panic_with_error(). Callers and indexers match on the u32 code rather than parsing raw strings.", "variants": [ - { "code": 1, "name": "NotInitialized", "when": "A function is called before init." }, - { "code": 2, "name": "AlreadyInitialized", "when": "init is called more than once." }, - { "code": 3, "name": "Unauthorized", "when": "Caller is not the registered vault or admin." }, - { "code": 4, "name": "AmountNotPositive", "when": "amount is zero or negative." }, - { "code": 5, "name": "DeveloperRequired", "when": "to_pool=false but no developer address was supplied." }, - { "code": 6, "name": "DeveloperMustBeNone", "when": "to_pool=true but a developer address was given." }, - { "code": 7, "name": "PoolOverflow", "when": "Global pool i128 addition would overflow." }, - { "code": 8, "name": "DeveloperOverflow", "when": "Developer balance i128 addition would overflow." }, - { "code": 9, "name": "UsdcTokenNotConfigured", "when": "USDC token address not configured for withdrawals." }, - { "code": 10, "name": "InsufficientDeveloperBalance", "when": "Developer balance is less than withdrawal amount." }, - { "code": 11, "name": "DeveloperBalanceUnderflow", "when": "Developer balance subtraction would overflow." }, - { "code": 12, "name": "InsufficientContractBalance", "when": "Settlement contract lacks on-ledger USDC." }, - { "code": 13, "name": "GasExhaustionRisk", "when": "Index exceeds 100 entries; use get_developer_balances_cursor instead of get_all_developer_balances." } + { + "code": 1, + "name": "NotInitialized", + "when": "A function is called before init." + }, + { + "code": 2, + "name": "AlreadyInitialized", + "when": "init is called more than once." + }, + { + "code": 3, + "name": "Unauthorized", + "when": "Caller is not the registered vault or admin." + }, + { + "code": 4, + "name": "AmountNotPositive", + "when": "amount is zero or negative." + }, + { + "code": 5, + "name": "DeveloperRequired", + "when": "to_pool=false but no developer address was supplied." + }, + { + "code": 6, + "name": "DeveloperMustBeNone", + "when": "to_pool=true but a developer address was given." + }, + { + "code": 7, + "name": "PoolOverflow", + "when": "Global pool i128 addition would overflow." + }, + { + "code": 8, + "name": "DeveloperOverflow", + "when": "Developer balance i128 addition would overflow." + }, + { + "code": 9, + "name": "UsdcTokenNotConfigured", + "when": "USDC token address not configured for withdrawals." + }, + { + "code": 10, + "name": "InsufficientDeveloperBalance", + "when": "Developer balance is less than withdrawal amount." + }, + { + "code": 11, + "name": "DeveloperBalanceUnderflow", + "when": "Developer balance subtraction would overflow." + }, + { + "code": 12, + "name": "InsufficientContractBalance", + "when": "Settlement contract lacks on-ledger USDC." + }, + { + "code": 13, + "name": "GasExhaustionRisk", + "when": "Index exceeds 100 entries; use get_developer_balances_cursor instead of get_all_developer_balances." + } ] }, @@ -28,32 +80,62 @@ "DeveloperBalance": { "description": "Snapshot of a single developer's tracked balance.", "fields": { - "address": { "type": "Address", "description": "Developer Stellar address." }, - "balance": { "type": "i128", "description": "Credited balance in USDC base units (stroops)." } + "address": { + "type": "Address", + "description": "Developer Stellar address." + }, + "balance": { + "type": "i128", + "description": "Credited balance in USDC base units (stroops)." + } } }, "GlobalPool": { "description": "Aggregate pool state.", "fields": { - "total_balance": { "type": "i128", "description": "Total USDC credited to the global pool." }, - "last_updated": { "type": "u64", "description": "Ledger timestamp of the last pool update." } + "total_balance": { + "type": "i128", + "description": "Total USDC credited to the global pool." + }, + "last_updated": { + "type": "u64", + "description": "Ledger timestamp of the last pool update." + } } }, "PaymentReceivedEvent": { "description": "Event payload emitted on every receive_payment call.", "fields": { - "from_vault": { "type": "Address", "description": "Address of the caller (typically the vault)." }, - "amount": { "type": "i128", "description": "Payment amount in USDC base units." }, - "to_pool": { "type": "bool", "description": "True when credited to the global pool; false when credited to a developer." }, - "developer": { "type": "Address | null", "description": "Developer address when to_pool=false; null otherwise." } + "from_vault": { + "type": "Address", + "description": "Address of the caller (typically the vault)." + }, + "amount": { + "type": "i128", + "description": "Payment amount in USDC base units." + }, + "to_pool": { + "type": "bool", + "description": "True when credited to the global pool; false when credited to a developer." + }, + "developer": { + "type": "Address | null", + "description": "Developer address when to_pool=false; null otherwise." + } } }, "BalanceCreditedEvent": { "description": "Event payload emitted when a developer balance is increased (to_pool=false).", "fields": { - "developer": { "type": "Address", "description": "Developer address that was credited." }, - "amount": { "type": "i128", "description": "Amount credited." }, - "new_balance": { "type": "i128", "description": "Developer's balance after crediting." } + "developer": { + "type": "Address", + "description": "Developer address that was credited." + }, + "amount": { "type": "i128", "description": "Amount credited." }, + "new_balance": { + "type": "i128", + "description": "Developer's balance after crediting." + } } } }, @@ -64,12 +146,26 @@ "description": "Initialize the settlement contract. Can only be called once. Sets admin, registers the vault address, creates an empty developer balance map, and initializes the global pool.", "access": "any (no auth requirement on init itself; admin address is stored)", "params": [ - { "name": "admin", "type": "Address", "optional": false, "description": "Address that may call set_admin and set_vault." }, - { "name": "vault_address", "type": "Address", "optional": false, "description": "Vault contract address permitted to call receive_payment." } + { + "name": "admin", + "type": "Address", + "optional": false, + "description": "Address that may call set_admin and set_vault." + }, + { + "name": "vault_address", + "type": "Address", + "optional": false, + "description": "Vault contract address permitted to call receive_payment." + } ], "returns": "void", "errors": [ - { "code": 2, "name": "AlreadyInitialized", "when": "Called more than once." } + { + "code": 2, + "name": "AlreadyInitialized", + "when": "Called more than once." + } ], "events": [] }, @@ -79,19 +175,59 @@ "description": "Receive a payment from the vault and credit the global pool or a specific developer. The caller must be the registered vault or the admin.", "access": "registered vault OR admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the registered vault or admin; must authorize." }, - { "name": "amount", "type": "i128", "optional": false, "description": "Payment amount in USDC base units; must be > 0." }, - { "name": "to_pool", "type": "bool", "optional": false, "description": "If true, credit the global pool. If false, credit the specified developer." }, - { "name": "developer", "type": "Address | null", "optional": true, "description": "Required when to_pool=false; the developer to credit. Ignored when to_pool=true." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the registered vault or admin; must authorize." + }, + { + "name": "amount", + "type": "i128", + "optional": false, + "description": "Payment amount in USDC base units; must be > 0." + }, + { + "name": "to_pool", + "type": "bool", + "optional": false, + "description": "If true, credit the global pool. If false, credit the specified developer." + }, + { + "name": "developer", + "type": "Address | null", + "optional": true, + "description": "Required when to_pool=false; the developer to credit. Ignored when to_pool=true." + } ], "returns": "void", "errors": [ - { "code": 3, "name": "Unauthorized", "when": "Caller is neither the vault nor the admin." }, - { "code": 4, "name": "AmountNotPositive", "when": "amount <= 0." }, - { "code": 5, "name": "DeveloperRequired", "when": "to_pool=false and developer is null." }, - { "code": 6, "name": "DeveloperMustBeNone", "when": "to_pool=true and developer is not null." }, - { "code": 7, "name": "PoolOverflow", "when": "Global pool i128 addition would overflow." }, - { "code": 8, "name": "DeveloperOverflow", "when": "Developer balance i128 addition would overflow." } + { + "code": 3, + "name": "Unauthorized", + "when": "Caller is neither the vault nor the admin." + }, + { "code": 4, "name": "AmountNotPositive", "when": "amount <= 0." }, + { + "code": 5, + "name": "DeveloperRequired", + "when": "to_pool=false and developer is null." + }, + { + "code": 6, + "name": "DeveloperMustBeNone", + "when": "to_pool=true and developer is not null." + }, + { + "code": 7, + "name": "PoolOverflow", + "when": "Global pool i128 addition would overflow." + }, + { + "code": 8, + "name": "DeveloperOverflow", + "when": "Developer balance i128 addition would overflow." + } ], "events": [ { @@ -112,7 +248,12 @@ "description": "Return the tracked USDC balance for a developer. Returns 0 if the developer has never received a payment.", "access": "any", "params": [ - { "name": "developer", "type": "Address", "optional": false, "description": "Developer address to query." } + { + "name": "developer", + "type": "Address", + "optional": false, + "description": "Developer address to query." + } ], "returns": "i128", "errors": [ @@ -126,13 +267,26 @@ "description": "Return a list of all developer balances. Admin only. Rejects the call with GasExhaustionRisk when the developer index exceeds 100 entries — use get_developer_balances_cursor for larger sets. The index is maintained in deterministic ascending order by address bytes.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin." + } ], "returns": "Vec", "errors": [ - { "code": 1, "name": "NotInitialized", "when": "Called before init." }, - { "code": 3, "name": "Unauthorized", "when": "Caller is not the admin." }, - { "code": 13, "name": "GasExhaustionRisk", "when": "Developer index has more than 100 entries." } + { "code": 1, "name": "NotInitialized", "when": "Called before init." }, + { + "code": 3, + "name": "Unauthorized", + "when": "Caller is not the admin." + }, + { + "code": 13, + "name": "GasExhaustionRisk", + "when": "Developer index has more than 100 entries." + } ], "events": [] }, @@ -142,9 +296,24 @@ "description": "Cursor-based paginated developer balances (admin only). Returns up to `limit` DeveloperBalance records starting after the supplied `cursor` address (exclusive), or from the beginning of the sorted index when `cursor` is null. The DeveloperIndex is maintained in deterministic ascending order by address bytes, so pages are stable across interleaved receive_payment calls for developers that sort after the cursor. The limit is capped at 100 (MAX_DEVELOPER_BALANCES_PAGE_SIZE).", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." }, - { "name": "cursor", "type": "Address | null", "optional": true, "description": "Exclusive start position. Pass null for the first page; pass the next_cursor value returned by the previous call for subsequent pages." }, - { "name": "limit", "type": "u32", "optional": false, "description": "Maximum records to return per page; silently capped at 100." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + }, + { + "name": "cursor", + "type": "Address | null", + "optional": true, + "description": "Exclusive start position. Pass null for the first page; pass the next_cursor value returned by the previous call for subsequent pages." + }, + { + "name": "limit", + "type": "u32", + "optional": false, + "description": "Maximum records to return per page; silently capped at 100." + } ], "returns": { "type": "(Vec, Address | null)", @@ -152,7 +321,11 @@ }, "errors": [ { "code": 1, "name": "NotInitialized", "when": "Called before init." }, - { "code": 3, "name": "Unauthorized", "when": "Caller is not the admin." } + { + "code": 3, + "name": "Unauthorized", + "when": "Caller is not the admin." + } ], "events": [], "notes": [ @@ -214,15 +387,32 @@ "description": "Nominate a new admin. The nominee must call accept_admin to finalize.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." }, - { "name": "new_admin", "type": "Address", "optional": false, "description": "Proposed new admin address." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + }, + { + "name": "new_admin", + "type": "Address", + "optional": false, + "description": "Proposed new admin address." + } ], "returns": "void", "errors": [ - { "code": 3, "name": "Unauthorized", "when": "Caller is not the current admin." } + { + "code": 3, + "name": "Unauthorized", + "when": "Caller is not the current admin." + } ], "events": [ - { "topics": ["\"admin_nominated\"", "current_admin", "new_admin"], "data": "void" } + { + "topics": ["\"admin_nominated\"", "current_admin", "new_admin"], + "data": "void" + } ] }, @@ -236,7 +426,10 @@ "\"no admin transfer pending\" — set_admin was not called first." ], "events": [ - { "topics": ["\"admin_accepted\"", "old_admin", "new_admin"], "data": "void" } + { + "topics": ["\"admin_accepted\"", "old_admin", "new_admin"], + "data": "void" + } ] }, @@ -245,12 +438,26 @@ "description": "Update the registered vault address. Only admin may call this.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." }, - { "name": "new_vault", "type": "Address", "optional": false, "description": "New vault contract address." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + }, + { + "name": "new_vault", + "type": "Address", + "optional": false, + "description": "New vault contract address." + } ], "returns": "void", "errors": [ - { "code": 3, "name": "Unauthorized", "when": "Caller is not the current admin." } + { + "code": 3, + "name": "Unauthorized", + "when": "Caller is not the current admin." + } ], "events": [] } diff --git a/docs/interfaces/vault.json b/docs/interfaces/vault.json index ec3d6367..f3eae10a 100644 --- a/docs/interfaces/vault.json +++ b/docs/interfaces/vault.json @@ -8,35 +8,151 @@ "errors": { "description": "Typed error codes returned by vault operations. These replace string panics to enable machine-readable error handling.", "codes": [ - { "code": 1, "name": "NotInitialized", "description": "Vault has not been initialized yet." }, - { "code": 2, "name": "AlreadyInitialized", "description": "Vault has already been initialized." }, - { "code": 3, "name": "Unauthorized", "description": "Caller is not authorized for this operation." }, - { "code": 4, "name": "Paused", "description": "Vault is currently paused." }, - { "code": 5, "name": "InsufficientBalance", "description": "Insufficient balance for the requested operation." }, - { "code": 6, "name": "AmountNotPositive", "description": "Amount must be positive." }, - { "code": 7, "name": "ExceedsMaxDeduct", "description": "Deduct amount exceeds the configured maximum." }, - { "code": 8, "name": "BelowMinDeposit", "description": "Deposit amount is below the configured minimum." }, - { "code": 9, "name": "Overflow", "description": "Arithmetic overflow detected." }, - { "code": 10, "name": "InitialBalanceNegative", "description": "Initial balance must be non-negative." }, - { "code": 11, "name": "MinDepositNotPositive", "description": "Min deposit must be positive." }, - { "code": 12, "name": "MaxDeductNotPositive", "description": "Max deduct must be positive." }, - { "code": 13, "name": "MinDepositExceedsMaxDeduct", "description": "Min deposit cannot exceed max deduct." }, - { "code": 14, "name": "UsdcTokenCannotBeVault", "description": "USDC token address cannot be the vault address." }, - { "code": 15, "name": "RevenuePoolCannotBeVault", "description": "Revenue pool address cannot be the vault address." }, - { "code": 16, "name": "AuthorizedCallerCannotBeVault", "description": "Authorized caller address cannot be the vault address." }, - { "code": 17, "name": "InitialBalanceExceedsOnLedger", "description": "Initial balance exceeds on-ledger USDC balance." }, - { "code": 18, "name": "AlreadyPaused", "description": "Vault is already paused." }, - { "code": 19, "name": "NotPaused", "description": "Vault is not paused." }, - { "code": 20, "name": "SettlementNotSet", "description": "Settlement address has not been configured." }, - { "code": 21, "name": "BatchEmpty", "description": "Batch deduct requires at least one item." }, - { "code": 22, "name": "BatchTooLarge", "description": "Batch size exceeds maximum allowed." }, - { "code": 23, "name": "NewOwnerSameAsCurrent", "description": "New owner must be different from current owner." }, - { "code": 24, "name": "NoOwnershipTransferPending", "description": "No ownership transfer is pending." }, - { "code": 25, "name": "NoAdminTransferPending", "description": "No admin transfer is pending." }, - { "code": 26, "name": "OfferingIdTooLong", "description": "Offering ID exceeds maximum length." }, - { "code": 27, "name": "MetadataTooLong", "description": "Metadata exceeds maximum length." }, - { "code": 28, "name": "PriceParseError", "description": "Price parsing error or non‑positive price." }, - { "code": 29, "name": "DuplicateRequestId", "description": "Duplicate request ID detected; this request_id has already been processed." } + { + "code": 1, + "name": "NotInitialized", + "description": "Vault has not been initialized yet." + }, + { + "code": 2, + "name": "AlreadyInitialized", + "description": "Vault has already been initialized." + }, + { + "code": 3, + "name": "Unauthorized", + "description": "Caller is not authorized for this operation." + }, + { + "code": 4, + "name": "Paused", + "description": "Vault is currently paused." + }, + { + "code": 5, + "name": "InsufficientBalance", + "description": "Insufficient balance for the requested operation." + }, + { + "code": 6, + "name": "AmountNotPositive", + "description": "Amount must be positive." + }, + { + "code": 7, + "name": "ExceedsMaxDeduct", + "description": "Deduct amount exceeds the configured maximum." + }, + { + "code": 8, + "name": "BelowMinDeposit", + "description": "Deposit amount is below the configured minimum." + }, + { + "code": 9, + "name": "Overflow", + "description": "Arithmetic overflow detected." + }, + { + "code": 10, + "name": "InitialBalanceNegative", + "description": "Initial balance must be non-negative." + }, + { + "code": 11, + "name": "MinDepositNotPositive", + "description": "Min deposit must be positive." + }, + { + "code": 12, + "name": "MaxDeductNotPositive", + "description": "Max deduct must be positive." + }, + { + "code": 13, + "name": "MinDepositExceedsMaxDeduct", + "description": "Min deposit cannot exceed max deduct." + }, + { + "code": 14, + "name": "UsdcTokenCannotBeVault", + "description": "USDC token address cannot be the vault address." + }, + { + "code": 15, + "name": "RevenuePoolCannotBeVault", + "description": "Revenue pool address cannot be the vault address." + }, + { + "code": 16, + "name": "AuthorizedCallerCannotBeVault", + "description": "Authorized caller address cannot be the vault address." + }, + { + "code": 17, + "name": "InitialBalanceExceedsOnLedger", + "description": "Initial balance exceeds on-ledger USDC balance." + }, + { + "code": 18, + "name": "AlreadyPaused", + "description": "Vault is already paused." + }, + { + "code": 19, + "name": "NotPaused", + "description": "Vault is not paused." + }, + { + "code": 20, + "name": "SettlementNotSet", + "description": "Settlement address has not been configured." + }, + { + "code": 21, + "name": "BatchEmpty", + "description": "Batch deduct requires at least one item." + }, + { + "code": 22, + "name": "BatchTooLarge", + "description": "Batch size exceeds maximum allowed." + }, + { + "code": 23, + "name": "NewOwnerSameAsCurrent", + "description": "New owner must be different from current owner." + }, + { + "code": 24, + "name": "NoOwnershipTransferPending", + "description": "No ownership transfer is pending." + }, + { + "code": 25, + "name": "NoAdminTransferPending", + "description": "No admin transfer is pending." + }, + { + "code": 26, + "name": "OfferingIdTooLong", + "description": "Offering ID exceeds maximum length." + }, + { + "code": 27, + "name": "MetadataTooLong", + "description": "Metadata exceeds maximum length." + }, + { + "code": 28, + "name": "PriceParseError", + "description": "Price parsing error or non‑positive price." + }, + { + "code": 29, + "name": "DuplicateRequestId", + "description": "Duplicate request ID detected; this request_id has already been processed." + } ] }, @@ -44,17 +160,35 @@ "VaultMeta": { "description": "On-chain vault state returned by init and get_meta.", "fields": { - "owner": { "type": "Address", "description": "Vault owner; always permitted to deposit and withdraw." }, - "balance": { "type": "i128", "description": "Tracked USDC balance in token base units (stroops)." }, - "authorized_caller": { "type": "Address | null", "description": "Address permitted to call deduct/batch_deduct. Null means only the owner may deduct." }, - "min_deposit": { "type": "i128", "description": "Minimum accepted deposit amount. Defaults to 1." } + "owner": { + "type": "Address", + "description": "Vault owner; always permitted to deposit and withdraw." + }, + "balance": { + "type": "i128", + "description": "Tracked USDC balance in token base units (stroops)." + }, + "authorized_caller": { + "type": "Address | null", + "description": "Address permitted to call deduct/batch_deduct. Null means only the owner may deduct." + }, + "min_deposit": { + "type": "i128", + "description": "Minimum accepted deposit amount. Defaults to 1." + } } }, "DeductItem": { "description": "Single entry in a batch_deduct call.", "fields": { - "amount": { "type": "i128", "description": "Amount to deduct in token base units; must be > 0." }, - "request_id": { "type": "Symbol | null", "description": "Optional tracking identifier. When provided it is enforced as a single-use idempotency key." } + "amount": { + "type": "i128", + "description": "Amount to deduct in token base units; must be > 0." + }, + "request_id": { + "type": "Symbol | null", + "description": "Optional tracking identifier. When provided it is enforced as a single-use idempotency key." + } } } }, @@ -65,13 +199,48 @@ "description": "Initialize the vault. Can only be called once. Owner must sign the transaction.", "access": "owner (must sign)", "params": [ - { "name": "owner", "type": "Address", "optional": false, "description": "Vault owner address; must authorize." }, - { "name": "usdc_token", "type": "Address", "optional": false, "description": "USDC token contract address (must not equal the vault address)." }, - { "name": "initial_balance", "type": "i128 | null", "optional": true, "description": "Pre-seeded tracked balance. USDC must already be held by the vault. Defaults to 0." }, - { "name": "authorized_caller", "type": "Address | null", "optional": true, "description": "Address allowed to trigger deductions. Defaults to none (owner only)." }, - { "name": "min_deposit", "type": "i128 | null", "optional": true, "description": "Minimum per-deposit amount. Defaults to 1." }, - { "name": "revenue_pool", "type": "Address | null", "optional": true, "description": "Optional informational revenue-pool contract address. Must not equal vault address." }, - { "name": "max_deduct", "type": "i128 | null", "optional": true, "description": "Maximum amount per single deduct call. Defaults to i128::MAX (no cap). Must be positive and >= min_deposit." } + { + "name": "owner", + "type": "Address", + "optional": false, + "description": "Vault owner address; must authorize." + }, + { + "name": "usdc_token", + "type": "Address", + "optional": false, + "description": "USDC token contract address (must not equal the vault address)." + }, + { + "name": "initial_balance", + "type": "i128 | null", + "optional": true, + "description": "Pre-seeded tracked balance. USDC must already be held by the vault. Defaults to 0." + }, + { + "name": "authorized_caller", + "type": "Address | null", + "optional": true, + "description": "Address allowed to trigger deductions. Defaults to none (owner only)." + }, + { + "name": "min_deposit", + "type": "i128 | null", + "optional": true, + "description": "Minimum per-deposit amount. Defaults to 1." + }, + { + "name": "revenue_pool", + "type": "Address | null", + "optional": true, + "description": "Optional informational revenue-pool contract address. Must not equal vault address." + }, + { + "name": "max_deduct", + "type": "i128 | null", + "optional": true, + "description": "Maximum amount per single deduct call. Defaults to i128::MAX (no cap). Must be positive and >= min_deposit." + } ], "returns": "VaultMeta", "panics": [ @@ -83,17 +252,25 @@ "\"usdc_token cannot be vault address\" — self-reference guard.", "\"revenue_pool cannot be vault address\" — self-reference guard." ], - "events": [ - { "topics": ["\"init\"", "owner"], "data": "balance (i128)" } - ] + "events": [{ "topics": ["\"init\"", "owner"], "data": "balance (i128)" }] }, { "name": "deposit", "description": "Transfer USDC from depositor into vault and increase tracked balance. Blocked when paused.", "access": "owner OR allowed depositor", "params": [ - { "name": "depositor", "type": "Address", "optional": false, "description": "Must be owner or an allowed depositor; must authorize." }, - { "name": "amount", "type": "i128", "optional": false, "description": "Amount to deposit; must be > 0 and >= min_deposit." } + { + "name": "depositor", + "type": "Address", + "optional": false, + "description": "Must be owner or an allowed depositor; must authorize." + }, + { + "name": "amount", + "type": "i128", + "optional": false, + "description": "Amount to deposit; must be > 0 and >= min_deposit." + } ], "returns": "i128 (new balance)", "panics": [ @@ -104,7 +281,10 @@ "\"balance overflow\" — extremely unlikely arithmetic overflow." ], "events": [ - { "topics": ["\"deposit\"", "depositor"], "data": "(amount, new_balance) tuple" } + { + "topics": ["\"deposit\"", "depositor"], + "data": "(amount, new_balance) tuple" + } ] }, { @@ -112,9 +292,24 @@ "description": "Store off‑chain price for an offering. Owner‑only. Offering ID limited to 64 characters. Price must be a positive integer string.", "access": "owner", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Vault owner; must authorize." }, - { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier; max 64 characters." }, - { "name": "price", "type": "String", "optional": false, "description": "Price value as string; must parse to a positive i128." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Vault owner; must authorize." + }, + { + "name": "offering_id", + "type": "String", + "optional": false, + "description": "Offering identifier; max 64 characters." + }, + { + "name": "price", + "type": "String", + "optional": false, + "description": "Price value as string; must parse to a positive i128." + } ], "returns": "void", "panics": [ @@ -123,7 +318,10 @@ "\"price parse error\" — price cannot be parsed to a positive integer." ], "events": [ - { "topics": ["\"price_set\"", "caller", "offering_id"], "data": "price (String)" } + { + "topics": ["\"price_set\"", "caller", "offering_id"], + "data": "price (String)" + } ] }, { @@ -131,7 +329,12 @@ "description": "Retrieve stored price for an offering. Returns null if not set.", "access": "any", "params": [ - { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier to look up." } + { + "name": "offering_id", + "type": "String", + "optional": false, + "description": "Offering identifier to look up." + } ], "returns": "String | null", "panics": [], @@ -142,9 +345,24 @@ "description": "Store off-chain metadata (e.g. IPFS CID) for an offering. Owner-only. offering_id max 64 chars; metadata max 256 chars.", "access": "owner", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the vault owner; must authorize." }, - { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier; max 64 characters." }, - { "name": "metadata", "type": "String", "optional": false, "description": "Metadata value (e.g. IPFS CID or URI); max 256 characters." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the vault owner; must authorize." + }, + { + "name": "offering_id", + "type": "String", + "optional": false, + "description": "Offering identifier; max 64 characters." + }, + { + "name": "metadata", + "type": "String", + "optional": false, + "description": "Metadata value (e.g. IPFS CID or URI); max 256 characters." + } ], "returns": "String (stored metadata)", "panics": [ @@ -153,7 +371,10 @@ "\"metadata exceeds max length\" — metadata.len() > 256." ], "events": [ - { "topics": ["\"metadata_set\"", "offering_id", "caller"], "data": "metadata (String)" } + { + "topics": ["\"metadata_set\"", "offering_id", "caller"], + "data": "metadata (String)" + } ] }, { @@ -161,7 +382,12 @@ "description": "Retrieve stored offering metadata. Returns null if not set.", "access": "any", "params": [ - { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier to look up." } + { + "name": "offering_id", + "type": "String", + "optional": false, + "description": "Offering identifier to look up." + } ], "returns": "String | null", "panics": [], @@ -172,12 +398,15 @@ "description": "Utility: panic with 'unauthorized: owner only' if caller is not the vault owner. Exposed publicly so external contracts can use it as a guard.", "access": "any (panics if caller != owner)", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Address to validate as owner." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Address to validate as owner." + } ], "returns": "void", - "panics": [ - "\"unauthorized: owner only\" — caller is not the owner." - ], + "panics": ["\"unauthorized: owner only\" — caller is not the owner."], "events": [] }, @@ -186,9 +415,24 @@ "description": "Deduct USDC for a single API call. Decreases the tracked balance and forwards funds to the configured settlement contract. Blocked when paused.", "access": "owner OR authorized_caller", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the owner or the authorized_caller; must authorize." }, - { "name": "amount", "type": "i128", "optional": false, "description": "Amount to deduct; must be > 0 and <= max_deduct." }, - { "name": "request_id", "type": "Symbol | null", "optional": true, "description": "Optional tracking key emitted in the event. When provided it must be unique across successful deductions." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the owner or the authorized_caller; must authorize." + }, + { + "name": "amount", + "type": "i128", + "optional": false, + "description": "Amount to deduct; must be > 0 and <= max_deduct." + }, + { + "name": "request_id", + "type": "Symbol | null", + "optional": true, + "description": "Optional tracking key emitted in the event. When provided it must be unique across successful deductions." + } ], "returns": "i128 (new balance)", "panics": [ @@ -199,7 +443,10 @@ "\"insufficient balance\" — balance < amount." ], "events": [ - { "topics": ["\"deduct\"", "caller", "request_id (or empty Symbol)"], "data": "(amount, new_balance) tuple" } + { + "topics": ["\"deduct\"", "caller", "request_id (or empty Symbol)"], + "data": "(amount, new_balance) tuple" + } ] }, @@ -208,8 +455,18 @@ "description": "Atomically deduct multiple amounts. The entire batch is validated before any state change. One event is emitted per item. Blocked when paused.", "access": "owner OR authorized_caller", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the owner or the authorized_caller; must authorize." }, - { "name": "items", "type": "Vec", "optional": false, "description": "Ordered list of deductions. Must be non-empty and within MAX_BATCH_SIZE." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the owner or the authorized_caller; must authorize." + }, + { + "name": "items", + "type": "Vec", + "optional": false, + "description": "Ordered list of deductions. Must be non-empty and within MAX_BATCH_SIZE." + } ], "returns": "i128 (final balance after all deductions)", "panics": [ @@ -221,7 +478,11 @@ "\"insufficient balance\" — cumulative deductions exceed current balance." ], "events": [ - { "note": "One event per item in order.", "topics": ["\"deduct\"", "caller", "request_id (or empty Symbol)"], "data": "(item.amount, running_balance) tuple" } + { + "note": "One event per item in order.", + "topics": ["\"deduct\"", "caller", "request_id (or empty Symbol)"], + "data": "(item.amount, running_balance) tuple" + } ] }, @@ -230,7 +491,12 @@ "description": "Transfer USDC from the vault to the owner. Allowed while paused (recovery path).", "access": "owner (must sign)", "params": [ - { "name": "amount", "type": "i128", "optional": false, "description": "Amount to withdraw; must be > 0 and <= balance." } + { + "name": "amount", + "type": "i128", + "optional": false, + "description": "Amount to withdraw; must be > 0 and <= balance." + } ], "returns": "i128 (new balance)", "panics": [ @@ -245,8 +511,18 @@ "description": "Transfer USDC from the vault to an arbitrary recipient. Allowed while paused (recovery path).", "access": "owner (must sign)", "params": [ - { "name": "to", "type": "Address", "optional": false, "description": "Recipient address." }, - { "name": "amount", "type": "i128", "optional": false, "description": "Amount to withdraw; must be > 0 and <= balance." } + { + "name": "to", + "type": "Address", + "optional": false, + "description": "Recipient address." + }, + { + "name": "amount", + "type": "i128", + "optional": false, + "description": "Amount to withdraw; must be > 0 and <= balance." + } ], "returns": "i128 (new balance)", "panics": [ @@ -261,9 +537,24 @@ "description": "Admin-only: transfer USDC held by the vault to a developer address. Allowed while paused (recovery path).", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the current admin; must authorize." }, - { "name": "to", "type": "Address", "optional": false, "description": "Recipient address." }, - { "name": "amount", "type": "i128", "optional": false, "description": "Amount to transfer; must be > 0 and <= vault USDC balance." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the current admin; must authorize." + }, + { + "name": "to", + "type": "Address", + "optional": false, + "description": "Recipient address." + }, + { + "name": "amount", + "type": "i128", + "optional": false, + "description": "Amount to transfer; must be > 0 and <= vault USDC balance." + } ], "returns": "void", "panics": [ @@ -282,9 +573,7 @@ "access": "any", "params": [], "returns": "i128", - "panics": [ - "\"vault not initialized\" — called before init." - ], + "panics": ["\"vault not initialized\" — called before init."], "events": [] }, @@ -294,9 +583,7 @@ "access": "any", "params": [], "returns": "VaultMeta", - "panics": [ - "\"vault not initialized\" — called before init." - ], + "panics": ["\"vault not initialized\" — called before init."], "events": [] }, @@ -325,16 +612,19 @@ "description": "Activate the circuit breaker. Blocks deposit, deduct, and batch_deduct.", "access": "admin OR owner", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be admin or owner; must authorize." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be admin or owner; must authorize." + } ], "returns": "void", "panics": [ "\"unauthorized: caller is not admin or owner\" — auth failure.", "\"vault already paused\" — already in paused state." ], - "events": [ - { "topics": ["\"vault_paused\"", "caller"], "data": "void" } - ] + "events": [{ "topics": ["\"vault_paused\"", "caller"], "data": "void" }] }, { @@ -342,16 +632,19 @@ "description": "Deactivate the circuit breaker. Restores deposit, deduct, and batch_deduct.", "access": "admin OR owner", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be admin or owner; must authorize." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be admin or owner; must authorize." + } ], "returns": "void", "panics": [ "\"unauthorized: caller is not admin or owner\" — auth failure.", "\"vault not paused\" — not currently paused." ], - "events": [ - { "topics": ["\"vault_unpaused\"", "caller"], "data": "void" } - ] + "events": [{ "topics": ["\"vault_unpaused\"", "caller"], "data": "void" }] }, { @@ -359,13 +652,21 @@ "description": "Add or clear the set of allowed depositors. Pass Some(address) to add; None to revoke all.", "access": "owner", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the vault owner; must authorize." }, - { "name": "depositor", "type": "Address | null", "optional": false, "description": "Address to add, or null to clear all allowed depositors." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the vault owner; must authorize." + }, + { + "name": "depositor", + "type": "Address | null", + "optional": false, + "description": "Address to add, or null to clear all allowed depositors." + } ], "returns": "void", - "panics": [ - "\"unauthorized: owner only\" — caller is not the owner." - ], + "panics": ["\"unauthorized: owner only\" — caller is not the owner."], "events": [] }, @@ -374,7 +675,12 @@ "description": "Return true if the given address is permitted to deposit (owner or allowed depositor).", "access": "any", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Address to check." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Address to check." + } ], "returns": "bool", "panics": [], @@ -386,14 +692,20 @@ "description": "Set or clear the address permitted to trigger deductions. Replaces any previously set value.", "access": "owner (must sign)", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "New authorized caller address; also used to authorize the call (must be current owner)." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "New authorized caller address; also used to authorize the call (must be current owner)." + } ], "returns": "void", - "panics": [ - "\"vault not initialized\" — called before init." - ], + "panics": ["\"vault not initialized\" — called before init."], "events": [ - { "topics": ["\"set_authorized_caller\"", "owner"], "data": "(old_authorized_caller, new_authorized_caller) tuple" } + { + "topics": ["\"set_authorized_caller\"", "owner"], + "data": "(old_authorized_caller, new_authorized_caller) tuple" + } ] }, @@ -402,7 +714,12 @@ "description": "Nominate a new vault owner. The nominee must call accept_ownership to finalize.", "access": "owner (must sign)", "params": [ - { "name": "new_owner", "type": "Address", "optional": false, "description": "Proposed new owner; must be different from the current owner." } + { + "name": "new_owner", + "type": "Address", + "optional": false, + "description": "Proposed new owner; must be different from the current owner." + } ], "returns": "void", "panics": [ @@ -410,7 +727,10 @@ "\"vault not initialized\" — called before init." ], "events": [ - { "topics": ["\"ownership_nominated\"", "current_owner", "new_owner"], "data": "void" } + { + "topics": ["\"ownership_nominated\"", "current_owner", "new_owner"], + "data": "void" + } ] }, @@ -424,7 +744,10 @@ "\"no ownership transfer pending\" — transfer_ownership was not called first." ], "events": [ - { "topics": ["\"ownership_accepted\"", "old_owner", "new_owner"], "data": "void" } + { + "topics": ["\"ownership_accepted\"", "old_owner", "new_owner"], + "data": "void" + } ] }, @@ -434,9 +757,7 @@ "access": "any", "params": [], "returns": "Address", - "panics": [ - "\"vault not initialized\" — called before init." - ], + "panics": ["\"vault not initialized\" — called before init."], "events": [] }, @@ -445,15 +766,28 @@ "description": "Nominate a new admin. The nominee must call accept_admin to finalize.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be current admin; must authorize." }, - { "name": "new_admin", "type": "Address", "optional": false, "description": "Proposed new admin address." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be current admin; must authorize." + }, + { + "name": "new_admin", + "type": "Address", + "optional": false, + "description": "Proposed new admin address." + } ], "returns": "void", "panics": [ "\"unauthorized: caller is not admin\" — caller != current admin." ], "events": [ - { "topics": ["\"admin_nominated\"", "current_admin", "new_admin"], "data": "void" } + { + "topics": ["\"admin_nominated\"", "current_admin", "new_admin"], + "data": "void" + } ] }, @@ -467,7 +801,10 @@ "\"no admin transfer pending\" — set_admin was not called first." ], "events": [ - { "topics": ["\"admin_accepted\"", "old_admin", "new_admin"], "data": "void" } + { + "topics": ["\"admin_accepted\"", "old_admin", "new_admin"], + "data": "void" + } ] }, @@ -476,8 +813,18 @@ "description": "Configure the settlement contract address. Deducted USDC is always forwarded here during deduct/batch_deduct.", "access": "admin", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be current admin; must authorize." }, - { "name": "settlement_address", "type": "Address", "optional": false, "description": "Settlement contract address." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be current admin; must authorize." + }, + { + "name": "settlement_address", + "type": "Address", + "optional": false, + "description": "Settlement contract address." + } ], "returns": "void", "panics": [ @@ -503,9 +850,24 @@ "description": "Store off-chain metadata (e.g. IPFS CID) for an offering. Owner-only. offering_id max 64 chars; metadata max 256 chars.", "access": "owner", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the vault owner; must authorize." }, - { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier; max 64 characters." }, - { "name": "metadata", "type": "String", "optional": false, "description": "Metadata value (e.g. IPFS CID or URI); max 256 characters." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the vault owner; must authorize." + }, + { + "name": "offering_id", + "type": "String", + "optional": false, + "description": "Offering identifier; max 64 characters." + }, + { + "name": "metadata", + "type": "String", + "optional": false, + "description": "Metadata value (e.g. IPFS CID or URI); max 256 characters." + } ], "returns": "String (stored metadata)", "panics": [ @@ -514,7 +876,10 @@ "\"metadata exceeds max length\" — metadata.len() > 256." ], "events": [ - { "topics": ["\"metadata_set\"", "offering_id", "caller"], "data": "metadata (String)" } + { + "topics": ["\"metadata_set\"", "offering_id", "caller"], + "data": "metadata (String)" + } ] }, @@ -523,7 +888,12 @@ "description": "Retrieve stored offering metadata. Returns null if not set.", "access": "any", "params": [ - { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier to look up." } + { + "name": "offering_id", + "type": "String", + "optional": false, + "description": "Offering identifier to look up." + } ], "returns": "String | null", "panics": [], @@ -542,7 +912,7 @@ { "name": "get_pending_admin", - "description": "Return the pending admin address, or null if no admin transfer is in progress.", + "description": "Return the pending admin address, or null if no two-step transfer is in progress. Integrators can poll this to detect an in-flight admin handover before accept_admin is called.", "access": "any", "params": [], "returns": "Address | null", @@ -555,9 +925,24 @@ "description": "Overwrite existing offering metadata. Owner-only. Emits old and new values. offering_id max 64 chars; metadata max 256 chars.", "access": "owner", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Must be the vault owner; must authorize." }, - { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier; max 64 characters." }, - { "name": "metadata", "type": "String", "optional": false, "description": "New metadata value; max 256 characters." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Must be the vault owner; must authorize." + }, + { + "name": "offering_id", + "type": "String", + "optional": false, + "description": "Offering identifier; max 64 characters." + }, + { + "name": "metadata", + "type": "String", + "optional": false, + "description": "New metadata value; max 256 characters." + } ], "returns": "String (new metadata)", "panics": [ @@ -566,7 +951,10 @@ "\"metadata exceeds max length\" — metadata.len() > 256." ], "events": [ - { "topics": ["\"metadata_updated\"", "offering_id", "caller"], "data": "(old_metadata, new_metadata) tuple" } + { + "topics": ["\"metadata_updated\"", "offering_id", "caller"], + "data": "(old_metadata, new_metadata) tuple" + } ] }, @@ -575,15 +963,16 @@ "description": "Utility: panic with 'unauthorized: owner only' if caller is not the vault owner. Exposed publicly so external contracts can use it as a guard.", "access": "any (panics if caller != owner)", "params": [ - { "name": "caller", "type": "Address", "optional": false, "description": "Address to validate as owner." } + { + "name": "caller", + "type": "Address", + "optional": false, + "description": "Address to validate as owner." + } ], "returns": "void", - "panics": [ - "\"unauthorized: owner only\" — caller is not the owner." - ], + "panics": ["\"unauthorized: owner only\" — caller is not the owner."], "events": [] } ] } - -