Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion contracts/revenue_pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address> {
env.storage()
.instance()
Expand Down Expand Up @@ -732,3 +738,6 @@ mod test_invariant;

#[cfg(test)]
mod test_proptest;

#[cfg(test)]
mod test_error_codes;
14 changes: 13 additions & 1 deletion contracts/revenue_pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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()));
Expand Down
36 changes: 36 additions & 0 deletions contracts/revenue_pool/src/test_error_codes.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
45 changes: 45 additions & 0 deletions contracts/settlement/src/errors.rs
Original file line number Diff line number Diff line change
@@ -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,
}
56 changes: 9 additions & 47 deletions contracts/settlement/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,16 @@
#![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;

/// 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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1241,3 +1200,6 @@ mod test_views;

#[cfg(test)]
mod test_invariant;

#[cfg(test)]
mod test_error_codes;
62 changes: 62 additions & 0 deletions contracts/settlement/src/test_error_codes.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
117 changes: 117 additions & 0 deletions contracts/vault/src/errors.rs
Original file line number Diff line number Diff line change
@@ -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,
}
Loading
Loading