Skip to content

Commit 054ca09

Browse files
authored
Merge branch 'main' into fix-conflict
2 parents c5d3764 + 6aefb17 commit 054ca09

4 files changed

Lines changed: 405 additions & 7 deletions

File tree

creator-keys/src/events.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,20 @@ pub const PROTOCOL_FEE_RECIPIENT_UPDATED_EVENT_NAME: Symbol = symbol_short!("p_f
176176
/// Event name for creator fee recipient updated.
177177
pub const CREATOR_FEE_RECIPIENT_UPDATED_EVENT_NAME: Symbol = symbol_short!("c_fee_upd");
178178

179+
/// Event name for co-creator fee accrual.
180+
pub const CO_CREATOR_FEE_EARNED_EVENT_NAME: Symbol = symbol_short!("co_fee");
181+
179182
/// Stable field order for dividend distributed event payloads.
180183
pub const DIVIDEND_DISTRIBUTED_DATA_FIELDS: [&str; 4] =
181184
["creator", "total_amount", "snapshot_supply", "ledger"];
182185

183186
/// Stable field order for dividend claimed event payloads.
184187
pub const DIVIDEND_CLAIMED_DATA_FIELDS: [&str; 3] = ["creator", "claimant", "amount"];
185188

189+
/// Stable field order for co-creator fee earned event payloads.
190+
pub const CO_CREATOR_FEE_EARNED_DATA_FIELDS: [&str; 4] =
191+
["creator_id", "co_creator", "amount", "ledger"];
192+
186193
#[derive(Clone, Debug, Eq, PartialEq)]
187194
#[contracttype]
188195
pub struct DividendDistributedEvent {
@@ -246,6 +253,26 @@ pub struct CreatorFeeRecipientUpdatedEvent {
246253
pub new_recipient: Address,
247254
}
248255

256+
#[derive(Clone, Debug, Eq, PartialEq)]
257+
#[contracttype]
258+
pub struct CoCreatorFeeEarned {
259+
pub creator_id: Address,
260+
pub co_creator: Address,
261+
pub amount: i128,
262+
pub ledger: u32,
263+
}
264+
265+
pub fn co_creator_fee_earned_topics(
266+
creator_id: &Address,
267+
co_creator: &Address,
268+
) -> (Symbol, Address, Address) {
269+
(
270+
CO_CREATOR_FEE_EARNED_EVENT_NAME,
271+
creator_id.clone(),
272+
co_creator.clone(),
273+
)
274+
}
275+
249276
/// Event name for key transfer.
250277
pub const KEYS_TRANSFERRED_EVENT_NAME: Symbol = symbol_short!("xfer");
251278

creator-keys/src/lib.rs

Lines changed: 175 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

8386
pub 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]
536575
pub 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+
629735
fn 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

Comments
 (0)