Skip to content

Commit 5ec99ba

Browse files
authored
Merge pull request #839 from DammyAji/feat/834-key-deprecation-buyback
feat: add deprecate_key and redeem for holder buybacks
2 parents ba45cee + 305635f commit 5ec99ba

3 files changed

Lines changed: 615 additions & 1 deletion

File tree

creator-keys/src/events.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,3 +1666,82 @@ pub struct AuctionCancelledEvent {
16661666
pub fn auction_cancelled_topics(creator: &Address) -> (Symbol, Address) {
16671667
(AUCTION_CANCELLED_EVENT_NAME, creator.clone())
16681668
}
1669+
1670+
// --- Key deprecation and holder buyback events (#834) ---
1671+
1672+
/// Event name emitted when a creator deprecates their key.
1673+
pub const KEY_DEPRECATED_EVENT_NAME: Symbol = symbol_short!("key_dep");
1674+
1675+
/// Event name emitted when a holder redeems keys on a deprecated key.
1676+
pub const KEYS_REDEEMED_EVENT_NAME: Symbol = symbol_short!("key_rdm");
1677+
1678+
/// Stable field order for the key_deprecated event payload.
1679+
pub const KEY_DEPRECATED_DATA_FIELDS: [&str; 5] = [
1680+
"creator",
1681+
"buyback_price_per_key",
1682+
"circulating_supply",
1683+
"total_escrow",
1684+
"ledger",
1685+
];
1686+
1687+
/// Stable field order for the keys_redeemed event payload.
1688+
pub const KEYS_REDEEMED_DATA_FIELDS: [&str; 6] = [
1689+
"creator",
1690+
"holder",
1691+
"quantity",
1692+
"payout",
1693+
"new_supply",
1694+
"ledger",
1695+
];
1696+
1697+
/// Stable key-deprecated event payload for downstream indexers.
1698+
///
1699+
/// Event shape:
1700+
/// - topics: `(KEY_DEPRECATED_EVENT_NAME, creator)`
1701+
/// - data: `KeyDeprecatedEvent`
1702+
#[derive(Clone, Debug, Eq, PartialEq)]
1703+
#[contracttype]
1704+
pub struct KeyDeprecatedEvent {
1705+
/// Creator who deprecated their key.
1706+
pub creator: Address,
1707+
/// Fixed XLM payout per key for all redemptions.
1708+
pub buyback_price_per_key: i128,
1709+
/// Circulating supply at the time of deprecation.
1710+
pub circulating_supply: u32,
1711+
/// Total XLM escrowed (`circulating_supply * buyback_price_per_key`).
1712+
pub total_escrow: i128,
1713+
/// Ledger sequence number at the time of deprecation.
1714+
pub ledger: u32,
1715+
}
1716+
1717+
/// Shared key-deprecated event topics tuple.
1718+
pub fn key_deprecated_topics(creator: &Address) -> (Symbol, Address) {
1719+
(KEY_DEPRECATED_EVENT_NAME, creator.clone())
1720+
}
1721+
1722+
/// Stable keys-redeemed event payload for downstream indexers.
1723+
///
1724+
/// Event shape:
1725+
/// - topics: `(KEYS_REDEEMED_EVENT_NAME, creator, holder)`
1726+
/// - data: `KeysRedeemedEvent`
1727+
#[derive(Clone, Debug, Eq, PartialEq)]
1728+
#[contracttype]
1729+
pub struct KeysRedeemedEvent {
1730+
/// Creator whose deprecated key is being redeemed.
1731+
pub creator: Address,
1732+
/// Holder who redeemed their keys.
1733+
pub holder: Address,
1734+
/// Number of keys redeemed by the holder.
1735+
pub quantity: u32,
1736+
/// Total XLM payout transferred to the holder.
1737+
pub payout: i128,
1738+
/// Total key supply for the creator after this redemption.
1739+
pub new_supply: u32,
1740+
/// Ledger sequence number at the time of redemption.
1741+
pub ledger: u32,
1742+
}
1743+
1744+
/// Shared keys-redeemed event topics tuple.
1745+
pub fn keys_redeemed_topics(creator: &Address, holder: &Address) -> (Symbol, Address, Address) {
1746+
(KEYS_REDEEMED_EVENT_NAME, creator.clone(), holder.clone())
1747+
}

creator-keys/src/lib.rs

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ pub enum ContractError {
8989
NameTooLong = 60,
9090
BioTooLong = 61,
9191
KeyAlreadyInitialised = 62,
92+
/// The key has been deprecated by its creator; new buys are no longer accepted.
93+
KeyDeprecated = 63,
94+
/// The creator did not provide enough XLM to cover the full buyback escrow
95+
/// (`circulating_supply * buyback_price_per_key`).
96+
InsufficientEscrow = 64,
9297
}
9398

9499
/// Errors raised by the staking lifecycle entrypoints
@@ -600,8 +605,17 @@ pub mod constants {
600605
pub fn auction_config(creator: &Address) -> DataKey {
601606
DataKey::AuctionConfig(creator.clone())
602607
}
603-
}
604608

609+
/// Storage key for a creator's deprecation marker; value is `buyback_price_per_key` (i128).
610+
pub fn deprecated_key(creator: &Address) -> DataKey {
611+
DataKey::DeprecatedKey(creator.clone())
612+
}
613+
614+
/// Storage key for the escrow balance held for a deprecated key's buyback pool.
615+
pub fn deprecation_escrow(creator: &Address) -> DataKey {
616+
DataKey::DeprecationEscrow(creator.clone())
617+
}
618+
}
605619
fn creator_key(creator: &Address) -> DataKey {
606620
DataKey::Creator(creator.clone())
607621
}
@@ -1034,6 +1048,11 @@ pub enum DataKey {
10341048
/// Per-creator buy cooldown in ledgers. A value of `0` (or absent) means
10351049
/// no cooldown is configured. Set via `set_buy_cooldown`.
10361050
BuyCooldown(Address),
1051+
/// Marks a creator key as deprecated. Value is the fixed `buyback_price_per_key` (i128).
1052+
DeprecatedKey(Address),
1053+
/// Escrow balance held on behalf of a deprecated key's creator.
1054+
/// Funds are paid out to redeeming holders and any remainder is returned on full redemption.
1055+
DeprecationEscrow(Address),
10371056
}
10381057

10391058
#[derive(Clone, Debug, PartialEq)]
@@ -2747,6 +2766,15 @@ impl CreatorKeysContract {
27472766
assert_not_blacklisted(&env, &buyer)?;
27482767
assert_before_global_deadline(&env)?;
27492768

2769+
// Reject buys on deprecated keys immediately — before any price or fee math.
2770+
if env
2771+
.storage()
2772+
.persistent()
2773+
.has(&constants::storage::deprecated_key(&creator))
2774+
{
2775+
return Err(ContractError::KeyDeprecated);
2776+
}
2777+
27502778
if payment <= 0 {
27512779
return Err(ContractError::NotPositiveAmount);
27522780
}
@@ -3377,6 +3405,185 @@ impl CreatorKeysContract {
33773405
Ok(profile.supply)
33783406
}
33793407

3408+
// =========================================================================
3409+
// #834 — Key deprecation and holder buybacks
3410+
// =========================================================================
3411+
3412+
/// Deprecates a creator key, disabling new buys and initiating an orderly
3413+
/// shutdown via a fixed-price holder buyback.
3414+
///
3415+
/// The creator must escrow `circulating_supply * buyback_price_per_key` XLM
3416+
/// (`escrow_payment`) at the time of calling. Holders can then call
3417+
/// [`CreatorKeysContract::redeem`] to exchange their keys for the fixed
3418+
/// buyback price.
3419+
///
3420+
/// # Errors
3421+
///
3422+
/// - [`ContractError::Unauthorized`] if `caller != creator`.
3423+
/// - [`ContractError::NotRegistered`] if the creator is not registered.
3424+
/// - [`ContractError::NotPositiveAmount`] if `buyback_price_per_key <= 0`.
3425+
/// - [`ContractError::KeyDeprecated`] if the key is already deprecated.
3426+
/// - [`ContractError::InsufficientEscrow`] if `escrow_payment` is less than
3427+
/// `circulating_supply * buyback_price_per_key`.
3428+
/// - [`ContractError::ProtocolPaused`] if the contract is paused.
3429+
pub fn deprecate_key(
3430+
env: Env,
3431+
creator: Address,
3432+
caller: Address,
3433+
buyback_price_per_key: i128,
3434+
escrow_payment: i128,
3435+
) -> Result<(), ContractError> {
3436+
caller.require_auth();
3437+
assert_not_paused(&env)?;
3438+
3439+
if caller != creator {
3440+
return Err(ContractError::Unauthorized);
3441+
}
3442+
if buyback_price_per_key <= 0 {
3443+
return Err(ContractError::NotPositiveAmount);
3444+
}
3445+
3446+
let profile = read_registered_creator_profile(&env, &creator)?;
3447+
3448+
// Reject if already deprecated.
3449+
if env
3450+
.storage()
3451+
.persistent()
3452+
.has(&constants::storage::deprecated_key(&creator))
3453+
{
3454+
return Err(ContractError::KeyDeprecated);
3455+
}
3456+
3457+
// Compute the required escrow: circulating_supply * buyback_price_per_key.
3458+
let circulating_supply = profile.supply;
3459+
let required_escrow = (circulating_supply as i128)
3460+
.checked_mul(buyback_price_per_key)
3461+
.ok_or(ContractError::Overflow)?;
3462+
3463+
if escrow_payment < required_escrow {
3464+
return Err(ContractError::InsufficientEscrow);
3465+
}
3466+
3467+
// Persist the deprecation marker (stores the fixed buyback price).
3468+
let dep_key = constants::storage::deprecated_key(&creator);
3469+
env.storage()
3470+
.persistent()
3471+
.set(&dep_key, &buyback_price_per_key);
3472+
extend_key_ttl_to_full_window(&env, &dep_key);
3473+
3474+
// Persist the escrow balance (capped at required_escrow; any overpayment
3475+
// is treated as excess and not credited to the escrow pool).
3476+
let escrow_key = constants::storage::deprecation_escrow(&creator);
3477+
env.storage()
3478+
.persistent()
3479+
.set(&escrow_key, &required_escrow);
3480+
extend_key_ttl_to_full_window(&env, &escrow_key);
3481+
3482+
env.events().publish(
3483+
events::key_deprecated_topics(&creator),
3484+
events::KeyDeprecatedEvent {
3485+
creator: creator.clone(),
3486+
buyback_price_per_key,
3487+
circulating_supply,
3488+
total_escrow: required_escrow,
3489+
ledger: env.ledger().sequence(),
3490+
},
3491+
);
3492+
3493+
Ok(())
3494+
}
3495+
3496+
/// Redeems all keys held by `holder` for a deprecated creator key.
3497+
///
3498+
/// Transfers `holder_balance * buyback_price_per_key` XLM from the escrow
3499+
/// pool to the holder, burns the holder's keys, and decrements the supply.
3500+
///
3501+
/// # Errors
3502+
///
3503+
/// - [`ContractError::NotRegistered`] if the creator is not registered.
3504+
/// - [`ContractError::KeyDeprecated`] is **not** returned here — it is the
3505+
/// *required* condition. The function returns [`ContractError::NotRegistered`]
3506+
/// when the key has not been deprecated (reusing `NotRegistered` to mean
3507+
/// "the deprecation record does not exist").
3508+
/// - [`ContractError::InsufficientBalance`] if the holder has no keys.
3509+
/// - [`ContractError::InsufficientEscrow`] if the escrow pool is unexpectedly
3510+
/// short (should not happen under normal conditions).
3511+
/// - [`ContractError::ProtocolPaused`] if the contract is paused.
3512+
pub fn redeem(env: Env, creator: Address, holder: Address) -> Result<i128, ContractError> {
3513+
holder.require_auth();
3514+
assert_not_paused(&env)?;
3515+
3516+
// The key must be deprecated before holders can redeem.
3517+
let dep_key = constants::storage::deprecated_key(&creator);
3518+
let buyback_price_per_key: i128 = env
3519+
.storage()
3520+
.persistent()
3521+
.get(&dep_key)
3522+
.ok_or(ContractError::NotRegistered)?;
3523+
3524+
let mut profile = read_registered_creator_profile(&env, &creator)?;
3525+
3526+
let balance_key = constants::storage::holder_balance_key(&creator, &holder);
3527+
let holder_balance: u32 = env.storage().persistent().get(&balance_key).unwrap_or(0);
3528+
3529+
if holder_balance == 0 {
3530+
return Err(ContractError::InsufficientBalance);
3531+
}
3532+
3533+
// Compute payout.
3534+
let payout = (holder_balance as i128)
3535+
.checked_mul(buyback_price_per_key)
3536+
.ok_or(ContractError::Overflow)?;
3537+
3538+
// Deduct from escrow.
3539+
let escrow_key = constants::storage::deprecation_escrow(&creator);
3540+
let current_escrow: i128 = env.storage().persistent().get(&escrow_key).unwrap_or(0);
3541+
3542+
if current_escrow < payout {
3543+
return Err(ContractError::InsufficientEscrow);
3544+
}
3545+
3546+
let new_escrow = current_escrow
3547+
.checked_sub(payout)
3548+
.ok_or(ContractError::Overflow)?;
3549+
3550+
// Burn holder's keys and update supply / holder count.
3551+
profile.supply = profile
3552+
.supply
3553+
.checked_sub(holder_balance)
3554+
.ok_or(ContractError::SellUnderflow)?;
3555+
profile.holder_count = profile
3556+
.holder_count
3557+
.checked_sub(1)
3558+
.ok_or(ContractError::SellUnderflow)?;
3559+
3560+
// Persist updated state.
3561+
let creator_key = constants::storage::creator(&creator);
3562+
env.storage().persistent().set(&creator_key, &profile);
3563+
env.storage().persistent().remove(&balance_key);
3564+
3565+
if new_escrow == 0 {
3566+
env.storage().persistent().remove(&escrow_key);
3567+
} else {
3568+
env.storage().persistent().set(&escrow_key, &new_escrow);
3569+
extend_key_ttl_to_full_window(&env, &escrow_key);
3570+
}
3571+
3572+
env.events().publish(
3573+
events::keys_redeemed_topics(&creator, &holder),
3574+
events::KeysRedeemedEvent {
3575+
creator: creator.clone(),
3576+
holder: holder.clone(),
3577+
quantity: holder_balance,
3578+
payout,
3579+
new_supply: profile.supply,
3580+
ledger: env.ledger().sequence(),
3581+
},
3582+
);
3583+
3584+
Ok(payout)
3585+
}
3586+
33803587
/// Creator-only airdrop that mints keys to a list of recipient wallets.
33813588
///
33823589
/// The creator pays the bonding curve cost for every key across all

0 commit comments

Comments
 (0)