Skip to content
Open
73 changes: 73 additions & 0 deletions creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,17 @@ pub struct RoyaltyConfig {
pub sell_fee_bps: u32,
}

/// Lifecycle state for a creator's archive/restore flow (issue #709).
///
/// Absent storage entries default to `Active`, keeping storage sparse.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[contracttype]
pub enum CreatorLifecycleState {
Active = 0,
Archived = 1,
Restoring = 2,
}

/// Result of a single order in a batch buy.
#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
Expand Down Expand Up @@ -2043,6 +2054,29 @@ fn read_curve_exponent(env: &Env, creator: &Address) -> Option<u32> {
.get(&constants::storage::curve_exponent(creator))
}

/// Reads a creator's lifecycle state, defaulting to [`CreatorLifecycleState::Active`]
/// when no lifecycle entry exists.
pub fn read_creator_lifecycle(env: &Env, creator: &Address) -> CreatorLifecycleState {
env.storage()
.persistent()
.get(&constants::storage::creator_lifecycle(creator))
.unwrap_or(CreatorLifecycleState::Active)
}

/// Guard rejecting trades for creators whose state is `Archived` or `Restoring`.
///
/// Read-only views intentionally bypass this guard.
fn assert_creator_lifecycle_allows_trading(
env: &Env,
creator: &Address,
) -> Result<(), ContractError> {
match read_creator_lifecycle(env, creator) {
CreatorLifecycleState::Active => Ok(()),
CreatorLifecycleState::Archived => Err(ContractError::CreatorArchived),
CreatorLifecycleState::Restoring => Err(ContractError::StateRestoring),
}
}

fn compute_bonding_curve_price(
env: &Env,
creator: &Address,
Expand Down Expand Up @@ -2545,6 +2579,7 @@ impl CreatorKeysContract {
assert_not_paused(&env)?;
assert_not_blacklisted(&env, &buyer)?;
assert_before_global_deadline(&env)?;
assert_creator_lifecycle_allows_trading(&env, &creator)?;

if payment <= 0 {
return Err(ContractError::NotPositiveAmount);
Expand Down Expand Up @@ -2851,6 +2886,7 @@ impl CreatorKeysContract {
assert_global_trading_not_halted(&env)?;
assert_not_paused(&env)?;
assert_not_blacklisted(&env, &seller)?;
assert_creator_lifecycle_allows_trading(&env, &creator)?;

let mut profile: CreatorProfile = read_registered_creator_profile(&env, &creator)?;

Expand Down Expand Up @@ -3042,6 +3078,7 @@ impl CreatorKeysContract {
) -> Result<u32, ContractError> {
caller.require_auth();
assert_not_paused(&env)?;
assert_creator_lifecycle_allows_trading(&env, &creator)?;

if caller != creator {
return Err(ContractError::Unauthorized);
Expand Down Expand Up @@ -3962,6 +3999,42 @@ impl CreatorKeysContract {
}
}

/// Re-extends the TTL of all known global entries plus the scoped entries
/// of the supplied creators in a single admin call.
///
/// Every global storage key (fee config, key price, treasury address and
/// balance, protocol fee rate) plus each listed creator's profile key is
/// pushed out to the full [`CREATOR_TTL_LEDGERS`] window, guaranteeing the
/// contract keeps serving these entries for at least
/// [`TTL_MIN_EXTENSION_LEDGERS`].
///
/// Only callable by an authorized admin; any other caller receives
/// [`ContractError::Unauthorized`].
pub fn refresh_ttl(
env: Env,
admin: Address,
creators: Vec<Address>,
) -> Result<(), ContractError> {
admin.require_auth();
assert_is_admin(&env, &admin)?;

for key in [
constants::storage::FEE_CONFIG,
constants::storage::KEY_PRICE,
constants::storage::TREASURY_ADDRESS,
constants::storage::TREASURY_BALANCE,
constants::storage::PROTOCOL_FEE_BPS,
] {
extend_key_ttl_to_full_window(&env, &key);
}

for creator in creators.iter() {
extend_key_ttl_to_full_window(&env, &constants::storage::creator(&creator));
}

Ok(())
}

/// Sets the protocol admin address.
///
/// Only callable by an authorized admin. Stores the admin address used
Expand Down
19 changes: 11 additions & 8 deletions creator-keys/src/test_new_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,18 @@ fn test_circuit_breaker_threshold_configuration_and_trigger() {
let creator = Address::generate(&env);
register_creator(&env, &client, &creator);

// Default threshold is 30%.
// Buy 1: supply 0 -> 1. Price moves from base_price (100) to 200 (100% increase > 30%).
// Set a slope so price increases with supply, enabling circuit breaker to fire.
// With slope=100 and base_price=100: price at supply 0 = 100, supply 1 = 200 (100% increase).
client.set_curve_slope(&admin, &100i128);

// Default threshold is 30%. First buy: supply 0->1, pre_price=100, post_price=200 (100% > 30%).
let buyer = Address::generate(&env);
let result = client.try_buy_key(&creator, &buyer, &1000i128, &None);
assert_eq!(result, Err(Ok(ContractError::CircuitBreakerTriggered)));

// Admin sets threshold to 200% (200)
// Admin raises threshold to 200%. Price delta (100%) < 200%, so buy succeeds.
client.set_circuit_breaker_threshold(&admin, &200u32);

// Now buy succeeds because price delta (100%) < 200% threshold
let supply = client.buy_key(&creator, &buyer, &1000i128, &None);
assert_eq!(supply, 1);
}
Expand Down Expand Up @@ -81,8 +83,9 @@ fn test_referral_system_fee_split_and_validation() {
);
assert_eq!(res_creator_ref, Err(Ok(ContractError::InvalidReferrer)));

// Valid referral buy
// Price at supply 0 is 100. Protocol fee at 10% (1000 bps) is 10.
// Valid referral buy.
// Slope defaults to 0, so price stays flat at 100 regardless of supply.
// Protocol fee at 10% (1000 bps) of 100 = 10.
// Treasury gets 50% (5), referrer gets 50% (5).
let treasury_bal_before = client.get_treasury_balance();
client.buy_key_with_referrer(&creator, &buyer, &1000i128, &None, &Some(referrer.clone()));
Expand All @@ -93,12 +96,12 @@ fn test_referral_system_fee_split_and_validation() {
let ref_earnings = client.get_referral_earnings(&referrer);
assert_eq!(ref_earnings, 5);

// Buy without referrer sends full protocol fee (20) to treasury (price at supply 1 is 200, 10% = 20)
// Buy without referrer: price still 100 (flat curve), protocol fee = 10, all to treasury.
let buyer2 = Address::generate(&env);
let treasury_bal_before2 = client.get_treasury_balance();
client.buy_key(&creator, &buyer2, &1000i128, &None);
let treasury_bal_after2 = client.get_treasury_balance();
assert_eq!(treasury_bal_after2 - treasury_bal_before2, 20);
assert_eq!(treasury_bal_after2 - treasury_bal_before2, 10);
}

#[test]
Expand Down
Loading
Loading