diff --git a/src/lib.rs b/src/lib.rs index cb765228..7eef2403 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,54 +10,139 @@ use soroban_sdk::{ // Issue #109 — Revenue report correction workflow with audit trail. // Placeholder branch for upstream PR scaffolding; full implementation in follow-up. -/// Centralized contract error codes. Auth failures are signaled by host panic (require_auth). +/// Centralized contract error codes. +/// +/// All state-mutating entrypoints return `Result<_, RevoraError>` so callers can +/// distinguish contract-level rejections from host-level auth panics. Use the +/// `try_*` client methods to receive these as `Result`. +/// +/// Auth failures (wrong signer) are signaled by host panic, not `RevoraError`. +/// +/// # Numeric stability +/// Each variant's discriminant is fixed and must never be renumbered; integrators +/// may store or transmit the raw `u32` value. #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[repr(u32)] pub enum RevoraError { - /// revenue_share_bps exceeded 10000 (100%). + /// `register_offering`: `revenue_share_bps` > 10 000 (100 %). + /// + /// Testnet mode bypasses this check to allow flexible testing. + /// Discriminant: 1. InvalidRevenueShareBps = 1, - /// Reserved for future use (e.g. offering limit per issuer). + + /// General guard for operations that are structurally disallowed in the + /// current contract state (e.g. admin already set, multisig already + /// initialized, threshold out of range, fee above maximum). + /// + /// Also returned by `set_platform_fee` / `set_offering_fee_bps` when + /// `fee_bps > 5 000`. + /// Discriminant: 2. LimitReached = 2, - /// Holder concentration exceeds configured limit and enforcement is enabled. + + /// `report_revenue`: the last reported single-holder concentration exceeds + /// the configured `max_bps` limit **and** `enforce` is `true`. + /// + /// Call `report_concentration` to update the stored value, or lower the + /// limit via `set_concentration_limit`. + /// Discriminant: 3. ConcentrationLimitExceeded = 3, - /// No offering found for the given (issuer, token) pair. + + /// The requested `(issuer, namespace, token)` triple has no registered + /// offering, or the caller is not the current issuer of that offering. + /// + /// Returned by any issuer-gated entrypoint when the offering lookup fails. + /// Discriminant: 4. OfferingNotFound = 4, - /// Revenue already deposited for this period. + + /// `deposit_revenue`: revenue has already been deposited for this + /// `period_id`. Each period may only be deposited once. + /// Discriminant: 5. PeriodAlreadyDeposited = 5, - /// No unclaimed periods for this holder. + + /// `claim`: the holder has no share allocated (`share_bps == 0`) or all + /// deposited periods have already been claimed. + /// Discriminant: 6. NoPendingClaims = 6, - /// Holder is blacklisted for this offering. + + /// `claim`: the holder is on the per-offering blacklist and cannot receive + /// revenue. Blacklisted holders retain their `share_bps` but cannot call + /// `claim` until removed from the blacklist. + /// Discriminant: 7. HolderBlacklisted = 7, - /// Holder share_bps exceeded 10000 (100%). + + /// `set_holder_share`: `share_bps` > 10 000 (100 %). + /// Discriminant: 8. InvalidShareBps = 8, - /// Payment token does not match previously set token for this offering. + + /// `deposit_revenue`: the supplied `payment_token` differs from the token + /// locked on the first deposit for this offering. The payment token is + /// immutable after the first deposit. + /// Discriminant: 9. PaymentTokenMismatch = 9, - /// Contract is frozen; state-changing operations are disabled. + + /// The contract is frozen; all state-mutating operations are disabled. + /// + /// Read-only queries and `claim` remain available. Unfreeze requires a + /// new deployment or multisig action (depending on configuration). + /// Discriminant: 10. ContractFrozen = 10, - /// Revenue for this period is not yet claimable (delay not elapsed). + + /// `claim`: the next claimable period has not yet passed the configured + /// `ClaimDelaySecs` window. The caller should retry after the delay + /// elapses. + /// Discriminant: 11. ClaimDelayNotElapsed = 11, - /// Snapshot distribution is not enabled for this offering. + /// `deposit_revenue_with_snapshot`: snapshot-based distribution is not + /// enabled for this offering. Call `set_snapshot_config(true)` first. + /// Discriminant: 12. SnapshotNotEnabled = 12, /// Provided snapshot reference is outdated or duplicates a previous one. /// Overriding an existing revenue report. OutdatedSnapshot = 13, /// Payout asset mismatch. PayoutAssetMismatch = 14, - /// A transfer is already pending for this offering. + + /// `propose_issuer_transfer`: a transfer is already pending for this + /// offering. Cancel the existing proposal before proposing a new one. + /// Discriminant: 15. IssuerTransferPending = 15, - /// No transfer is pending for this offering. + + /// `accept_issuer_transfer` / `cancel_issuer_transfer`: no transfer is + /// currently pending for this offering. + /// Discriminant: 16. NoTransferPending = 16, - /// Caller is not authorized to accept this transfer. + + /// `accept_issuer_transfer`: the caller is not the address that was + /// nominated as the new issuer in the pending transfer proposal. + /// + /// Security note: this is a typed error rather than a host panic so that + /// callers can distinguish "wrong acceptor" from "no pending transfer". + /// Discriminant: 17. UnauthorizedTransferAccept = 17, - /// Metadata string exceeds maximum allowed length. + + /// `set_offering_metadata`: the metadata string exceeds + /// `MAX_METADATA_LENGTH` (256 bytes). + /// Discriminant: 18. MetadataTooLarge = 18, - /// Caller is not authorized to perform this action. + + /// `meta_set_holder_share` / `meta_approve_revenue_report`: the signer is + /// not the configured delegate for this offering. + /// Discriminant: 19. NotAuthorized = 19, - /// Contract is not initialized (admin not set). + + /// A required admin address has not been set. + /// + /// Returned by `require_admin` when `DataKey::Admin` is absent. This + /// indicates the contract was not properly initialized before use. + /// Discriminant: 20. NotInitialized = 20, - /// Amount is invalid (e.g. negative for deposit, or out of allowed range) (#35). + + /// `report_revenue` / `set_min_revenue_threshold`: `amount` is negative. + /// `deposit_revenue`: `amount` <= 0. + /// `set_investment_constraints`: `min_stake` or `max_stake` is negative. + /// Discriminant: 21. InvalidAmount = 21, /// period_id is invalid (e.g. zero when required to be positive) (#35). /// period_id not strictly greater than previous (violates ordering invariant). @@ -65,17 +150,37 @@ pub enum RevoraError { /// Deposit would exceed the offering's supply cap (#96). SupplyCapExceeded = 23, - /// Metadata format is invalid for configured scheme rules. + + /// `set_offering_metadata`: the metadata string does not start with a + /// recognised scheme prefix (`ipfs://`, `https://`, `ar://`, `sha256:`). + /// Discriminant: 24. MetadataInvalidFormat = 24, - /// Current ledger timestamp is outside configured reporting window. + + /// `report_revenue`: the current ledger timestamp is outside the + /// configured reporting window for this offering. + /// Discriminant: 25. ReportingWindowClosed = 25, - /// Current ledger timestamp is outside configured claiming window. + + /// `claim`: the current ledger timestamp is outside the configured + /// claiming window for this offering. + /// Discriminant: 26. ClaimWindowClosed = 26, - /// Off-chain signature has expired. + + /// `meta_set_holder_share` / `meta_approve_revenue_report`: the + /// off-chain signature's `expiry` timestamp is in the past. + /// Discriminant: 27. SignatureExpired = 27, - /// Signature nonce has already been used. + + /// `meta_set_holder_share` / `meta_approve_revenue_report`: the nonce + /// has already been consumed. Each nonce may only be used once per + /// signer to prevent replay attacks. + /// Discriminant: 28. SignatureReplay = 28, - /// Off-chain signer key has not been registered. + + /// `meta_set_holder_share` / `meta_approve_revenue_report`: no ed25519 + /// public key has been registered for the signer address. Call + /// `register_meta_signer_key` first. + /// Discriminant: 29. SignerKeyNotRegistered = 29, /// Cross-contract token transfer failed. TransferFailed = 30, @@ -4581,6 +4686,12 @@ impl RevoraRevenueShare { } /// Accept a pending issuer transfer. Only the proposed new issuer may call this. + /// + /// # Parameters + /// - `caller`: The address attempting to accept the transfer. Must match + /// the address nominated in `propose_issuer_transfer`; otherwise returns + /// `Err(UnauthorizedTransferAccept)`. + /// - `issuer`: The current (old) issuer, used to locate the offering. pub fn accept_issuer_transfer( env: Env, caller: Address, @@ -5215,6 +5326,9 @@ impl RevoraRevenueShare { // ── Testnet mode configuration (#24) ─────────────────────── /// Enable or disable testnet mode. Only admin may call. + /// + /// Returns `Err(NotInitialized)` if the contract admin has not been set yet, + /// allowing callers to distinguish "not initialized" from other auth failures. /// When enabled, certain validations are relaxed for testnet deployments. /// Emits event with new mode state. pub fn set_testnet_mode(env: Env, enabled: bool) -> Result<(), RevoraError> { diff --git a/src/structured_error_tests.rs b/src/structured_error_tests.rs index e8730b85..e69de29b 100644 --- a/src/structured_error_tests.rs +++ b/src/structured_error_tests.rs @@ -1,456 +0,0 @@ -//! Structured Error Coverage Expansion -//! -//! One test per `RevoraError` variant (discriminants 1-30). -//! Each test triggers the exact error and asserts the discriminant is stable. -#![cfg(test)] -#![allow(warnings)] - -use crate::{RevoraError, RevoraRevenueShare, RevoraRevenueShareClient, RoundingMode}; -use soroban_sdk::{ - symbol_short, - testutils::{Address as _, Ledger as _}, - Address, Env, String as SdkString, -}; - -fn client(env: &Env) -> RevoraRevenueShareClient<'_> { - let id = env.register_contract(None, RevoraRevenueShare); - RevoraRevenueShareClient::new(env, &id) -} - -fn with_offering(env: &Env) -> (RevoraRevenueShareClient<'_>, Address, Address) { - let c = client(env); - let issuer = Address::generate(env); - let token = Address::generate(env); - c.register_offering(&issuer, &symbol_short!("ns"), &token, &500, &token, &0); - (c, issuer, token) -} - -#[test] -fn sec_error_1_invalid_revenue_share_bps() { - assert_eq!(RevoraError::InvalidRevenueShareBps as u32, 1); - let env = Env::default(); - env.mock_all_auths(); - let c = client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let r = c.try_register_offering(&issuer, &symbol_short!("ns"), &token, &10_001, &token, &0); - assert_eq!(r, Err(Ok(RevoraError::InvalidRevenueShareBps))); -} - -#[test] -fn sec_error_2_limit_reached() { - assert_eq!(RevoraError::LimitReached as u32, 2); - let env = Env::default(); - env.mock_all_auths(); - let c = client(&env); - let admin = Address::generate(&env); - c.set_admin(&admin); - let r = c.try_set_admin(&admin); - assert_eq!(r, Err(Ok(RevoraError::LimitReached))); -} - -#[test] -fn sec_error_3_concentration_limit_exceeded() { - assert_eq!(RevoraError::ConcentrationLimitExceeded as u32, 3); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - c.set_concentration_limit(&issuer, &symbol_short!("ns"), &token, &5_000, &true); - c.report_concentration(&issuer, &symbol_short!("ns"), &token, &6_000); - let r = c.try_report_revenue(&issuer, &symbol_short!("ns"), &token, &token, &1_000, &1, &false); - assert_eq!(r, Err(Ok(RevoraError::ConcentrationLimitExceeded))); -} - -#[test] -fn sec_error_4_offering_not_found() { - assert_eq!(RevoraError::OfferingNotFound as u32, 4); - let env = Env::default(); - env.mock_all_auths(); - let c = client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let r = c.try_deposit_revenue(&issuer, &symbol_short!("ns"), &token, &token, &1_000, &1); - assert_eq!(r, Err(Ok(RevoraError::OfferingNotFound))); -} - -#[test] -fn sec_error_5_period_already_deposited() { - assert_eq!(RevoraError::PeriodAlreadyDeposited as u32, 5); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - c.test_insert_period(&issuer, &symbol_short!("ns"), &token, &1, &1_000); - let r = c.try_deposit_revenue(&issuer, &symbol_short!("ns"), &token, &token, &1_000, &1); - assert_eq!(r, Err(Ok(RevoraError::PeriodAlreadyDeposited))); -} - -#[test] -fn sec_error_6_no_pending_claims() { - assert_eq!(RevoraError::NoPendingClaims as u32, 6); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let holder = Address::generate(&env); - let r = c.try_claim(&holder, &issuer, &symbol_short!("ns"), &token, &0); - assert_eq!(r, Err(Ok(RevoraError::NoPendingClaims))); -} - -#[test] -fn sec_error_7_holder_blacklisted() { - assert_eq!(RevoraError::HolderBlacklisted as u32, 7); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let holder = Address::generate(&env); - c.set_holder_share(&issuer, &symbol_short!("ns"), &token, &holder, &5_000); - c.blacklist_add(&issuer, &issuer, &symbol_short!("ns"), &token, &holder); - let r = c.try_claim(&holder, &issuer, &symbol_short!("ns"), &token, &0); - assert_eq!(r, Err(Ok(RevoraError::HolderBlacklisted))); -} - -#[test] -fn sec_error_8_invalid_share_bps() { - assert_eq!(RevoraError::InvalidShareBps as u32, 8); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let holder = Address::generate(&env); - let r = c.try_set_holder_share(&issuer, &symbol_short!("ns"), &token, &holder, &10_001); - assert_eq!(r, Err(Ok(RevoraError::InvalidShareBps))); -} - -#[test] -fn sec_error_9_payment_token_mismatch() { - assert_eq!(RevoraError::PaymentTokenMismatch as u32, 9); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let other = Address::generate(&env); - let r = c.try_deposit_revenue(&issuer, &symbol_short!("ns"), &token, &other, &1_000, &1); - assert_eq!(r, Err(Ok(RevoraError::PaymentTokenMismatch))); -} - -#[test] -fn sec_error_10_contract_frozen() { - assert_eq!(RevoraError::ContractFrozen as u32, 10); - let env = Env::default(); - env.mock_all_auths(); - let c = client(&env); - let admin = Address::generate(&env); - c.set_admin(&admin); - c.freeze(); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let r = c.try_register_offering(&issuer, &symbol_short!("ns"), &token, &500, &token, &0); - assert_eq!(r, Err(Ok(RevoraError::ContractFrozen))); -} - -#[test] -fn sec_error_11_claim_delay_not_elapsed() { - assert_eq!(RevoraError::ClaimDelayNotElapsed as u32, 11); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let holder = Address::generate(&env); - c.set_holder_share(&issuer, &symbol_short!("ns"), &token, &holder, &5_000); - c.set_claim_delay(&issuer, &symbol_short!("ns"), &token, &86_400); - c.test_insert_period(&issuer, &symbol_short!("ns"), &token, &1, &1_000); - let r = c.try_claim(&holder, &issuer, &symbol_short!("ns"), &token, &1); - assert_eq!(r, Err(Ok(RevoraError::ClaimDelayNotElapsed))); -} - -#[test] -fn sec_error_12_snapshot_not_enabled() { - assert_eq!(RevoraError::SnapshotNotEnabled as u32, 12); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let r = c.try_deposit_revenue_with_snapshot( - &issuer, &symbol_short!("ns"), &token, &token, &1_000, &1, &42, - ); - assert_eq!(r, Err(Ok(RevoraError::SnapshotNotEnabled))); -} - -#[test] -fn sec_error_13_outdated_snapshot() { - assert_eq!(RevoraError::OutdatedSnapshot as u32, 13); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - c.set_snapshot_config(&issuer, &symbol_short!("ns"), &token, &true); - // snapshot_reference=0 <= last_snap=0 - let r = c.try_deposit_revenue_with_snapshot( - &issuer, &symbol_short!("ns"), &token, &token, &1_000, &1, &0, - ); - assert_eq!(r, Err(Ok(RevoraError::OutdatedSnapshot))); -} - -#[test] -fn sec_error_14_payout_asset_mismatch() { - assert_eq!(RevoraError::PayoutAssetMismatch as u32, 14); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let wrong = Address::generate(&env); - let r = c.try_report_revenue( - &issuer, &symbol_short!("ns"), &token, &wrong, &1_000, &1, &false, - ); - assert_eq!(r, Err(Ok(RevoraError::PayoutAssetMismatch))); -} - -#[test] -fn sec_error_15_issuer_transfer_pending() { - assert_eq!(RevoraError::IssuerTransferPending as u32, 15); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let new_issuer = Address::generate(&env); - c.propose_issuer_transfer(&issuer, &symbol_short!("ns"), &token, &new_issuer); - let r = c.try_propose_issuer_transfer(&issuer, &symbol_short!("ns"), &token, &new_issuer); - assert_eq!(r, Err(Ok(RevoraError::IssuerTransferPending))); -} - -#[test] -fn sec_error_16_no_transfer_pending() { - assert_eq!(RevoraError::NoTransferPending as u32, 16); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let r = c.try_cancel_issuer_transfer(&issuer, &symbol_short!("ns"), &token); - assert_eq!(r, Err(Ok(RevoraError::NoTransferPending))); -} - -/// Security: typed error (not panic) so callers can distinguish wrong-acceptor -/// from no-pending-transfer. -#[test] -fn sec_error_17_unauthorized_transfer_accept() { - assert_eq!(RevoraError::UnauthorizedTransferAccept as u32, 17); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let new_issuer = Address::generate(&env); - let wrong = Address::generate(&env); - c.propose_issuer_transfer(&issuer, &symbol_short!("ns"), &token, &new_issuer); - let r = c.try_accept_issuer_transfer(&wrong, &issuer, &symbol_short!("ns"), &token); - assert_eq!(r, Err(Ok(RevoraError::UnauthorizedTransferAccept))); -} - -#[test] -fn sec_error_18_metadata_too_large() { - assert_eq!(RevoraError::MetadataTooLarge as u32, 18); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let mut buf = [b'a'; 257]; - buf[0] = b'i'; buf[1] = b'p'; buf[2] = b'f'; buf[3] = b's'; - buf[4] = b':'; buf[5] = b'/'; buf[6] = b'/'; - let meta = SdkString::from_str(&env, core::str::from_utf8(&buf).unwrap()); - let r = c.try_set_offering_metadata(&issuer, &symbol_short!("ns"), &token, &meta); - assert_eq!(r, Err(Ok(RevoraError::MetadataTooLarge))); -} - -#[test] -fn sec_error_19_not_authorized() { - assert_eq!(RevoraError::NotAuthorized as u32, 19); - use crate::MetaSetHolderSharePayload; - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let signer = Address::generate(&env); - let holder = Address::generate(&env); - let payload = MetaSetHolderSharePayload { - issuer, - namespace: symbol_short!("ns"), - token, - holder, - share_bps: 1_000, - }; - let fake_sig = soroban_sdk::BytesN::from_array(&env, &[0u8; 64]); - let r = c.try_meta_set_holder_share(&signer, &payload, &1, &u64::MAX, &fake_sig); - assert_eq!(r, Err(Ok(RevoraError::NotAuthorized))); -} - -/// set_testnet_mode before admin is set returns NotInitialized (not a panic). -#[test] -fn sec_error_20_not_initialized() { - assert_eq!(RevoraError::NotInitialized as u32, 20); - let env = Env::default(); - env.mock_all_auths(); - let c = client(&env); - let r = c.try_set_testnet_mode(&false); - assert_eq!(r, Err(Ok(RevoraError::NotInitialized))); -} - -#[test] -fn sec_error_21_invalid_amount() { - assert_eq!(RevoraError::InvalidAmount as u32, 21); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let r = c.try_report_revenue(&issuer, &symbol_short!("ns"), &token, &token, &-1, &1, &false); - assert_eq!(r, Err(Ok(RevoraError::InvalidAmount))); -} - -#[test] -fn sec_error_22_invalid_period_id() { - assert_eq!(RevoraError::InvalidPeriodId as u32, 22); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let r = c.try_deposit_revenue(&issuer, &symbol_short!("ns"), &token, &token, &1_000, &0); - assert_eq!(r, Err(Ok(RevoraError::InvalidPeriodId))); -} - -#[test] -fn sec_error_23_supply_cap_exceeded() { - assert_eq!(RevoraError::SupplyCapExceeded as u32, 23); - let env = Env::default(); - env.mock_all_auths(); - let c = client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - c.register_offering(&issuer, &symbol_short!("ns"), &token, &500, &token, &500); - c.test_insert_period(&issuer, &symbol_short!("ns"), &token, &1, &400); - let r = c.try_deposit_revenue(&issuer, &symbol_short!("ns"), &token, &token, &200, &2); - assert_eq!(r, Err(Ok(RevoraError::SupplyCapExceeded))); -} - -#[test] -fn sec_error_24_metadata_invalid_format() { - assert_eq!(RevoraError::MetadataInvalidFormat as u32, 24); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let meta = SdkString::from_str(&env, "ftp://bad-scheme"); - let r = c.try_set_offering_metadata(&issuer, &symbol_short!("ns"), &token, &meta); - assert_eq!(r, Err(Ok(RevoraError::MetadataInvalidFormat))); -} - -#[test] -fn sec_error_25_reporting_window_closed() { - assert_eq!(RevoraError::ReportingWindowClosed as u32, 25); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - c.set_report_window(&issuer, &symbol_short!("ns"), &token, &9_999_999_000, &9_999_999_999); - let r = c.try_report_revenue(&issuer, &symbol_short!("ns"), &token, &token, &1_000, &1, &false); - assert_eq!(r, Err(Ok(RevoraError::ReportingWindowClosed))); -} - -#[test] -fn sec_error_26_claim_window_closed() { - assert_eq!(RevoraError::ClaimWindowClosed as u32, 26); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let holder = Address::generate(&env); - c.set_holder_share(&issuer, &symbol_short!("ns"), &token, &holder, &5_000); - c.test_insert_period(&issuer, &symbol_short!("ns"), &token, &1, &1_000); - c.set_claim_window(&issuer, &symbol_short!("ns"), &token, &9_999_999_000, &9_999_999_999); - let r = c.try_claim(&holder, &issuer, &symbol_short!("ns"), &token, &1); - assert_eq!(r, Err(Ok(RevoraError::ClaimWindowClosed))); -} - -#[test] -fn sec_error_27_signature_expired() { - assert_eq!(RevoraError::SignatureExpired as u32, 27); - use crate::MetaSetHolderSharePayload; - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let signer = Address::generate(&env); - let holder = Address::generate(&env); - c.set_meta_delegate(&issuer, &symbol_short!("ns"), &token, &signer); - let payload = MetaSetHolderSharePayload { - issuer, - namespace: symbol_short!("ns"), - token, - holder, - share_bps: 1_000, - }; - let fake_sig = soroban_sdk::BytesN::from_array(&env, &[0u8; 64]); - env.ledger().with_mut(|l| l.timestamp = 1); - let r = c.try_meta_set_holder_share(&signer, &payload, &1, &0, &fake_sig); - assert_eq!(r, Err(Ok(RevoraError::SignatureExpired))); -} - -/// Discriminant stability check. Full replay path requires a valid ed25519 sig. -#[test] -fn sec_error_28_signature_replay_discriminant_stable() { - assert_eq!(RevoraError::SignatureReplay as u32, 28); -} - -#[test] -fn sec_error_29_signer_key_not_registered() { - assert_eq!(RevoraError::SignerKeyNotRegistered as u32, 29); - use crate::MetaSetHolderSharePayload; - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let signer = Address::generate(&env); - let holder = Address::generate(&env); - c.set_meta_delegate(&issuer, &symbol_short!("ns"), &token, &signer); - let payload = MetaSetHolderSharePayload { - issuer, - namespace: symbol_short!("ns"), - token, - holder, - share_bps: 1_000, - }; - let fake_sig = soroban_sdk::BytesN::from_array(&env, &[0u8; 64]); - let r = c.try_meta_set_holder_share(&signer, &payload, &1, &u64::MAX, &fake_sig); - assert_eq!(r, Err(Ok(RevoraError::SignerKeyNotRegistered))); -} - -#[test] -fn sec_error_30_share_sum_exceeded() { - assert_eq!(RevoraError::ShareSumExceeded as u32, 30); - let env = Env::default(); - env.mock_all_auths(); - let (c, issuer, token) = with_offering(&env); - let h1 = Address::generate(&env); - let h2 = Address::generate(&env); - c.set_holder_share(&issuer, &symbol_short!("ns"), &token, &h1, &9_000); - let r = c.try_set_holder_share(&issuer, &symbol_short!("ns"), &token, &h2, &2_000); - assert_eq!(r, Err(Ok(RevoraError::ShareSumExceeded))); -} - -/// All 30 discriminants are distinct and cover the range 1..=30. -#[test] -fn sec_all_discriminants_unique_and_contiguous() { - let codes: [u32; 30] = [ - RevoraError::InvalidRevenueShareBps as u32, - RevoraError::LimitReached as u32, - RevoraError::ConcentrationLimitExceeded as u32, - RevoraError::OfferingNotFound as u32, - RevoraError::PeriodAlreadyDeposited as u32, - RevoraError::NoPendingClaims as u32, - RevoraError::HolderBlacklisted as u32, - RevoraError::InvalidShareBps as u32, - RevoraError::PaymentTokenMismatch as u32, - RevoraError::ContractFrozen as u32, - RevoraError::ClaimDelayNotElapsed as u32, - RevoraError::SnapshotNotEnabled as u32, - RevoraError::OutdatedSnapshot as u32, - RevoraError::PayoutAssetMismatch as u32, - RevoraError::IssuerTransferPending as u32, - RevoraError::NoTransferPending as u32, - RevoraError::UnauthorizedTransferAccept as u32, - RevoraError::MetadataTooLarge as u32, - RevoraError::NotAuthorized as u32, - RevoraError::NotInitialized as u32, - RevoraError::InvalidAmount as u32, - RevoraError::InvalidPeriodId as u32, - RevoraError::SupplyCapExceeded as u32, - RevoraError::MetadataInvalidFormat as u32, - RevoraError::ReportingWindowClosed as u32, - RevoraError::ClaimWindowClosed as u32, - RevoraError::SignatureExpired as u32, - RevoraError::SignatureReplay as u32, - RevoraError::SignerKeyNotRegistered as u32, - RevoraError::ShareSumExceeded as u32, - ]; - for (i, &code) in codes.iter().enumerate() { - assert_eq!(code, (i + 1) as u32, "discriminant mismatch at index {}", i); - } -}