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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]

### Features
- CORS configuration for cross-origin API access
- Add canonical `VaultError` namespace module and replace contract panics with stable error codes (#754)
- Structured logging, graceful shutdown, caching, and API key authentication
- Network badge showing testnet vs mainnet status in the frontend
- Add wallet activity heatmap aggregation endpoint for admin analytics without exposing raw wallet records (#712)
Expand Down
125 changes: 125 additions & 0 deletions contracts/vault/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Canonical on-chain error namespace for YieldVault.
//!
//! All user-facing failure paths must return [`VaultError`] rather than panicking.
//! Numeric codes are stable across contract versions; integrators should map them
//! via `docs/api/ERROR_CODE_CATALOG.md`.

use soroban_sdk::contracterror;

/// Core vault contract errors (codes 1–99).
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum VaultError {
// ── Core operations (1–24) ───────────────────────────────────────────────
/// Contract has already been initialized.
AlreadyInitialized = 1,
/// User does not have enough shares to withdraw.
InsufficientShares = 2,
/// Amount is invalid (zero or negative).
InvalidAmount = 3,
/// Vault is paused; deposits and withdrawals are blocked.
ContractPaused = 4,
/// Deposit would exceed per-user cap.
ExceedsUserCap = 5,
/// Deposit is below minimum deposit threshold.
MinDepositNotMet = 6,
/// Large withdrawal timelock has not expired yet.
TimelockNotExpired = 7,
/// No pending withdrawal exists for this user.
NoPendingWithdrawal = 8,
/// Strategy allocation would leave idle liquidity below the configured buffer.
LiquidityBufferNotMet = 9,
/// Strategy allocation exceeds configured cap.
ExceedsStrategyCap = 10,
/// Strategy allocation exceeds configured risk threshold.
ExceedsRiskThreshold = 11,
/// Withdrawal blocked due to active deposit cooldown.
WithdrawalCooldownActive = 12,
/// Requested storage migration target is older than the current stored version.
InvalidMigrationTarget = 13,
/// Arithmetic overflow was detected before mutating state.
MathOverflow = 14,
/// Strategy operation exceeded maximum allowed slippage.
SlippageExceeded = 15,
/// Batch deposit entries vector exceeds the maximum allowed size.
BatchTooLarge = 16,
/// Caller is not a registered relayer and cannot submit batch deposits.
RelayerNotAuthorized = 17,
/// Emergency proposal is still within the dispute window and cannot be confirmed yet.
DisputeWindowActive = 18,
/// Emergency proposal has been cancelled and cannot be confirmed or executed.
ProposalCancelled = 19,
/// Dispute window has already closed; the proposal can no longer be cancelled.
DisputeWindowClosed = 20,
/// Withdrawal was queued because idle liquidity was insufficient.
WithdrawalQueued = 21,
/// Admin parameter change attempted before the minimum interval elapsed.
AdminParamChangeTooSoon = 22,
/// No strategy has been configured on the vault.
StrategyNotConfigured = 23,
/// Vault does not have enough idle liquidity to satisfy the operation.
InsufficientLiquidity = 24,

// ── Governance (25–26, 30–36) ────────────────────────────────────────────
/// Governance signers are not configured.
GovernanceSignersNotConfigured = 25,
/// Governance signature threshold was not met.
GovernanceThresholdNotMet = 26,
/// DAO or admin threshold must be greater than zero.
InvalidDaoThreshold = 30,
/// Governance signer threshold is outside the valid range.
InvalidGovernanceThreshold = 31,
/// Vote weight must be greater than zero.
InvalidVoteWeight = 32,
/// Voter has already cast a ballot on this proposal.
DuplicateVote = 33,
/// Proposal has already been executed.
ProposalAlreadyExecuted = 34,
/// Proposal has not reached the required quorum.
QuorumNotReached = 35,
/// Proposal was rejected (no votes exceed against votes).
ProposalRejected = 36,

// ── Oracle / treasury / strategy health (27–29, 37) ──────────────────────
/// Oracle validation failed (stale or manipulated price).
OracleValidationFailed = 27,
/// Treasury claim quota exceeded for the current epoch.
ClaimQuotaExceeded = 28,
/// Strategy heartbeat expired; allocation operations are blocked.
StrategyHeartbeatExpired = 29,
/// Caller is not the configured or whitelisted strategy.
UnauthorizedStrategy = 37,

// ── Admin configuration (38–42) ────────────────────────────────────────
/// Protocol fee basis points are outside 0–10000.
InvalidFeeBps = 38,
/// No protocol fees are available to claim.
NoFeesToClaim = 39,
/// Minimum deposit parameter is negative.
InvalidMinDeposit = 40,
/// Minimum liquidity buffer parameter is negative.
InvalidLiquidityBuffer = 41,
/// Risk threshold basis points are outside 0–10000.
InvalidRiskThreshold = 42,

// ── Whitelist / strategy registration (43–45) ────────────────────────────
/// Strategy address is not on the whitelist.
StrategyNotWhitelisted = 43,
/// Whitelist mutation failed.
WhitelistOperationFailed = 44,
/// Accrued yield amount must be greater than zero.
InvalidYieldAmount = 45,

// ── RWA / pagination / batch limits (46–48) ──────────────────────────────
/// Shipment identifier already exists.
ShipmentAlreadyExists = 46,
/// Page size must be greater than zero.
InvalidPageSize = 47,
/// Maximum batch size must be greater than zero.
InvalidMaxBatchSize = 48,

// ── Guard rails (49) ───────────────────────────────────────────────────
/// Opposing deposit/withdraw action in the same ledger is not allowed.
RapidAction = 49,
}
6 changes: 3 additions & 3 deletions contracts/vault/src/event_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,7 @@ fn test_claim_fees_transfers_to_treasury() {
}

#[test]
#[should_panic(expected = "no fees to claim")]
fn test_claim_fees_panics_when_balance_zero() {
fn test_claim_fees_returns_error_when_balance_zero() {
let env = Env::default();
env.mock_all_auths();

Expand All @@ -199,7 +198,8 @@ fn test_claim_fees_panics_when_balance_zero() {
vault.initialize(&admin, &usdc.address);
vault.set_treasury(&treasury);

vault.claim_fees(); // should panic
let result = vault.try_claim_fees();
assert_eq!(result, Err(Ok(VaultError::NoFeesToClaim)));
}

#[test]
Expand Down
Loading
Loading