@@ -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