diff --git a/campaign/src/lib.rs b/campaign/src/lib.rs index a83ffc7..b225021 100644 --- a/campaign/src/lib.rs +++ b/campaign/src/lib.rs @@ -790,6 +790,7 @@ pub fn validate_milestone_transition( #[cfg(test)] mod test { pub mod claim_refund_tests; + pub mod error_discriminant_tests; pub mod get_campaign_status_tests; pub mod integration_tests; pub mod invariant_tests; diff --git a/campaign/src/test/error_discriminant_tests.rs b/campaign/src/test/error_discriminant_tests.rs new file mode 100644 index 0000000..6c3f2a2 --- /dev/null +++ b/campaign/src/test/error_discriminant_tests.rs @@ -0,0 +1,72 @@ +use crate::types::Error; +use common::ErrorCode; + +#[test] +fn campaign_and_common_error_discriminants_do_not_collide() { + let campaign_codes = [ + Error::AlreadyInitialized as u32, + Error::NotInitialized as u32, + Error::Unauthorized as u32, + Error::CampaignEnded as u32, + Error::CampaignNotActive as u32, + Error::AssetNotAccepted as u32, + Error::DonationTooSmall as u32, + Error::MilestoneNotFound as u32, + Error::MilestoneNotUnlocked as u32, + Error::PreviousMilestoneNotReleased as u32, + Error::CannotCancelWithFunds as u32, + Error::RefundWindowClosed as u32, + Error::InvalidGoalAmount as u32, + Error::InvalidEndTime as u32, + Error::InvalidMilestones as u32, + Error::InsufficientContractBalance as u32, + Error::Overflow as u32, + Error::InvalidAssets as u32, + Error::InvalidAssetCode as u32, + Error::MilestoneMismatch as u32, + Error::InvalidMilestoneCount as u32, + Error::InvalidCampaignTransition as u32, + Error::InvalidMilestoneTransition as u32, + Error::GoalNotReached as u32, + Error::InvalidStorageValue as u32, + Error::StorageWriteError as u32, + Error::InvalidRecipient as u32, + Error::MissingIssuerAddress as u32, + Error::ZeroReleaseAmount as u32, + Error::NothingToRelease as u32, + Error::MilestoneReleasedExceedsTarget as u32, + Error::MilestoneAlreadyReleased as u32, + Error::UnreleasedMilestonesExist as u32, + Error::RefundNotPermitted as u32, + Error::NoDonorRecord as u32, + Error::RefundAlreadyClaimed as u32, + Error::ReentrantCall as u32, + Error::InvalidAmount as u32, + Error::ContractFrozen as u32, + ]; + + let common_codes = [ + ErrorCode::NotInitialized as u32, + ErrorCode::AlreadyInitialized as u32, + ErrorCode::Unauthorized as u32, + ErrorCode::InvalidAmount as u32, + ]; + + for common_code in common_codes { + assert!( + (1000..=1099).contains(&common_code), + "common error code {common_code} must stay in the shared 1000..=1099 namespace" + ); + assert!( + !campaign_codes.contains(&common_code), + "common error code {common_code} collides with campaign::Error" + ); + } + + for campaign_code in campaign_codes { + assert!( + campaign_code < 1000, + "campaign error code {campaign_code} must stay below the shared common namespace" + ); + } +} diff --git a/campaign/src/types.rs b/campaign/src/types.rs index c70eac7..4788659 100644 --- a/campaign/src/types.rs +++ b/campaign/src/types.rs @@ -9,6 +9,10 @@ use soroban_sdk::{contracterror, contracttype, Address, BytesN, Env, String, Vec /// Codes are stable — never renumber an existing variant; only append new ones. /// Each code maps to a `u32` via `contracterror` and is surfaced in transaction /// results as `Error(Contract, #N)`. +/// +/// Campaign owns the `1..=999` contract-local error namespace. Shared workspace +/// errors from `common::ErrorCode` are reserved for `1000..=1099`; keep the two +/// ranges disjoint whenever a new variant is added. #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Error { diff --git a/common/src/lib.rs b/common/src/lib.rs index a41e8bf..938399f 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,10 +1,13 @@ //! Common types shared across the OrbitChain workspace. //! //! This crate provides canonical definitions for `CampaignStatus`, `MilestoneStatus`, -//! `AssetInfo`, and `ErrorCode` used by both campaign and core contracts. +//! `AssetInfo`, and the shared error-code range used by both campaign and core +//! contracts. //! //! # Versioning -//! All discriminants are stable — never renumber existing variants. +//! All discriminants are stable — never renumber existing variants. Shared +//! workspace errors must stay in the `1000..=1099` range so they cannot collide +//! with contract-local error enums such as `campaign::types::Error`. #![no_std] use soroban_sdk::{contracterror, contracttype}; @@ -44,11 +47,11 @@ pub struct AssetInfo { #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ErrorCode { /// Contract has not been initialized yet. - NotInitialized = 1, + NotInitialized = 1000, /// Contract has already been initialized. - AlreadyInitialized = 2, + AlreadyInitialized = 1001, /// Caller is not authorized to perform this operation. - Unauthorized = 3, + Unauthorized = 1002, /// The amount supplied is invalid (zero, negative, or out of range). - InvalidAmount = 4, + InvalidAmount = 1003, } diff --git a/docs/deployment.md b/docs/deployment.md index e0c9b8b..9b6ab66 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -47,6 +47,18 @@ timestamp. This prevents accidental or malicious `u64`-scale future dates from making status views, refund-window checks, milestone release arithmetic, and campaign reports meaningless while still allowing long-running campaigns. +## Error-Code Migration Note + +`campaign::types::Error` owns the contract-local `1..=999` error namespace. +`common::ErrorCode` owns the shared workspace `1000..=1099` namespace. This +keeps `Error(Contract, #N)` values unambiguous for off-chain indexers and any +future crate that imports both enums. + +If a deployed integration previously interpreted `common::ErrorCode` values as +`1..=4`, migrate that integration before it consumes the shared crate again: +`NotInitialized=1000`, `AlreadyInitialized=1001`, `Unauthorized=1002`, and +`InvalidAmount=1003`. Campaign contract error values are unchanged. + ## Troubleshooting - **`InsufficientFee`**: Add `--fee 1000000` to the deploy command.