@@ -78,6 +78,9 @@ pub enum ContractError {
7878 WhitelistTooLarge = 29 ,
7979 InsufficientTreasuryBalance = 30 ,
8080 BatchClaimExceedsLimit = 31 ,
81+ InsufficientTreasuryBalance = 28 ,
82+ BatchClaimExceedsLimit = 29 ,
83+ InvalidCoCreatorShare = 30 ,
8184}
8285
8386pub mod fee {
@@ -197,6 +200,19 @@ pub mod fee {
197200 Some ( ( creator_amount, protocol_amount) )
198201 }
199202
203+ /// Splits `total` into `(remainder, shared_amount)` by basis points.
204+ ///
205+ /// Remainder from integer division stays with the primary recipient so the
206+ /// two outputs always sum to `total`.
207+ pub fn checked_split_bps_amount ( total : i128 , share_bps : u32 ) -> Option < ( i128 , i128 ) > {
208+ if total <= 0 {
209+ return Some ( ( 0 , 0 ) ) ;
210+ }
211+ let shared_amount = apply_percentage_fee ( total, share_bps) ?;
212+ let remainder = checked_sub_i128 ( total, shared_amount) ?;
213+ Some ( ( remainder, shared_amount) )
214+ }
215+
200216 /// Performs checked integer multiplication for quote math helpers.
201217 pub fn checked_mul_i128 ( a : i128 , b : i128 ) -> Option < i128 > {
202218 a. checked_mul ( b)
@@ -293,6 +309,14 @@ pub mod constants {
293309 DataKey :: CreatorFeeBalance ( creator. clone ( ) )
294310 }
295311
312+ pub fn co_creator ( creator : & Address ) -> DataKey {
313+ DataKey :: CoCreator ( creator. clone ( ) )
314+ }
315+
316+ pub fn co_creator_fee_balance ( creator : & Address , co_creator : & Address ) -> DataKey {
317+ DataKey :: CoCreatorFeeBalance ( creator. clone ( ) , co_creator. clone ( ) )
318+ }
319+
296320 pub fn creator ( creator : & Address ) -> DataKey {
297321 creator_key ( creator)
298322 }
@@ -340,6 +364,8 @@ pub mod constants {
340364 pub const FEE_CONFIG : & str = "get_creator_fee_config" ;
341365 pub const FEE_RECIPIENT : & str = "get_creator_fee_recipient" ;
342366 pub const FEE_RECIPIENT_BALANCE : & str = "get_creator_fee_balance" ;
367+ pub const CO_CREATOR : & str = "get_co_creator" ;
368+ pub const CO_CREATOR_FEE_BALANCE : & str = "get_co_creator_fee_balance" ;
343369 pub const HOLDER_KEY_COUNT : & str = "get_holder_key_count" ;
344370 pub const PROFILE : & str = "get_creator" ;
345371 pub const SUPPLY : & str = "get_creator_supply" ;
@@ -500,6 +526,8 @@ pub enum DataKey {
500526 CurvePreset ( Address ) ,
501527 Whitelist ( Address ) ,
502528 TreasuryBalance ,
529+ CoCreator ( Address ) ,
530+ CoCreatorFeeBalance ( Address , Address ) ,
503531}
504532
505533/// Immutable early-access whitelist configuration set at creator registration.
@@ -531,6 +559,17 @@ pub struct LockedAllocation {
531559 pub claimed : bool ,
532560}
533561
562+ /// Optional immutable collaborator split configured at creator registration.
563+ ///
564+ /// `share_bps` is the co-creator's share of the creator fee, not of the full
565+ /// trade price. It must be in the inclusive range `1..=9999`.
566+ #[ derive( Clone , Debug , PartialEq ) ]
567+ #[ contracttype]
568+ pub struct CoCreatorConfig {
569+ pub address : Address ,
570+ pub share_bps : u32 ,
571+ }
572+
534573#[ derive( Clone , Debug , PartialEq ) ]
535574#[ contracttype]
536575pub struct CreatorProfile {
@@ -626,6 +665,73 @@ fn credit_creator_fee_recipient_balance(
626665 Ok ( ( ) )
627666}
628667
668+ fn read_co_creator_config ( env : & Env , creator : & Address ) -> Option < CoCreatorConfig > {
669+ let key = constants:: storage:: co_creator ( creator) ;
670+ env. storage ( )
671+ . persistent ( )
672+ . get :: < DataKey , CoCreatorConfig > ( & key)
673+ }
674+
675+ fn validate_co_creator_config ( env : & Env , config : & CoCreatorConfig ) -> Result < ( ) , ContractError > {
676+ validate_non_zero_address ( env, & config. address ) ?;
677+ if !( 1 ..fee:: BPS_MAX ) . contains ( & config. share_bps ) {
678+ return Err ( ContractError :: InvalidCoCreatorShare ) ;
679+ }
680+ Ok ( ( ) )
681+ }
682+
683+ /// Reads accrued fee balance for a creator's configured co-creator.
684+ pub fn read_co_creator_fee_balance ( env : & Env , creator : & Address , co_creator : & Address ) -> i128 {
685+ let key = constants:: storage:: co_creator_fee_balance ( creator, co_creator) ;
686+ env. storage ( ) . persistent ( ) . get ( & key) . unwrap_or ( 0 )
687+ }
688+
689+ fn credit_co_creator_fee_balance (
690+ env : & Env ,
691+ creator : & Address ,
692+ co_creator : & Address ,
693+ amount : i128 ,
694+ ) -> Result < ( ) , ContractError > {
695+ if amount <= 0 {
696+ return Ok ( ( ) ) ;
697+ }
698+ let key = constants:: storage:: co_creator_fee_balance ( creator, co_creator) ;
699+ let current = read_co_creator_fee_balance ( env, creator, co_creator) ;
700+ let updated = current. checked_add ( amount) . ok_or ( ContractError :: Overflow ) ?;
701+ env. storage ( ) . persistent ( ) . set ( & key, & updated) ;
702+ Ok ( ( ) )
703+ }
704+
705+ fn credit_creator_fee ( env : & Env , creator : & Address , amount : i128 ) -> Result < ( ) , ContractError > {
706+ if amount <= 0 {
707+ return Ok ( ( ) ) ;
708+ }
709+
710+ let Some ( config) = read_co_creator_config ( env, creator) else {
711+ return credit_creator_fee_recipient_balance ( env, creator, amount) ;
712+ } ;
713+
714+ let co_creator = config. address ;
715+ let ( creator_recipient_amount, co_creator_amount) =
716+ fee:: checked_split_bps_amount ( amount, config. share_bps ) . ok_or ( ContractError :: Overflow ) ?;
717+ credit_creator_fee_recipient_balance ( env, creator, creator_recipient_amount) ?;
718+ credit_co_creator_fee_balance ( env, creator, & co_creator, co_creator_amount) ?;
719+
720+ if co_creator_amount > 0 {
721+ env. events ( ) . publish (
722+ events:: co_creator_fee_earned_topics ( creator, & co_creator) ,
723+ events:: CoCreatorFeeEarned {
724+ creator_id : creator. clone ( ) ,
725+ co_creator,
726+ amount : co_creator_amount,
727+ ledger : env. ledger ( ) . sequence ( ) ,
728+ } ,
729+ ) ;
730+ }
731+
732+ Ok ( ( ) )
733+ }
734+
629735fn is_valid_handle_byte ( byte : u8 ) -> bool {
630736 byte. is_ascii_lowercase ( ) || byte. is_ascii_digit ( ) || byte == b'_'
631737}
@@ -789,23 +895,26 @@ fn assert_sell_proceeds_slippage(
789895 Ok ( ( ) )
790896}
791897
792- fn accrue_sell_protocol_fee ( env : & Env , price : i128 ) -> Result < ( ) , ContractError > {
898+ fn accrue_sell_trade_fees ( env : & Env , creator : & Address , price : i128 ) -> Result < ( ) , ContractError > {
793899 if read_protocol_fee_config ( env) . is_none ( ) {
794900 return Ok ( ( ) ) ;
795901 }
796902
797- let ( _, protocol_fee) = CreatorKeysContract :: compute_fees_for_payment ( env. clone ( ) , price) ?;
903+ let ( creator_fee, protocol_fee) =
904+ CreatorKeysContract :: compute_fees_for_payment ( env. clone ( ) , price) ?;
905+ credit_creator_fee ( env, creator, creator_fee) ?;
798906 credit_treasury_balance ( env, protocol_fee) ?;
799907
800908 if env
801909 . storage ( )
802910 . persistent ( )
803911 . get :: < DataKey , Address > ( & constants:: storage:: PROTOCOL_FEE_RECIPIENT )
804- . is_none ( )
912+ . is_some ( )
805913 {
806- return Ok ( ( ) ) ;
914+ credit_protocol_fee_recipient_balance ( env , protocol_fee ) ? ;
807915 }
808- credit_protocol_fee_recipient_balance ( env, protocol_fee)
916+
917+ Ok ( ( ) )
809918}
810919
811920/// Resolves and validates the shared inputs required by read-only quote methods.
@@ -1105,6 +1214,25 @@ fn extend_creator_ttl(env: &Env, creator: &Address) {
11051214 . persistent ( )
11061215 . extend_ttl ( & curve_preset_key, threshold, extend_to) ;
11071216 }
1217+
1218+ let co_creator_key = constants:: storage:: co_creator ( creator) ;
1219+ if env. storage ( ) . persistent ( ) . has ( & co_creator_key) {
1220+ env. storage ( )
1221+ . persistent ( )
1222+ . extend_ttl ( & co_creator_key, threshold, extend_to) ;
1223+
1224+ if let Some ( config) = read_co_creator_config ( env, creator) {
1225+ let co_creator_balance_key =
1226+ constants:: storage:: co_creator_fee_balance ( creator, & config. address ) ;
1227+ if env. storage ( ) . persistent ( ) . has ( & co_creator_balance_key) {
1228+ env. storage ( ) . persistent ( ) . extend_ttl (
1229+ & co_creator_balance_key,
1230+ threshold,
1231+ extend_to,
1232+ ) ;
1233+ }
1234+ }
1235+ }
11081236}
11091237
11101238#[ contract]
@@ -1128,6 +1256,10 @@ impl CreatorKeysContract {
11281256 /// If provided, `unlock_ledger` must be strictly greater than current ledger.
11291257 /// - `max_supply`: optional maximum supply cap. If provided, must be greater than zero.
11301258 /// - `whitelist_window`: optional immutable early-access address list and ledger duration.
1259+
1260+ /// - `co_creator`: optional immutable collaborator split. If provided, `share_bps`
1261+ /// must be in the inclusive range `1..=9999`.
1262+
11311263 pub fn register_creator (
11321264 env : Env ,
11331265 creator : Address ,
@@ -1136,11 +1268,16 @@ impl CreatorKeysContract {
11361268 max_supply : Option < u32 > ,
11371269 curve_preset : Option < CurvePreset > ,
11381270 whitelist_window : Option < WhitelistConfig > ,
1271+ co_creator : Option < CoCreatorConfig > ,
1272+
11391273 ) -> Result < ( ) , ContractError > {
11401274 creator. require_auth ( ) ;
11411275 assert_not_paused ( & env) ?;
11421276
11431277 validate_creator_handle ( & handle) ?;
1278+ if let Some ( config) = co_creator. as_ref ( ) {
1279+ validate_co_creator_config ( & env, config) ?;
1280+ }
11441281
11451282 let key = constants:: storage:: creator ( & creator) ;
11461283 // Creator profile storage is a single source of truth keyed by creator address.
@@ -1209,6 +1346,12 @@ impl CreatorKeysContract {
12091346 let preset_key = constants:: storage:: curve_preset ( & creator) ;
12101347 env. storage ( ) . persistent ( ) . set ( & preset_key, & preset) ;
12111348
1349+ if let Some ( config) = co_creator {
1350+ env. storage ( )
1351+ . persistent ( )
1352+ . set ( & constants:: storage:: co_creator ( & creator) , & config) ;
1353+ }
1354+
12121355 let profile = CreatorProfile {
12131356 creator : creator. clone ( ) ,
12141357 handle,
@@ -1238,6 +1381,11 @@ impl CreatorKeysContract {
12381381 env. storage ( )
12391382 . persistent ( )
12401383 . extend_ttl ( & whitelist_key, current_ledger, extend_to) ;
1384+ let co_creator_key = constants:: storage:: co_creator ( & creator) ;
1385+ if env. storage ( ) . persistent ( ) . has ( & co_creator_key) {
1386+ env. storage ( )
1387+ . persistent ( )
1388+ . extend_ttl ( & co_creator_key, current_ledger, extend_to) ;
12411389 }
12421390
12431391 env. events ( ) . publish (
@@ -1329,7 +1477,7 @@ impl CreatorKeysContract {
13291477 let ( creator_fee, protocol_fee) =
13301478 fee:: checked_compute_fee_split ( price, config. creator_bps , config. protocol_bps )
13311479 . ok_or ( ContractError :: Overflow ) ?;
1332- credit_creator_fee_recipient_balance ( & env, & creator, creator_fee) ?;
1480+ credit_creator_fee ( & env, & creator, creator_fee) ?;
13331481 credit_protocol_fee_recipient_balance ( & env, protocol_fee) ?;
13341482 credit_treasury_balance ( & env, protocol_fee) ?;
13351483 }
@@ -1399,7 +1547,7 @@ impl CreatorKeysContract {
13991547 // supply/holder_count invariants for subsequent reads.
14001548 env. storage ( ) . persistent ( ) . set ( & key, & profile) ;
14011549 env. storage ( ) . persistent ( ) . set ( & balance_key, & new_balance) ;
1402- accrue_sell_protocol_fee ( & env, price) ?;
1550+ accrue_sell_trade_fees ( & env, & creator , price) ?;
14031551
14041552 env. events ( ) . publish (
14051553 ( events:: SELL_EVENT_NAME , creator. clone ( ) , seller) ,
@@ -1763,6 +1911,26 @@ impl CreatorKeysContract {
17631911 Ok ( read_creator_fee_recipient_balance ( & env, & creator) )
17641912 }
17651913
1914+ /// Read-only view: returns the optional immutable co-creator config.
1915+ ///
1916+ /// Returns `None` when the creator was registered without a co-creator split.
1917+ pub fn get_co_creator ( env : Env , creator : Address ) -> Option < CoCreatorConfig > {
1918+ read_co_creator_config ( & env, & creator)
1919+ }
1920+
1921+ /// Read-only view: returns accrued co-creator fee balance for a creator.
1922+ ///
1923+ /// Fails with [`ContractError::NotRegistered`] if the creator is not registered.
1924+ /// Returns `0` when no co-creator fees have accrued for the address.
1925+ pub fn get_co_creator_fee_balance (
1926+ env : Env ,
1927+ creator : Address ,
1928+ co_creator : Address ,
1929+ ) -> Result < i128 , ContractError > {
1930+ read_registered_creator_profile ( & env, & creator) ?;
1931+ Ok ( read_co_creator_fee_balance ( & env, & creator, & co_creator) )
1932+ }
1933+
17661934 /// Read-only view: returns the configured creator fee rate in basis points.
17671935 ///
17681936 /// The returned value is the creator-facing share stored in the current protocol
0 commit comments