Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 108 additions & 3 deletions creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ pub enum ContractError {
InsufficientSupply = 25,
SelfTransfer = 26,
ZeroTransferAmount = 27,
InsufficientTreasuryBalance = 28,
BatchClaimExceedsLimit = 29,
InvalidCoCreatorShare = 30,
WhitelistOnly = 28,
WhitelistTooLarge = 29,
InsufficientTreasuryBalance = 30,
BatchClaimExceedsLimit = 31,
InvalidCoCreatorShare = 32,
}

pub mod fee {
Expand Down Expand Up @@ -313,6 +315,10 @@ pub mod constants {
DataKey::CoCreatorFeeBalance(creator.clone(), co_creator.clone())
}

pub fn whitelist(creator: &Address) -> DataKey {
DataKey::Whitelist(creator.clone())
}

pub fn creator(creator: &Address) -> DataKey {
creator_key(creator)
}
Expand Down Expand Up @@ -482,6 +488,9 @@ pub const CREATOR_TTL_LEDGERS: u32 = 6311520; // ~2 years at 5s per ledger
pub const HANDLE_LEN_MIN: u32 = 3;
pub const HANDLE_LEN_MAX: u32 = 32;

/// Maximum number of addresses accepted in a creator whitelist.
pub const MAX_WHITELIST_SIZE: u32 = 500;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[contracttype]
pub enum CurvePreset {
Expand Down Expand Up @@ -518,6 +527,7 @@ pub enum DataKey {
TreasuryBalance,
CoCreator(Address),
CoCreatorFeeBalance(Address, Address),
Whitelist(Address),
}

/// Time-locked key allocation for creator self-vesting.
Expand All @@ -543,6 +553,21 @@ pub struct CoCreatorConfig {
pub share_bps: u32,
}

#[derive(Clone, Debug, PartialEq)]
#[contracttype]
pub struct WhitelistConfig {
pub addresses: soroban_sdk::Vec<Address>,
pub window_ledgers: u32,
}

#[derive(Clone, Debug, PartialEq)]
#[contracttype]
pub struct WhitelistStatus {
pub active: bool,
pub expires_at_ledger: u32,
pub remaining_ledgers: u32,
}

#[derive(Clone, Debug, PartialEq)]
#[contracttype]
pub struct CreatorProfile {
Expand Down Expand Up @@ -1159,6 +1184,55 @@ fn extend_creator_ttl(env: &Env, creator: &Address) {
}
}

fn whitelist_status_for_profile(
env: &Env,
creator: &Address,
registered_at: u32,
) -> WhitelistStatus {
let Some(config) = env
.storage()
.persistent()
.get::<DataKey, WhitelistConfig>(&constants::storage::whitelist(creator))
else {
return WhitelistStatus {
active: false,
expires_at_ledger: 0,
remaining_ledgers: 0,
};
};
let expires_at_ledger = registered_at.saturating_add(config.window_ledgers);
let current = env.ledger().sequence();
let remaining_ledgers = expires_at_ledger.saturating_sub(current);
WhitelistStatus {
active: remaining_ledgers > 0,
expires_at_ledger,
remaining_ledgers,
}
}

fn enforce_whitelist_window(
env: &Env,
profile: &CreatorProfile,
creator: &Address,
buyer: &Address,
) -> Result<(), ContractError> {
let status = whitelist_status_for_profile(env, creator, profile.registered_at);
if !status.active {
return Ok(());
}
let config: WhitelistConfig = env
.storage()
.persistent()
.get(&constants::storage::whitelist(creator))
.ok_or(ContractError::WhitelistOnly)?;
for address in config.addresses.iter() {
if address == *buyer {
return Ok(());
}
}
Err(ContractError::WhitelistOnly)
}

#[contract]
pub struct CreatorKeysContract;

Expand Down Expand Up @@ -1189,6 +1263,7 @@ impl CreatorKeysContract {
max_supply: Option<u32>,
curve_preset: Option<CurvePreset>,
co_creator: Option<CoCreatorConfig>,
whitelist: Option<WhitelistConfig>,
) -> Result<(), ContractError> {
creator.require_auth();
assert_not_paused(&env)?;
Expand All @@ -1197,6 +1272,11 @@ impl CreatorKeysContract {
if let Some(config) = co_creator.as_ref() {
validate_co_creator_config(&env, config)?;
}
if let Some(config) = whitelist.as_ref() {
if config.addresses.len() > MAX_WHITELIST_SIZE {
return Err(ContractError::WhitelistTooLarge);
}
}

let key = constants::storage::creator(&creator);
// Creator profile storage is a single source of truth keyed by creator address.
Expand Down Expand Up @@ -1262,6 +1342,12 @@ impl CreatorKeysContract {
.set(&constants::storage::co_creator(&creator), &config);
}

if let Some(config) = whitelist {
env.storage()
.persistent()
.set(&constants::storage::whitelist(&creator), &config);
}

let profile = CreatorProfile {
creator: creator.clone(),
handle,
Expand Down Expand Up @@ -1293,6 +1379,12 @@ impl CreatorKeysContract {
.persistent()
.extend_ttl(&co_creator_key, current_ledger, extend_to);
}
let whitelist_key = constants::storage::whitelist(&creator);
if env.storage().persistent().has(&whitelist_key) {
env.storage()
.persistent()
.extend_ttl(&whitelist_key, current_ledger, extend_to);
}

env.events().publish(
events::register_event_topics(&profile.creator),
Expand Down Expand Up @@ -1330,6 +1422,7 @@ impl CreatorKeysContract {
.ok_or(ContractError::KeyPriceNotSet)?;

let mut profile: CreatorProfile = read_registered_creator_profile(&env, &creator)?;
enforce_whitelist_window(&env, &profile, &creator, &buyer)?;
let price = compute_bonding_curve_price(&env, &creator, base_price, profile.supply)?;

assert_buy_price_slippage(price, max_price)?;
Expand Down Expand Up @@ -1743,6 +1836,18 @@ impl CreatorKeysContract {
read_key_balance(&env, &creator)
}

/// Read-only view: returns whitelist window state for a creator.
pub fn get_whitelist_status(env: Env, creator: Address) -> WhitelistStatus {
let Some(profile) = read_creator_profile(&env, &creator) else {
return WhitelistStatus {
active: false,
expires_at_ledger: 0,
remaining_ledgers: 0,
};
};
whitelist_status_for_profile(&env, &creator, profile.registered_at)
}

/// Read-only view: returns the current supply for a registered creator.
///
/// Fails with [`ContractError::NotRegistered`] if the creator does not exist.
Expand Down
Loading
Loading