Skip to content
153 changes: 152 additions & 1 deletion creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -65,6 +65,9 @@ pub enum ContractError {
SlippageExceeded = 16,
ProtocolPaused = 17,
Unauthorized = 18,
ZeroClaimable = 19,
NoHolders = 20,
DividendAmountZero = 21,
}

pub mod fee {
Expand Down Expand Up @@ -368,6 +371,8 @@ pub enum DataKey {
CreatorFeeBalance(Address),
ProtocolStateVersion,
Paused,
HoldersList(Address),
DividendClaimable(Address, Address),
}

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1437,6 +1565,29 @@ impl CreatorKeysContract {
}
}

fn read_holders_list(env: &Env, creator: &Address) -> Vec<Address> {
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<Address>) {
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;
Expand Down
Loading