Skip to content
Merged
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
1 change: 1 addition & 0 deletions .freebuff/project-id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a16e0d91-aeb4-469c-b8d2-61adab299ccc
4 changes: 4 additions & 0 deletions creator-keys/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2167,6 +2167,10 @@ pub struct KeyDeprecatedEvent {
pub circulating_supply: u32,
/// Total XLM escrowed (`circulating_supply * buyback_price_per_key`).
pub total_escrow: i128,
/// Human-readable reason supplied by the creator (#973).
pub reason: String,
/// Optional successor key address (#973). `None` when not designated.
pub successor_key_id: Option<Address>,
/// Ledger sequence number at the time of deprecation.
pub ledger: u32,
}
Expand Down
113 changes: 112 additions & 1 deletion creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,11 @@ pub mod constants {
DataKey::DeprecationEscrow(creator.clone())
}

/// Storage key for the rich deprecation metadata record (#973).
pub fn deprecation_info(creator: &Address) -> DataKey {
DataKey::DeprecationInfo(creator.clone())
}

/// Storage key for the price-oracle approved-caller allowlist.
pub const APPROVED_CALLERS: DataKey = DataKey::ApprovedCallers;

Expand Down Expand Up @@ -1660,6 +1665,9 @@ pub enum DataKey {
/// Escrow balance held on behalf of a deprecated key's creator.
/// Funds are paid out to redeeming holders and any remainder is returned on full redemption.
DeprecationEscrow(Address),
/// Rich deprecation metadata written by `deprecate_key` (#973).
/// Value is a `DeprecationInfo` struct holding reason, optional successor, and timestamp.
DeprecationInfo(Address),
/// Configured early exit penalty bps for key.
EarlyExitPenaltyBps(Address),
/// Maximum buy quantity per transaction for a creator.
Expand Down Expand Up @@ -2234,6 +2242,40 @@ pub struct AirdropSummary {
pub skipped_count: u32,
}

// =========================================================================
// #973 — Key deprecation and successor designation
// =========================================================================

/// Persistent storage record written by `deprecate_key` (#973).
///
/// Stored at `DataKey::DeprecationInfo(creator)` in addition to the existing
/// `DataKey::DeprecatedKey` marker used by the escrow-buyback path (#834).
#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub struct DeprecationInfo {
/// Human-readable reason supplied by the creator (max 256 bytes).
pub reason: String,
/// Optional address of the successor key the creator recommends holders
/// migrate to. `None` means no successor has been designated.
pub successor_key_id: Option<Address>,
/// Ledger sequence at which `deprecate_key` was called.
pub deprecated_at_ledger: u32,
}

/// Return type for `get_deprecation_status` (#973).
#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub struct DeprecationStatus {
/// `true` if the key has been deprecated.
pub is_deprecated: bool,
/// Human-readable reason (empty string when `is_deprecated` is `false`).
pub reason: String,
/// Optional successor key address; `None` when not deprecated or not set.
pub successor_key_id: Option<Address>,
/// Ledger sequence of deprecation; `0` when not deprecated.
pub deprecated_at_ledger: u32,
}

fn validate_whitelist_config(config: &WhitelistConfig) -> Result<(), ContractError> {
if config.addresses.len() > MAX_WHITELIST_SIZE {
return Err(ContractError::WhitelistTooLarge);
Expand Down Expand Up @@ -6435,6 +6477,7 @@ impl CreatorKeysContract {

// =========================================================================
// #834 — Key deprecation and holder buybacks
// #973 — Key deprecation reason and successor designation
// =========================================================================

/// Deprecates a creator key, disabling new buys and initiating an orderly
Expand All @@ -6445,6 +6488,10 @@ impl CreatorKeysContract {
/// [`CreatorKeysContract::redeem`] to exchange their keys for the fixed
/// buyback price.
///
/// `reason` is a human-readable string (max 256 bytes) explaining why the
/// key is being deprecated. `successor_key_id` is an optional address of a
/// replacement key the creator recommends holders migrate to.
///
/// # Errors
///
/// - [`ContractError::Unauthorized`] if `caller != creator`.
Expand All @@ -6460,6 +6507,8 @@ impl CreatorKeysContract {
caller: Address,
buyback_price_per_key: i128,
escrow_payment: i128,
reason: String,
successor_key_id: Option<Address>,
) -> Result<(), ContractError> {
caller.require_auth();
assert_not_paused(&env)?;
Expand All @@ -6471,6 +6520,11 @@ impl CreatorKeysContract {
return Err(ContractError::NotPositiveAmount);
}

// Reject reason strings longer than 256 bytes.
if reason.len() > 256 {
return Err(ContractError::BioTooLong);
}

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

// Reject if already deprecated.
Expand All @@ -6492,6 +6546,8 @@ impl CreatorKeysContract {
return Err(ContractError::InsufficientEscrow);
}

let current_ledger = env.ledger().sequence();

// Persist the deprecation marker (stores the fixed buyback price).
let dep_key = constants::storage::deprecated_key(&creator);
env.storage()
Expand All @@ -6507,14 +6563,28 @@ impl CreatorKeysContract {
.set(&escrow_key, &required_escrow);
extend_key_ttl_to_full_window(&env, &escrow_key);

// Persist the rich deprecation metadata (#973).
let info_key = constants::storage::deprecation_info(&creator);
env.storage().persistent().set(
&info_key,
&DeprecationInfo {
reason: reason.clone(),
successor_key_id: successor_key_id.clone(),
deprecated_at_ledger: current_ledger,
},
);
extend_key_ttl_to_full_window(&env, &info_key);

env.events().publish(
events::key_deprecated_topics(&creator),
events::KeyDeprecatedEvent {
creator: creator.clone(),
buyback_price_per_key,
circulating_supply,
total_escrow: required_escrow,
ledger: env.ledger().sequence(),
reason,
successor_key_id,
ledger: current_ledger,
},
);

Expand All @@ -6530,6 +6600,47 @@ impl CreatorKeysContract {
Ok(())
}

/// Returns the deprecation status for a creator key (#973).
///
/// Always succeeds — if the key is not deprecated, returns a
/// `DeprecationStatus` with `is_deprecated = false` and zeroed fields.
pub fn get_deprecation_status(env: Env, creator: Address) -> DeprecationStatus {
let is_deprecated = env
.storage()
.persistent()
.has(&constants::storage::deprecated_key(&creator));

if !is_deprecated {
return DeprecationStatus {
is_deprecated: false,
reason: String::from_str(&env, ""),
successor_key_id: None,
deprecated_at_ledger: 0,
};
}

let info: Option<DeprecationInfo> = env
.storage()
.persistent()
.get(&constants::storage::deprecation_info(&creator));

match info {
Some(d) => DeprecationStatus {
is_deprecated: true,
reason: d.reason,
successor_key_id: d.successor_key_id,
deprecated_at_ledger: d.deprecated_at_ledger,
},
// Fallback: key was deprecated via the old path without DeprecationInfo.
None => DeprecationStatus {
is_deprecated: true,
reason: String::from_str(&env, ""),
successor_key_id: None,
deprecated_at_ledger: 0,
},
}
}

/// Redeems all keys held by `holder` for a deprecated creator key.
///
/// Transfers `holder_balance * buyback_price_per_key` XLM from the escrow
Expand Down
Loading
Loading