33#![ deny( clippy:: dbg_macro, clippy:: todo, clippy:: unimplemented) ]
44use soroban_sdk:: {
55 contract, contracterror, contractimpl, contracttype, symbol_short, token, xdr:: ToXdr , Address ,
6- BytesN , Env , Map , String , Symbol , Vec ,
6+ BytesN , Env , IntoVal , Map , String , Symbol , Vec ,
77} ;
88
99// Issue #109 — Revenue report correction workflow with audit trail.
@@ -216,6 +216,25 @@ const STELLAR_CANONICAL_DECIMALS: u32 = 7;
216216/// Maximum accepted decimal precision (safety cap for normalization math).
217217const MAX_TOKEN_DECIMALS : u32 = 18 ;
218218
219+ // ── Missing legacy/v1 event symbols ──────────────────────────
220+ /// v1 schema version tag (legacy; v2 is the current standard).
221+ pub const EVENT_SCHEMA_VERSION : u32 = 1 ;
222+ const EVENT_SHARE_SET : Symbol = symbol_short ! ( "sh_set" ) ;
223+ const EVENT_OFFER_REG_V1 : Symbol = symbol_short ! ( "ofr_reg1" ) ;
224+ const EVENT_REV_INIT_V1 : Symbol = symbol_short ! ( "rv_init1" ) ;
225+ const EVENT_CONCENTRATION_WARNING : Symbol = symbol_short ! ( "conc_wrn" ) ;
226+ const EVENT_CONCENTRATION_REPORTED : Symbol = symbol_short ! ( "conc_rep" ) ;
227+ const EVENT_SNAP_COMMIT : Symbol = symbol_short ! ( "snap_cmt" ) ;
228+ const EVENT_SNAP_SHARES_APPLIED : Symbol = symbol_short ! ( "snap_shr" ) ;
229+ const EVENT_FREEZE_OFFERING : Symbol = symbol_short ! ( "frz_off" ) ;
230+ const EVENT_UNFREEZE_OFFERING : Symbol = symbol_short ! ( "ufrz_off" ) ;
231+ const EVENT_PROPOSAL_CREATED : Symbol = symbol_short ! ( "prop_new" ) ;
232+ const EVENT_FREEZE : Symbol = symbol_short ! ( "freeze" ) ;
233+ /// Issuer transfer expiry: 7 days in seconds.
234+ const ISSUER_TRANSFER_EXPIRY_SECS : u64 = 7 * 24 * 60 * 60 ;
235+ const EVENT_CLAIM : Symbol = symbol_short ! ( "claim" ) ;
236+ const EVENT_CLAIM_DELAY_SET : Symbol = symbol_short ! ( "dly_set" ) ;
237+
219238/// Represents a revenue-share offering registered on-chain.
220239/// Offerings are immutable once registered.
221240// ── Data structures ──────────────────────────────────────────
@@ -432,11 +451,8 @@ pub struct SnapshotEntry {
432451 pub total_bps : u32 ,
433452}
434453
435- /// Storage keys: offerings use OfferCount/OfferItem; blacklist uses Blacklist(token).
436- /// Multi-period claim keys use PeriodRevenue/PeriodEntry/PeriodCount for per-offering
437- /// period tracking, HolderShare for holder allocations, LastClaimedIdx for claim progress,
438- /// and PaymentToken for the token used to pay out revenue.
439- /// `RevenueIndex` and `RevenueReports` track reported (un-deposited) revenue totals and details.
454+ /// Primary storage keys for core contract state.
455+ /// Split from the full key set to stay within the Soroban XDR union variant limit (≤50).
440456#[ contracttype]
441457pub enum DataKey {
442458 /// Last deposited/reported period_id for offering (enforces strictly increasing ordering).
@@ -500,53 +516,58 @@ pub enum DataKey {
500516 /// Latest recorded snapshot reference for an offering.
501517 LastSnapshotRef ( OfferingId ) ,
502518 /// Committed snapshot entry keyed by (offering_id, snapshot_ref).
503- /// Stores the canonical SnapshotEntry for deterministic replay and audit.
504519 SnapshotEntry ( OfferingId , u64 ) ,
505- /// Per-snapshot holder share at index N: (offering_id, snapshot_ref, index) -> (holder, share_bps) .
520+ /// Per-snapshot holder share at index N.
506521 SnapshotHolder ( OfferingId , u64 , u32 ) ,
507- /// Total number of holders recorded in a snapshot: (offering_id, snapshot_ref) -> u32 .
522+ /// Total number of holders recorded in a snapshot.
508523 SnapshotHolderCount ( OfferingId , u64 ) ,
509524
510- /// Pending issuer transfer for an offering: OfferingId -> new_issuer .
525+ /// Pending issuer transfer for an offering.
511526 PendingIssuerTransfer ( OfferingId ) ,
512- /// Current issuer lookup by offering token: OfferingId -> issuer .
527+ /// Current issuer lookup by offering token.
513528 OfferingIssuer ( OfferingId ) ,
514- /// Testnet mode flag; when true, enables fee-free/simplified behavior (#24) .
529+ /// Testnet mode flag.
515530 TestnetMode ,
516531
517532 /// Safety role address for emergency pause (#7).
518533 Safety ,
519- /// Global pause flag; when true, state-mutating ops are disabled (#7) .
534+ /// Global pause flag.
520535 Paused ,
521536
522537 /// Configuration flag: when true, contract is event-only (no persistent business state).
523538 EventOnlyMode ,
524539
525- /// Metadata reference (IPFS hash, HTTPS URI, etc.) for an offering.
540+ /// Metadata reference for an offering.
526541 OfferingMetadata ( OfferingId ) ,
527- /// Platform fee in basis points (max 5000 = 50%) taken from reported revenue (#6) .
542+ /// Platform fee in basis points.
528543 PlatformFeeBps ,
529544 /// Per-offering per-asset fee override (#98).
530545 OfferingFeeBps ( OfferingId , Address ) ,
531546 /// Platform level per-asset fee (#98).
532547 PlatformFeePerAsset ( Address ) ,
533548
534- /// Per-offering minimum revenue threshold below which no distribution is triggered (#25).
549+ /// Per-offering minimum revenue threshold (#25).
535550 MinRevenueThreshold ( OfferingId ) ,
551+ /// Total deposited revenue for an offering (#39).
552+ DepositedRevenue ( OfferingId ) ,
553+ /// Per-offering supply cap (#96). 0 = no cap.
554+ SupplyCap ( OfferingId ) ,
555+ /// Per-offering investment constraints (#97).
556+ InvestmentConstraints ( OfferingId ) ,
557+ }
558+
559+ /// Secondary storage keys for auxiliary/extended contract state.
560+ /// Overflow enum to keep DataKey within the Soroban XDR union variant limit.
561+ #[ contracttype]
562+ pub enum DataKey2 {
536563 /// Global count of unique issuers (#39).
537564 IssuerCount ,
538565 /// Issuer address at global index (#39).
539566 IssuerItem ( u32 ) ,
540567 /// Whether an issuer is already registered in the global registry (#39).
541568 IssuerRegistered ( Address ) ,
542- /// Total deposited revenue for an offering (#39).
543- DepositedRevenue ( OfferingId ) ,
544- /// Per-offering supply cap (#96). 0 = no cap.
545- SupplyCap ( OfferingId ) ,
546- /// Per-offering investment constraints: min and max stake per investor (#97).
547- InvestmentConstraints ( OfferingId ) ,
548569
549- /// Per-issuer namespace tracking
570+ /// Per-issuer namespace tracking.
550571 NamespaceCount ( Address ) ,
551572 NamespaceItem ( Address , u32 ) ,
552573 NamespaceRegistered ( Address , Symbol ) ,
@@ -562,6 +583,12 @@ pub enum DataKey {
562583/// Maximum number of offerings returned in a single page.
563584const MAX_PAGE_LIMIT : u32 = 20 ;
564585
586+ /// Maximum number of addresses that can be blacklisted per offering.
587+ /// Prevents unbounded storage growth and keeps distribution gas predictable.
588+ /// Security assumption: an issuer cannot use the blacklist as a DoS vector
589+ /// against on-chain storage by adding an unlimited number of entries.
590+ const MAX_BLACKLIST_SIZE : u32 = 200 ;
591+
565592/// Maximum platform fee in basis points (50%).
566593const MAX_PLATFORM_FEE_BPS : u32 = 5_000 ;
567594
@@ -814,11 +841,11 @@ impl RevoraRevenueShare {
814841 /// Helper to emit deterministic v2 versioned events for core event versioning.
815842 /// Emits: topic -> (EVENT_SCHEMA_VERSION_V2, data...)
816843 /// All core events MUST use this for schema compliance and indexer compatibility.
817- fn emit_v2_event < T : IntoVal < Env , Vec > > (
818- env : & Env ,
819- topic_tuple : impl IntoVal < Env , ( Symbol , ) > ,
820- data : T ,
821- ) {
844+ fn emit_v2_event < Topics , T > ( env : & Env , topic_tuple : Topics , data : T )
845+ where
846+ Topics : IntoVal < Env , soroban_sdk :: Val > + soroban_sdk :: events :: Topics ,
847+ T : IntoVal < Env , soroban_sdk :: Val > + soroban_sdk :: TryIntoVal < Env , soroban_sdk :: Val > ,
848+ {
822849 env. events ( ) . publish ( topic_tuple, ( EVENT_SCHEMA_VERSION_V2 , data) ) ;
823850 }
824851
@@ -1283,13 +1310,13 @@ impl RevoraRevenueShare {
12831310 }
12841311
12851312 // Register namespace for issuer if not already present
1286- let ns_reg_key = DataKey :: NamespaceRegistered ( issuer. clone ( ) , namespace. clone ( ) ) ;
1313+ let ns_reg_key = DataKey2 :: NamespaceRegistered ( issuer. clone ( ) , namespace. clone ( ) ) ;
12871314 if !env. storage ( ) . persistent ( ) . has ( & ns_reg_key) {
1288- let ns_count_key = DataKey :: NamespaceCount ( issuer. clone ( ) ) ;
1315+ let ns_count_key = DataKey2 :: NamespaceCount ( issuer. clone ( ) ) ;
12891316 let count: u32 = env. storage ( ) . persistent ( ) . get ( & ns_count_key) . unwrap_or ( 0 ) ;
12901317 env. storage ( )
12911318 . persistent ( )
1292- . set ( & DataKey :: NamespaceItem ( issuer. clone ( ) , count) , & namespace) ;
1319+ . set ( & DataKey2 :: NamespaceItem ( issuer. clone ( ) , count) , & namespace) ;
12931320 env. storage ( ) . persistent ( ) . set ( & ns_count_key, & ( count + 1 ) ) ;
12941321 env. storage ( ) . persistent ( ) . set ( & ns_reg_key, & true ) ;
12951322 }
@@ -1942,10 +1969,17 @@ impl RevoraRevenueShare {
19421969 /// - `token`: The token representing the offering.
19431970 /// - `investor`: The address to be blacklisted.
19441971 ///
1972+ /// ### Security Assumptions
1973+ /// - `caller` must be the current issuer of the offering or the contract admin.
1974+ /// - The blacklist is capped at `MAX_BLACKLIST_SIZE` entries per offering to prevent
1975+ /// unbounded storage growth and keep distribution gas predictable.
1976+ /// - Idempotent adds (address already present) do not count against the size limit.
1977+ ///
19451978 /// ### Returns
19461979 /// - `Ok(())` on success.
19471980 /// - `Err(RevoraError::ContractFrozen)` if the contract is frozen.
19481981 /// - `Err(RevoraError::NotAuthorized)` if caller is not the current issuer.
1982+ /// - `Err(RevoraError::BlacklistSizeLimitExceeded)` if the blacklist is at capacity.
19491983 pub fn blacklist_add (
19501984 env : Env ,
19511985 caller : Address ,
@@ -1986,6 +2020,10 @@ impl RevoraRevenueShare {
19862020
19872021 let was_present = map. get ( investor. clone ( ) ) . unwrap_or ( false ) ;
19882022 if !was_present {
2023+ // Guard: reject if the blacklist is already at capacity.
2024+ if map. len ( ) >= MAX_BLACKLIST_SIZE {
2025+ return Err ( RevoraError :: BlacklistSizeLimitExceeded ) ;
2026+ }
19892027 map. set ( investor. clone ( ) , true ) ;
19902028 env. storage ( ) . persistent ( ) . set ( & key, & map) ;
19912029
@@ -2039,6 +2077,18 @@ impl RevoraRevenueShare {
20392077 } ;
20402078 Self :: require_not_offering_frozen ( & env, & offering_id) ?;
20412079
2080+ // Verify auth: caller must be issuer or admin.
2081+ // Security assumption: only the current issuer or contract admin may remove
2082+ // addresses from the blacklist. This mirrors the add-side guard and prevents
2083+ // unauthorized actors from re-enabling blacklisted investors.
2084+ let current_issuer =
2085+ Self :: get_current_issuer ( & env, issuer. clone ( ) , namespace. clone ( ) , token. clone ( ) )
2086+ . ok_or ( RevoraError :: OfferingNotFound ) ?;
2087+ let admin = Self :: get_admin ( env. clone ( ) ) . ok_or ( RevoraError :: NotInitialized ) ?;
2088+ if caller != current_issuer && caller != admin {
2089+ return Err ( RevoraError :: NotAuthorized ) ;
2090+ }
2091+
20422092 let key = DataKey :: Blacklist ( offering_id. clone ( ) ) ;
20432093 let mut map: Map < Address , bool > =
20442094 env. storage ( ) . persistent ( ) . get ( & key) . unwrap_or_else ( || Map :: new ( & env) ) ;
@@ -2095,6 +2145,28 @@ impl RevoraRevenueShare {
20952145 . unwrap_or_else ( || Vec :: new ( & env) )
20962146 }
20972147
2148+ /// Return the current number of blacklisted addresses for an offering.
2149+ ///
2150+ /// This is a cheap O(1) read of the underlying map length and can be used
2151+ /// by off-chain tooling to monitor proximity to `MAX_BLACKLIST_SIZE` (200)
2152+ /// before attempting an add.
2153+ ///
2154+ /// Returns 0 when no blacklist exists yet for the offering.
2155+ pub fn get_blacklist_size (
2156+ env : Env ,
2157+ issuer : Address ,
2158+ namespace : Symbol ,
2159+ token : Address ,
2160+ ) -> u32 {
2161+ let offering_id = OfferingId { issuer, namespace, token } ;
2162+ let key = DataKey :: Blacklist ( offering_id) ;
2163+ env. storage ( )
2164+ . persistent ( )
2165+ . get :: < DataKey , Map < Address , bool > > ( & key)
2166+ . map ( |m| m. len ( ) )
2167+ . unwrap_or ( 0 )
2168+ }
2169+
20982170 // ── Whitelist management ──────────────────────────────────
20992171
21002172 /// Set per-offering concentration limit. Caller must be the offering issuer.
@@ -3174,6 +3246,11 @@ impl RevoraRevenueShare {
31743246 let offering_id = OfferingId { issuer, namespace, token } ;
31753247 env. storage ( ) . persistent ( ) . get ( & DataKey :: SnapshotHolder ( offering_id, snapshot_ref, index) )
31763248 }
3249+ }
3250+
3251+ // ── Holder shares, claims, admin, governance, and utility methods ─────────────
3252+ // Plain impl block — excluded from the ABI spec to keep spec XDR within limit.
3253+ impl RevoraRevenueShare {
31773254 ///
31783255 /// The share determines the percentage of a period's revenue the holder can claim.
31793256 ///
@@ -3225,6 +3302,8 @@ impl RevoraRevenueShare {
32253302 )
32263303 }
32273304
3305+ // ── Meta-authorization, claims, windows, and query methods ───────────────────
3306+
32283307 /// Register an ed25519 public key for a signer address.
32293308 /// The signer must authorize this binding.
32303309 pub fn register_meta_signer_key (
0 commit comments