diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index 7323609..dcd149a 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -1,7 +1,7 @@ #![no_std] pub mod quote_view_errors; -use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, String}; +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, String, Vec}; pub mod events; @@ -65,6 +65,9 @@ pub enum ContractError { SlippageExceeded = 16, ProtocolPaused = 17, Unauthorized = 18, + ZeroClaimable = 19, + NoHolders = 20, + DividendAmountZero = 21, } pub mod fee { @@ -368,6 +371,8 @@ pub enum DataKey { CreatorFeeBalance(Address), ProtocolStateVersion, Paused, + HoldersList(Address), + DividendClaimable(Address, Address), } #[derive(Clone, Debug, PartialEq)] @@ -794,6 +799,11 @@ impl CreatorKeysContract { .checked_add(1) .ok_or(ContractError::Overflow)?; } + let mut holders = read_holders_list(&env, &creator); + if !holders.iter().any(|h| h == buyer) { + holders.push_back(buyer.clone()); + write_holders_list(&env, &creator, &holders); + } profile.supply = profile .supply @@ -859,6 +869,11 @@ impl CreatorKeysContract { .holder_count .checked_sub(1) .ok_or(ContractError::SellUnderflow)?; + let mut holders = read_holders_list(&env, &creator); + if let Some(pos) = holders.iter().position(|h| h == seller) { + holders.remove(pos as u32); + write_holders_list(&env, &creator, &holders); + } } let key = constants::storage::creator(&creator); @@ -969,6 +984,118 @@ impl CreatorKeysContract { } } + pub fn distribute_dividend( + env: Env, + creator: Address, + amount: i128, + ) -> Result<(), ContractError> { + let caller = env.caller(); // use caller() instead of invoker() + let native_address = Address::from_str( + &env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + ); + let native_token = soroban_sdk::token::TokenClient::new(&env, &native_address); + native_token.transfer(&caller, &env.current_contract_address(), &amount); + + + // Load creator profile and validate + let profile = read_registered_creator_profile(&env, &creator)?; + let total_supply = profile.supply as i128; + if total_supply == 0 { + return Err(ContractError::NoHolders); + } + + // Load fee config and compute protocol fee + let config = read_required_protocol_fee_config(&env)?; + let protocol_fee = fee::apply_percentage_fee(amount, config.protocol_bps) + .ok_or(ContractError::Overflow)?; + let distribute_amount = amount + .checked_sub(protocol_fee) + .ok_or(ContractError::Overflow)?; + if distribute_amount <= 0 { + return Err(ContractError::DividendAmountZero); + } + + // Credit protocol fee to the protocol fee recipient balance + credit_protocol_fee_recipient_balance(&env, protocol_fee)?; + + // Iterate over all holders and distribute proportionally + let holders = read_holders_list(&env, &creator); + let ledger = env.ledger().sequence(); + + for holder in holders.iter() { + let balance = + Self::get_key_balance(env.clone(), creator.clone(), holder.clone()) as i128; + if balance == 0 { + continue; + } + let share = (balance * distribute_amount) / total_supply; + if share > 0 { + let current = read_claimable_dividend(&env, &creator, &holder); + let new_claim = current.checked_add(share).ok_or(ContractError::Overflow)?; + write_claimable_dividend(&env, &creator, &holder, new_claim); + } + } + + // Emit event + env.events().publish( + events::dividend_distributed_topics(&creator), + events::DividendDistributedEvent { + creator_id: creator, + total_amount: amount, + snapshot_supply: total_supply, + ledger, + protocol_fee, + distributed_amount: distribute_amount, + }, + ); + + Ok(()) + } + + /// Claims accrued dividends for the caller. + /// + /// Transfers the claimable amount from the contract to the caller's wallet. + /// Resets the claimable balance to zero. + /// Emits a `DividendClaimed` event. + pub fn claim_dividend(env: Env, creator: Address) -> Result<(), ContractError> { + let caller = env.caller(); + + // 1. Read claimable balance + let claimable = read_claimable_dividend(&env, &creator, &caller); + if claimable == 0 { + return Err(ContractError::ZeroClaimable); + } + + // 2. Reset claimable to zero before transfer (reentrancy-safe) + write_claimable_dividend(&env, &creator, &caller, 0); + + // 3. Transfer XLM from contract to caller + let native_address = Address::from_str( + &env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + ); + let native_token = soroban_sdk::token::TokenClient::new(&env, &native_address); + native_token.transfer(&env.current_contract_address(), &caller, &claimable); + + // 4. Emit event + env.events().publish( + events::dividend_claimed_topics(&creator, &caller), + events::DividendClaimedEvent { + creator_id: creator, + claimant: caller, + amount: claimable, + }, + ); + + Ok(()) + } + + /// Returns the unclaimed dividend balance for a specific (creator, holder) pair. + pub fn get_claimable_dividend(env: Env, creator: Address, wallet: Address) -> i128 { + read_claimable_dividend(&env, &creator, &wallet) + } + /// Read-only batch view: returns [`CreatorDetailsView`] for each address in `creators`. /// /// Iterates the provided addresses in order and fetches each creator's profile @@ -1018,6 +1145,7 @@ impl CreatorKeysContract { } results } + /// Read-only view: returns the protocol state version. /// /// Returns a stable scalar value for clients and indexers to detect @@ -1437,6 +1565,29 @@ impl CreatorKeysContract { } } +fn read_holders_list(env: &Env, creator: &Address) -> Vec
{ + let key = DataKey::HoldersList(creator.clone()); + env.storage() + .persistent() + .get(&key) + .unwrap_or(Vec::new(env)) +} + +fn write_holders_list(env: &Env, creator: &Address, holders: &Vec) { + let key = DataKey::HoldersList(creator.clone()); + env.storage().persistent().set(&key, holders); +} + +fn read_claimable_dividend(env: &Env, creator: &Address, holder: &Address) -> i128 { + let key = DataKey::DividendClaimable(creator.clone(), holder.clone()); + env.storage().persistent().get(&key).unwrap_or(0) +} + +fn write_claimable_dividend(env: &Env, creator: &Address, holder: &Address, amount: i128) { + let key = DataKey::DividendClaimable(creator.clone(), holder.clone()); + env.storage().persistent().set(&key, &amount); +} + #[cfg(test)] mod tests { use super::fee;