diff --git a/.freebuff/project-id b/.freebuff/project-id
new file mode 100644
index 00000000..15d45306
--- /dev/null
+++ b/.freebuff/project-id
@@ -0,0 +1 @@
+a16e0d91-aeb4-469c-b8d2-61adab299ccc
diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs
index 305375c1..8f591556 100644
--- a/creator-keys/src/events.rs
+++ b/creator-keys/src/events.rs
@@ -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
,
/// Ledger sequence number at the time of deprecation.
pub ledger: u32,
}
diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs
index 83000dc3..704ca398 100644
--- a/creator-keys/src/lib.rs
+++ b/creator-keys/src/lib.rs
@@ -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;
@@ -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.
@@ -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,
+ /// 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,
+ /// 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);
@@ -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
@@ -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`.
@@ -6460,6 +6507,8 @@ impl CreatorKeysContract {
caller: Address,
buyback_price_per_key: i128,
escrow_payment: i128,
+ reason: String,
+ successor_key_id: Option,
) -> Result<(), ContractError> {
caller.require_auth();
assert_not_paused(&env)?;
@@ -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.
@@ -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()
@@ -6507,6 +6563,18 @@ 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 {
@@ -6514,7 +6582,9 @@ impl CreatorKeysContract {
buyback_price_per_key,
circulating_supply,
total_escrow: required_escrow,
- ledger: env.ledger().sequence(),
+ reason,
+ successor_key_id,
+ ledger: current_ledger,
},
);
@@ -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 = 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
diff --git a/creator-keys/tests/key_deprecation.rs b/creator-keys/tests/key_deprecation.rs
index 1d9bda64..becc25a8 100644
--- a/creator-keys/tests/key_deprecation.rs
+++ b/creator-keys/tests/key_deprecation.rs
@@ -73,7 +73,14 @@ fn test_buy_on_deprecated_key_returns_key_deprecated() {
buy_one(&client, &creator, &buyer);
// escrow = 1 key × 100 = 100
- client.deprecate_key(&creator, &creator, &100_i128, &100_i128);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &100_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
let result = client.try_buy_key(&creator, &buyer, &10_000_i128, &None);
assert_eq!(result, Err(Ok(ContractError::KeyDeprecated)));
@@ -97,7 +104,14 @@ fn test_holder_redeem_receives_correct_payout() {
let buyback_price: i128 = 500;
let required_escrow: i128 = 3 * buyback_price; // 1500
- client.deprecate_key(&creator, &creator, &buyback_price, &required_escrow);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &buyback_price,
+ &required_escrow,
+ &String::from_str(&env, "sunsetting"),
+ &None,
+ );
let payout = client.redeem(&creator, &holder);
assert_eq!(payout, required_escrow);
@@ -120,7 +134,14 @@ fn test_deprecate_key_insufficient_escrow_returns_error() {
buy_one(&client, &creator, &buyer);
buy_one(&client, &creator, &buyer);
- let result = client.try_deprecate_key(&creator, &creator, &100_i128, &199_i128);
+ let result = client.try_deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &199_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
assert_eq!(result, Err(Ok(ContractError::InsufficientEscrow)));
}
@@ -134,7 +155,14 @@ fn test_deprecate_key_exact_escrow_succeeds() {
buy_one(&client, &creator, &buyer);
buy_one(&client, &creator, &buyer);
- let result = client.try_deprecate_key(&creator, &creator, &100_i128, &200_i128);
+ let result = client.try_deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &200_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
assert_eq!(result, Ok(Ok(())));
}
@@ -144,7 +172,14 @@ fn test_deprecate_key_zero_supply_requires_zero_escrow() {
let creator = register_creator(&env, &client, "edgar");
// No keys minted; required escrow = 0 so even escrow_payment = 0 is fine.
- let result = client.try_deprecate_key(&creator, &creator, &100_i128, &0_i128);
+ let result = client.try_deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &0_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
assert_eq!(result, Ok(Ok(())));
}
@@ -158,7 +193,14 @@ fn test_non_creator_deprecate_key_returns_unauthorized() {
let creator = register_creator(&env, &client, "frank");
let attacker = Address::generate(&env);
- let result = client.try_deprecate_key(&creator, &attacker, &100_i128, &0_i128);
+ let result = client.try_deprecate_key(
+ &creator,
+ &attacker,
+ &100_i128,
+ &0_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
assert_eq!(result, Err(Ok(ContractError::Unauthorized)));
}
@@ -176,7 +218,14 @@ fn test_deprecate_key_emits_key_deprecated_event() {
let buyback_price: i128 = 200;
let escrow: i128 = 200; // 1 × 200
- client.deprecate_key(&creator, &creator, &buyback_price, &escrow);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &buyback_price,
+ &escrow,
+ &String::from_str(&env, "test reason"),
+ &None,
+ );
let all_events = env.events().all();
let dep_event = all_events
@@ -198,6 +247,8 @@ fn test_deprecate_key_emits_key_deprecated_event() {
assert_eq!(data.buyback_price_per_key, buyback_price);
assert_eq!(data.circulating_supply, 1);
assert_eq!(data.total_escrow, escrow);
+ assert_eq!(data.reason, String::from_str(&env, "test reason"));
+ assert_eq!(data.successor_key_id, None);
}
// ---------------------------------------------------------------------------
@@ -214,7 +265,14 @@ fn test_redeem_emits_keys_redeemed_event() {
buy_one(&client, &creator, &holder);
let buyback_price: i128 = 300;
- client.deprecate_key(&creator, &creator, &buyback_price, &(2 * buyback_price));
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &buyback_price,
+ &(2 * buyback_price),
+ &String::from_str(&env, "retiring"),
+ &None,
+ );
client.redeem(&creator, &holder);
@@ -253,7 +311,14 @@ fn test_redeem_zero_balance_returns_insufficient_balance() {
let no_keys = Address::generate(&env);
buy_one(&client, &creator, &other);
- client.deprecate_key(&creator, &creator, &100_i128, &100_i128);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &100_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
let result = client.try_redeem(&creator, &no_keys);
assert_eq!(result, Err(Ok(ContractError::InsufficientBalance)));
@@ -264,9 +329,23 @@ fn test_double_deprecation_returns_key_deprecated() {
let (env, client, _admin) = setup();
let creator = register_creator(&env, &client, "jake");
- client.deprecate_key(&creator, &creator, &100_i128, &0_i128);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &0_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
- let result = client.try_deprecate_key(&creator, &creator, &200_i128, &0_i128);
+ let result = client.try_deprecate_key(
+ &creator,
+ &creator,
+ &200_i128,
+ &0_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
assert_eq!(result, Err(Ok(ContractError::KeyDeprecated)));
}
@@ -283,7 +362,14 @@ fn test_multiple_holders_redeem_independently() {
buy_one(&client, &creator, &holder_b);
let buyback_price: i128 = 150;
- client.deprecate_key(&creator, &creator, &buyback_price, &(3 * buyback_price));
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &buyback_price,
+ &(3 * buyback_price),
+ &String::from_str(&env, "test"),
+ &None,
+ );
let payout_a = client.redeem(&creator, &holder_a);
assert_eq!(payout_a, 2 * buyback_price);
@@ -299,7 +385,14 @@ fn test_deprecate_key_negative_price_returns_not_positive_amount() {
let (env, client, _admin) = setup();
let creator = register_creator(&env, &client, "leo");
- let result = client.try_deprecate_key(&creator, &creator, &-1_i128, &0_i128);
+ let result = client.try_deprecate_key(
+ &creator,
+ &creator,
+ &-1_i128,
+ &0_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
assert_eq!(result, Err(Ok(ContractError::NotPositiveAmount)));
}
@@ -308,7 +401,14 @@ fn test_deprecate_key_unregistered_creator_returns_not_registered() {
let (env, client, _admin) = setup();
let ghost = Address::generate(&env);
- let result = client.try_deprecate_key(&ghost, &ghost, &100_i128, &0_i128);
+ let result = client.try_deprecate_key(
+ &ghost,
+ &ghost,
+ &100_i128,
+ &0_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
assert_eq!(result, Err(Ok(ContractError::NotRegistered)));
}
@@ -320,7 +420,14 @@ fn test_buy_key_with_referrer_also_blocked_on_deprecated_key() {
let referrer = Address::generate(&env);
buy_one(&client, &creator, &buyer);
- client.deprecate_key(&creator, &creator, &100_i128, &100_i128);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &100_i128,
+ &String::from_str(&env, "test"),
+ &None,
+ );
let result =
client.try_buy_key_with_referrer(&creator, &buyer, &10_000_i128, &None, &Some(referrer));
diff --git a/creator-keys/tests/key_deprecation_successor.rs b/creator-keys/tests/key_deprecation_successor.rs
new file mode 100644
index 00000000..94ae753d
--- /dev/null
+++ b/creator-keys/tests/key_deprecation_successor.rs
@@ -0,0 +1,280 @@
+//! Integration tests for issue #973 — key deprecation reason and successor designation.
+//!
+//! Acceptance criteria verified:
+//!
+//! 1. Deprecation blocked for non-creator callers (Unauthorized).
+//! 2. Buy rejected with KeyDeprecated error after deprecation.
+//! 3. Sell and transfer still permitted post-deprecation.
+//! 4. `get_deprecation_status` returns all fields correctly.
+//! 5. `KeyDeprecated` event includes reason and successor_key_id.
+
+use creator_keys::{
+ events, ContractError, CreatorKeysContract, CreatorKeysContractClient, RegisterCreatorParams,
+};
+use soroban_sdk::{
+ testutils::{Address as _, Events, Ledger as _},
+ Address, Env, IntoVal, String,
+};
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+fn setup() -> (Env, CreatorKeysContractClient<'static>, Address) {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let contract_id = env.register(CreatorKeysContract, ());
+ let client = CreatorKeysContractClient::new(&env, &contract_id);
+
+ let admin = Address::generate(&env);
+ client.set_protocol_admin(&admin, &admin);
+ client.set_key_price(&admin, &100_i128);
+ client.set_curve_slope(&admin, &0_i128);
+ client.set_fee_config(&admin, &9_000_u32, &1_000_u32);
+ client.set_circuit_breaker_threshold(&admin, &10_000_u32);
+
+ (env, client, admin)
+}
+
+fn register_creator(env: &Env, client: &CreatorKeysContractClient, handle: &str) -> Address {
+ let creator = Address::generate(env);
+ client.register_creator(
+ &RegisterCreatorParams {
+ creator: creator.clone(),
+ handle: String::from_str(env, handle),
+ },
+ &None,
+ &None,
+ &None,
+ &None,
+ &None,
+ &None,
+ );
+ creator
+}
+
+fn buy_one(client: &CreatorKeysContractClient, creator: &Address, buyer: &Address) {
+ client.buy_key(creator, buyer, &10_000_i128, &None);
+}
+
+// ---------------------------------------------------------------------------
+// 1. Deprecation blocked for non-creator callers
+// ---------------------------------------------------------------------------
+
+#[test]
+fn test_deprecation_blocked_for_non_creator() {
+ let (env, client, _admin) = setup();
+ let creator = register_creator(&env, &client, "alice973");
+ let attacker = Address::generate(&env);
+
+ let result = client.try_deprecate_key(
+ &creator,
+ &attacker,
+ &100_i128,
+ &0_i128,
+ &String::from_str(&env, "hostile takeover"),
+ &None,
+ );
+ assert_eq!(result, Err(Ok(ContractError::Unauthorized)));
+}
+
+// ---------------------------------------------------------------------------
+// 2. Buy rejected with KeyDeprecated after deprecation
+// ---------------------------------------------------------------------------
+
+#[test]
+fn test_buy_rejected_after_deprecation() {
+ let (env, client, _admin) = setup();
+ let creator = register_creator(&env, &client, "bob973");
+ let buyer = Address::generate(&env);
+
+ buy_one(&client, &creator, &buyer);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &100_i128,
+ &String::from_str(&env, "moving to v2"),
+ &None,
+ );
+
+ let result = client.try_buy_key(&creator, &buyer, &10_000_i128, &None);
+ assert_eq!(result, Err(Ok(ContractError::KeyDeprecated)));
+}
+
+// ---------------------------------------------------------------------------
+// 3. Sell still permitted post-deprecation
+// ---------------------------------------------------------------------------
+
+#[test]
+fn test_sell_still_permitted_after_deprecation() {
+ let (env, client, _admin) = setup();
+ let creator = register_creator(&env, &client, "carol973");
+ let holder = Address::generate(&env);
+
+ buy_one(&client, &creator, &holder);
+ assert_eq!(client.get_key_balance(&creator, &holder), 1);
+
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &100_i128,
+ &String::from_str(&env, "sunsetting"),
+ &None,
+ );
+
+ // Advance past the flash-loan guard window (a buy and sell in the same
+ // ledger are otherwise rejected independently of deprecation).
+ let mut ledger = env.ledger().get();
+ ledger.sequence_number += 1;
+ env.ledger().set(ledger);
+
+ // Sell should succeed even after deprecation.
+ let result = client.try_sell_key(&creator, &holder, &None);
+ assert!(result.is_ok(), "sell should succeed on deprecated key");
+ assert_eq!(client.get_key_balance(&creator, &holder), 0);
+}
+
+// ---------------------------------------------------------------------------
+// 3b. Transfer still permitted post-deprecation
+// ---------------------------------------------------------------------------
+
+#[test]
+fn test_transfer_still_permitted_after_deprecation() {
+ let (env, client, _admin) = setup();
+ let creator = register_creator(&env, &client, "carol973b");
+ let holder = Address::generate(&env);
+ let recipient = Address::generate(&env);
+
+ buy_one(&client, &creator, &holder);
+ assert_eq!(client.get_key_balance(&creator, &holder), 1);
+
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &100_i128,
+ &String::from_str(&env, "sunsetting"),
+ &None,
+ );
+
+ // Transfer should succeed even after deprecation.
+ let result = client.try_transfer_keys(&creator, &holder, &recipient, &1_u32);
+ assert!(result.is_ok(), "transfer should succeed on deprecated key");
+ assert_eq!(client.get_key_balance(&creator, &holder), 0);
+ assert_eq!(client.get_key_balance(&creator, &recipient), 1);
+}
+
+// ---------------------------------------------------------------------------
+// 4. get_deprecation_status returns all fields correctly
+// ---------------------------------------------------------------------------
+
+#[test]
+fn test_get_deprecation_status_undeprecated_returns_false() {
+ let (env, client, _admin) = setup();
+ let creator = register_creator(&env, &client, "dave973");
+
+ let status = client.get_deprecation_status(&creator);
+ assert!(!status.is_deprecated);
+ assert_eq!(status.deprecated_at_ledger, 0);
+ assert_eq!(status.successor_key_id, None);
+}
+
+#[test]
+fn test_get_deprecation_status_returns_reason_and_successor() {
+ let (env, client, _admin) = setup();
+ let creator = register_creator(&env, &client, "eve973");
+ let successor = register_creator(&env, &client, "successor973");
+
+ let reason_str = String::from_str(&env, "upgrade to v2");
+
+ // Pin the ledger sequence so the recorded deprecation timestamp is non-zero.
+ let mut ledger = env.ledger().get();
+ ledger.sequence_number = 42;
+ env.ledger().set(ledger);
+
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &0_i128,
+ &reason_str,
+ &Some(successor.clone()),
+ );
+
+ let status = client.get_deprecation_status(&creator);
+ assert!(status.is_deprecated);
+ assert_eq!(status.reason, reason_str);
+ assert_eq!(status.successor_key_id, Some(successor));
+ assert_eq!(status.deprecated_at_ledger, 42);
+}
+
+#[test]
+fn test_get_deprecation_status_no_successor_returns_none() {
+ let (env, client, _admin) = setup();
+ let creator = register_creator(&env, &client, "frank973");
+
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100_i128,
+ &0_i128,
+ &String::from_str(&env, "just retiring"),
+ &None,
+ );
+
+ let status = client.get_deprecation_status(&creator);
+ assert!(status.is_deprecated);
+ assert_eq!(status.successor_key_id, None);
+ assert_eq!(status.reason, String::from_str(&env, "just retiring"));
+}
+
+// ---------------------------------------------------------------------------
+// 5. KeyDeprecated event includes reason and successor_key_id
+// ---------------------------------------------------------------------------
+
+#[test]
+fn test_key_deprecated_event_includes_reason_and_successor() {
+ let (env, client, _admin) = setup();
+ let creator = register_creator(&env, &client, "grace973");
+ let successor = register_creator(&env, &client, "successorkey");
+ let buyer = Address::generate(&env);
+
+ buy_one(&client, &creator, &buyer);
+
+ let reason_str = String::from_str(&env, "v2 launch");
+ let buyback_price: i128 = 100;
+
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &buyback_price,
+ &buyback_price,
+ &reason_str,
+ &Some(successor.clone()),
+ );
+
+ let all_events = env.events().all();
+ let dep_event = all_events
+ .iter()
+ .rev()
+ .find(|(_, topics, _)| {
+ topics
+ .get(0)
+ .map(|v| {
+ let sym: soroban_sdk::Symbol = v.into_val(&env);
+ sym == events::KEY_DEPRECATED_EVENT_NAME
+ })
+ .unwrap_or(false)
+ })
+ .expect("key_deprecated event not found");
+
+ let data: events::KeyDeprecatedEvent = dep_event.2.into_val(&env);
+ assert_eq!(data.creator, creator);
+ assert_eq!(data.reason, reason_str);
+ assert_eq!(data.successor_key_id, Some(successor));
+ assert_eq!(data.buyback_price_per_key, buyback_price);
+ assert_eq!(data.circulating_supply, 1);
+}
diff --git a/creator-keys/tests/redeem_key.rs b/creator-keys/tests/redeem_key.rs
index d2965677..a577b030 100644
--- a/creator-keys/tests/redeem_key.rs
+++ b/creator-keys/tests/redeem_key.rs
@@ -60,9 +60,16 @@ fn buy(client: &CreatorKeysContractClient, creator: &Address, buyer: &Address, n
}
}
-fn deprecate(client: &CreatorKeysContractClient, creator: &Address) {
+fn deprecate(env: &Env, client: &CreatorKeysContractClient, creator: &Address) {
let supply = client.get_creator_supply(creator) as i128;
- client.deprecate_key(creator, creator, &BUYBACK_PRICE, &(supply * BUYBACK_PRICE));
+ client.deprecate_key(
+ creator,
+ creator,
+ &BUYBACK_PRICE,
+ &(supply * BUYBACK_PRICE),
+ &soroban_sdk::String::from_str(env, "deprecated"),
+ &None,
+ );
}
// ---------------------------------------------------------------------------
@@ -81,7 +88,7 @@ fn redeem_pays_balance_times_buyback_price_and_burns_only_callers_keys() {
assert_eq!(client.get_creator_supply(&creator), 5);
assert_eq!(client.get_creator_holder_count(&creator), 2);
- deprecate(&client, &creator);
+ deprecate(&env, &client, &creator);
let payout = client.redeem(&creator, &holder);
@@ -101,7 +108,7 @@ fn second_redeem_by_same_holder_fails_and_cannot_drain_escrow() {
buy(&client, &creator, &holder, 1);
buy(&client, &creator, &other, 1);
- deprecate(&client, &creator);
+ deprecate(&env, &client, &creator);
client.redeem(&creator, &holder);
let again = client.try_redeem(&creator, &holder);
@@ -168,7 +175,7 @@ fn redeem_emits_keys_redeemed_event_with_wallet_key_quantity_and_payout() {
let holder = Address::generate(&env);
buy(&client, &creator, &holder, 4);
- deprecate(&client, &creator);
+ deprecate(&env, &client, &creator);
client.redeem(&creator, &holder);
let (_, topics, data) = env
diff --git a/creator-keys/tests/reputation_scoring.rs b/creator-keys/tests/reputation_scoring.rs
index ddab874b..62d1b105 100644
--- a/creator-keys/tests/reputation_scoring.rs
+++ b/creator-keys/tests/reputation_scoring.rs
@@ -249,7 +249,14 @@ fn test_key_deprecation_decrements_reputation() {
// Escrow must cover every circulating key at the fixed buyback price.
let buyback_price = 100_i128;
let required_escrow = buyback_price * client.get_total_key_supply(&creator) as i128;
- client.deprecate_key(&creator, &creator, &buyback_price, &required_escrow);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &buyback_price,
+ &required_escrow,
+ &String::from_str(&env, "sunsetting"),
+ &None,
+ );
let view = client.get_reputation(&creator);
assert_eq!(view.score, before - REPUTATION_DEPRECATION_PENALTY);
@@ -378,7 +385,14 @@ fn test_deprecation_penalty_clamps_to_zero_not_negative() {
// A trade only earned 5 points, less than the 50-point deprecation penalty.
let before = client.get_reputation(&creator).score;
assert!(before < REPUTATION_DEPRECATION_PENALTY);
- client.deprecate_key(&creator, &creator, &100, &KEY_PRICE);
+ client.deprecate_key(
+ &creator,
+ &creator,
+ &100,
+ &KEY_PRICE,
+ &String::from_str(&env, "sunsetting"),
+ &None,
+ );
assert_eq!(client.get_reputation(&creator).score, 0);
}