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
166 changes: 140 additions & 26 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,72 +10,177 @@ 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).
InvalidPeriodId = 22,

/// 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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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> {
Expand Down
Loading
Loading