From d91ba6f576064adee326c8721ce9cfba338591d7 Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:39:33 +0100 Subject: [PATCH 1/8] feat(blacklist): add admin wallet blacklist storage and error variant Adds ContractError::WalletBlacklisted, a Blacklisted(Address) DataKey, and is_blacklisted/assert_not_blacklisted helpers backing the admin wallet blacklist feature. --- creator-keys/src/events.rs | 6 ++++ creator-keys/src/lib.rs | 63 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index c936edd5..4ba64f16 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -34,6 +34,12 @@ pub const PAUSE_EVENT_NAME: Symbol = symbol_short!("pause"); /// Event name for protocol unpause. pub const UNPAUSE_EVENT_NAME: Symbol = symbol_short!("unpause"); +/// Event name for a wallet being added to the admin blacklist. +pub const BLACKLIST_ADDED_EVENT_NAME: Symbol = symbol_short!("blk_add"); + +/// Event name for a wallet being removed from the admin blacklist. +pub const BLACKLIST_REMOVED_EVENT_NAME: Symbol = symbol_short!("blk_rem"); + /// Event name for creator registration. pub const REGISTER_EVENT_NAME: Symbol = symbol_short!("register"); diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index de70d4a9..6bbe622d 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -83,6 +83,7 @@ pub enum ContractError { InvalidReferrer = 34, WalletCapExceeded = 35, DiscountTierLimitExceeded = 36, + WalletBlacklisted = 37, } pub mod fee { @@ -332,6 +333,10 @@ pub mod constants { DataKey::Whitelist(creator.clone()) } + pub fn blacklisted(wallet: &Address) -> DataKey { + DataKey::Blacklisted(wallet.clone()) + } + pub fn creator(creator: &Address) -> DataKey { creator_key(creator) } @@ -608,6 +613,9 @@ pub enum DataKey { /// decide whether to emit the TTL-extension event without a TTL read /// (the Soroban SDK does not expose TTL reads to contract code). CreatorTtlLiveUntil(Address), + /// Wallet addresses the protocol admin has barred from buying, selling, + /// or registering as a creator. + Blacklisted(Address), } /// Time-locked key allocation for creator self-vesting. @@ -1004,6 +1012,20 @@ fn assert_not_paused(env: &Env) -> Result<(), ContractError> { Ok(()) } +fn is_blacklisted(env: &Env, wallet: &Address) -> bool { + env.storage() + .persistent() + .get::(&constants::storage::blacklisted(wallet)) + .unwrap_or(false) +} + +fn assert_not_blacklisted(env: &Env, wallet: &Address) -> Result<(), ContractError> { + if is_blacklisted(env, wallet) { + return Err(ContractError::WalletBlacklisted); + } + Ok(()) +} + fn assert_is_admin(env: &Env, caller: &Address) -> Result<(), ContractError> { let admin: Address = env .storage() @@ -1521,6 +1543,7 @@ impl CreatorKeysContract { creator.require_auth(); assert_not_paused(&env)?; + assert_not_blacklisted(&env, &creator)?; validate_creator_handle(&handle)?; if let Some(config) = co_creator.as_ref() { @@ -1684,6 +1707,7 @@ impl CreatorKeysContract { ) -> Result { buyer.require_auth(); assert_not_paused(&env)?; + assert_not_blacklisted(&env, &buyer)?; if payment <= 0 { return Err(ContractError::NotPositiveAmount); @@ -1852,6 +1876,7 @@ impl CreatorKeysContract { ) -> Result { seller.require_auth(); assert_not_paused(&env)?; + assert_not_blacklisted(&env, &seller)?; let mut profile: CreatorProfile = read_registered_creator_profile(&env, &creator)?; @@ -2231,6 +2256,44 @@ impl CreatorKeysContract { is_paused(&env) } + /// Blocks a wallet from buying, selling, or registering as a creator. + /// + /// Only the protocol admin may call this. Emits a `blacklist` event. + pub fn blacklist_wallet(env: Env, admin: Address, wallet: Address) -> Result<(), ContractError> { + admin.require_auth(); + assert_is_admin(&env, &admin)?; + env.storage() + .persistent() + .set(&constants::storage::blacklisted(&wallet), &true); + env.events() + .publish((events::BLACKLIST_ADDED_EVENT_NAME, wallet), ()); + Ok(()) + } + + /// Restores a previously blacklisted wallet's access to buy, sell, and + /// register as a creator. + /// + /// Only the protocol admin may call this. Emits an `unblacklist` event. + pub fn remove_from_blacklist( + env: Env, + admin: Address, + wallet: Address, + ) -> Result<(), ContractError> { + admin.require_auth(); + assert_is_admin(&env, &admin)?; + env.storage() + .persistent() + .remove(&constants::storage::blacklisted(&wallet)); + env.events() + .publish((events::BLACKLIST_REMOVED_EVENT_NAME, wallet), ()); + Ok(()) + } + + /// Read-only view: returns whether `wallet` is currently blacklisted. + pub fn is_wallet_blacklisted(env: Env, wallet: Address) -> bool { + is_blacklisted(&env, &wallet) + } + pub fn get_key_balance(env: Env, creator: Address, wallet: Address) -> u32 { let key = constants::storage::holder_balance_key(&creator, &wallet); // Read-only callers get `0` for unseen balances to avoid sparse-map lookups failing. From 24058f4e642e4f237f009c5f10e8627850643666 Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:40:37 +0100 Subject: [PATCH 2/8] test(blacklist): assert buy_key panics with WalletBlacklisted Covers the buy path from issue #700: a blacklisted buyer must be rejected and must not mutate supply or balance state. --- creator-keys/tests/wallet_blacklist.rs | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 creator-keys/tests/wallet_blacklist.rs diff --git a/creator-keys/tests/wallet_blacklist.rs b/creator-keys/tests/wallet_blacklist.rs new file mode 100644 index 00000000..e3b26b8f --- /dev/null +++ b/creator-keys/tests/wallet_blacklist.rs @@ -0,0 +1,64 @@ +//! Tests for the admin-managed wallet blacklist. +//! +//! Covers: blacklisted wallets rejected on buy/sell/creator registration, +//! restored access after removal from the blacklist, and admin-only +//! access to the blacklist mutators. + +mod contract_test_env; + +use contract_test_env::{ + register_creator_keys, register_test_creator, set_key_price_for_tests, test_env_with_auths, +}; +use creator_keys::{ContractError, RegisterCreatorParams}; +use soroban_sdk::{testutils::Address as _, Address, String}; + +/// Register a protocol admin into contract storage and return the admin address. +fn set_protocol_admin( + env: &soroban_sdk::Env, + client: &creator_keys::CreatorKeysContractClient<'_>, +) -> Address { + let admin = Address::generate(env); + client.set_protocol_admin(&admin, &admin); + admin +} + +// --------------------------------------------------------------------------- +// buy reverts with WalletBlacklisted when the buyer is blacklisted +// --------------------------------------------------------------------------- + +#[test] +fn test_buy_key_reverts_for_blacklisted_buyer() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + set_key_price_for_tests(&env, &client, 100); + let creator = register_test_creator(&env, &client, "alice"); + let buyer = Address::generate(&env); + + client.blacklist_wallet(&admin, &buyer); + + let result = client.try_buy_key(&creator, &buyer, &100, &None); + assert_eq!(result, Err(Ok(ContractError::WalletBlacklisted))); +} + +#[test] +fn test_buy_key_no_state_change_for_blacklisted_buyer() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + set_key_price_for_tests(&env, &client, 100); + let creator = register_test_creator(&env, &client, "alice"); + let buyer = Address::generate(&env); + + client.blacklist_wallet(&admin, &buyer); + + let supply_before = client.get_total_key_supply(&creator); + let _ = client.try_buy_key(&creator, &buyer, &100, &None); + let supply_after = client.get_total_key_supply(&creator); + + assert_eq!( + supply_before, supply_after, + "supply must not change when a blacklisted buyer's purchase is rejected" + ); + assert_eq!(client.get_key_balance(&creator, &buyer), 0); +} From 5d437bbabe36c2b4b00830c8108f65ea77e26a96 Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:40:51 +0100 Subject: [PATCH 3/8] test(blacklist): assert sell_key panics with WalletBlacklisted Covers the sell path from issue #700: a blacklisted seller must be rejected and must not mutate their key balance. --- creator-keys/tests/wallet_blacklist.rs | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/creator-keys/tests/wallet_blacklist.rs b/creator-keys/tests/wallet_blacklist.rs index e3b26b8f..26b154cf 100644 --- a/creator-keys/tests/wallet_blacklist.rs +++ b/creator-keys/tests/wallet_blacklist.rs @@ -62,3 +62,47 @@ fn test_buy_key_no_state_change_for_blacklisted_buyer() { ); assert_eq!(client.get_key_balance(&creator, &buyer), 0); } + +// --------------------------------------------------------------------------- +// sell reverts with WalletBlacklisted when the seller is blacklisted +// --------------------------------------------------------------------------- + +#[test] +fn test_sell_key_reverts_for_blacklisted_seller() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + set_key_price_for_tests(&env, &client, 100); + let creator = register_test_creator(&env, &client, "alice"); + let seller = Address::generate(&env); + + // Buy first, while not yet blacklisted. + client.buy_key(&creator, &seller, &100, &None); + + client.blacklist_wallet(&admin, &seller); + + let result = client.try_sell_key(&creator, &seller, &None); + assert_eq!(result, Err(Ok(ContractError::WalletBlacklisted))); +} + +#[test] +fn test_sell_key_no_state_change_for_blacklisted_seller() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + set_key_price_for_tests(&env, &client, 100); + let creator = register_test_creator(&env, &client, "alice"); + let seller = Address::generate(&env); + + client.buy_key(&creator, &seller, &100, &None); + client.blacklist_wallet(&admin, &seller); + + let balance_before = client.get_key_balance(&creator, &seller); + let _ = client.try_sell_key(&creator, &seller, &None); + let balance_after = client.get_key_balance(&creator, &seller); + + assert_eq!( + balance_before, balance_after, + "seller balance must not change when a blacklisted seller's sale is rejected" + ); +} From a8304d6260e76cb6847407e4fa9062eb4bf76dd3 Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:41:06 +0100 Subject: [PATCH 4/8] test(blacklist): assert register_creator panics with WalletBlacklisted Covers the registration path from issue #700: a blacklisted wallet must not be able to register as a creator. --- creator-keys/tests/wallet_blacklist.rs | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/creator-keys/tests/wallet_blacklist.rs b/creator-keys/tests/wallet_blacklist.rs index 26b154cf..6b9490bf 100644 --- a/creator-keys/tests/wallet_blacklist.rs +++ b/creator-keys/tests/wallet_blacklist.rs @@ -106,3 +106,56 @@ fn test_sell_key_no_state_change_for_blacklisted_seller() { "seller balance must not change when a blacklisted seller's sale is rejected" ); } + +// --------------------------------------------------------------------------- +// register_creator reverts with WalletBlacklisted when the creator is blacklisted +// --------------------------------------------------------------------------- + +#[test] +fn test_register_creator_reverts_for_blacklisted_wallet() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + + let creator = Address::generate(&env); + client.blacklist_wallet(&admin, &creator); + + let result = client.try_register_creator( + &RegisterCreatorParams { + creator: creator.clone(), + handle: String::from_str(&env, "alice"), + }, + &None, + &None, + &None, + &None, + &None, + &None, + ); + assert_eq!(result, Err(Ok(ContractError::WalletBlacklisted))); +} + +#[test] +fn test_register_creator_no_state_change_for_blacklisted_wallet() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + + let creator = Address::generate(&env); + client.blacklist_wallet(&admin, &creator); + + let _ = client.try_register_creator( + &RegisterCreatorParams { + creator: creator.clone(), + handle: String::from_str(&env, "alice"), + }, + &None, + &None, + &None, + &None, + &None, + &None, + ); + + assert!(!client.is_creator_registered(&creator)); +} From f19551b1e5ff8b279eba63f468c813bf7568e275 Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:41:21 +0100 Subject: [PATCH 5/8] test(blacklist): assert removal from blacklist restores full access Covers the removal path from issue #700: buy, sell, and registration must all succeed again once the admin lifts the block. --- creator-keys/tests/wallet_blacklist.rs | 71 ++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/creator-keys/tests/wallet_blacklist.rs b/creator-keys/tests/wallet_blacklist.rs index 6b9490bf..41944493 100644 --- a/creator-keys/tests/wallet_blacklist.rs +++ b/creator-keys/tests/wallet_blacklist.rs @@ -159,3 +159,74 @@ fn test_register_creator_no_state_change_for_blacklisted_wallet() { assert!(!client.is_creator_registered(&creator)); } + +// --------------------------------------------------------------------------- +// removing a wallet from the blacklist restores access +// --------------------------------------------------------------------------- + +#[test] +fn test_buy_key_succeeds_after_removal_from_blacklist() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + set_key_price_for_tests(&env, &client, 100); + let creator = register_test_creator(&env, &client, "alice"); + let buyer = Address::generate(&env); + + client.blacklist_wallet(&admin, &buyer); + assert!(client.is_wallet_blacklisted(&buyer)); + + let blocked = client.try_buy_key(&creator, &buyer, &100, &None); + assert_eq!(blocked, Err(Ok(ContractError::WalletBlacklisted))); + + client.remove_from_blacklist(&admin, &buyer); + assert!(!client.is_wallet_blacklisted(&buyer)); + + let supply = client.buy_key(&creator, &buyer, &100, &None); + assert_eq!(supply, 1); + assert_eq!(client.get_key_balance(&creator, &buyer), 1); +} + +#[test] +fn test_sell_key_succeeds_after_removal_from_blacklist() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + set_key_price_for_tests(&env, &client, 100); + let creator = register_test_creator(&env, &client, "alice"); + let seller = Address::generate(&env); + + client.buy_key(&creator, &seller, &100, &None); + client.blacklist_wallet(&admin, &seller); + client.remove_from_blacklist(&admin, &seller); + + let supply = client.sell_key(&creator, &seller, &None); + assert_eq!(supply, 0); + assert_eq!(client.get_key_balance(&creator, &seller), 0); +} + +#[test] +fn test_register_creator_succeeds_after_removal_from_blacklist() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + + let creator = Address::generate(&env); + client.blacklist_wallet(&admin, &creator); + client.remove_from_blacklist(&admin, &creator); + + client.register_creator( + &RegisterCreatorParams { + creator: creator.clone(), + handle: String::from_str(&env, "alice"), + }, + &None, + &None, + &None, + &None, + &None, + &None, + ); + + assert!(client.is_creator_registered(&creator)); +} From dd8485a5c5b4757a140b5b877eb8cb58444943b8 Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:41:34 +0100 Subject: [PATCH 6/8] test(blacklist): assert non-admin cannot modify the blacklist Covers the admin-only acceptance criterion from issue #700: neither blacklist_wallet nor remove_from_blacklist may be called by a non-admin, and storage state is unchanged when rejected. --- creator-keys/tests/wallet_blacklist.rs | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/creator-keys/tests/wallet_blacklist.rs b/creator-keys/tests/wallet_blacklist.rs index 41944493..9ec332f5 100644 --- a/creator-keys/tests/wallet_blacklist.rs +++ b/creator-keys/tests/wallet_blacklist.rs @@ -230,3 +230,47 @@ fn test_register_creator_succeeds_after_removal_from_blacklist() { assert!(client.is_creator_registered(&creator)); } + +// --------------------------------------------------------------------------- +// only the admin can add or remove blacklist entries +// --------------------------------------------------------------------------- + +#[test] +fn test_blacklist_wallet_reverts_for_non_admin() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + set_protocol_admin(&env, &client); + + let non_admin = Address::generate(&env); + let target = Address::generate(&env); + + let result = client.try_blacklist_wallet(&non_admin, &target); + assert_eq!(result, Err(Ok(ContractError::Unauthorized))); + assert!(!client.is_wallet_blacklisted(&target)); +} + +#[test] +fn test_remove_from_blacklist_reverts_for_non_admin() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + + let non_admin = Address::generate(&env); + let target = Address::generate(&env); + client.blacklist_wallet(&admin, &target); + + let result = client.try_remove_from_blacklist(&non_admin, &target); + assert_eq!(result, Err(Ok(ContractError::Unauthorized))); + assert!(client.is_wallet_blacklisted(&target)); +} + +#[test] +fn test_blacklist_wallet_rejected_when_no_admin_configured() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + + let caller = Address::generate(&env); + let target = Address::generate(&env); + let result = client.try_blacklist_wallet(&caller, &target); + assert_eq!(result, Err(Ok(ContractError::Unauthorized))); +} From bdab58e1cdde6d89b32d513ccea7d150eb85b506 Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:41:47 +0100 Subject: [PATCH 7/8] test(blacklist): assert blacklist entries are scoped per wallet Adds a regression covering that blacklisting one wallet does not block trading for any other, unrelated wallet. --- creator-keys/tests/wallet_blacklist.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/creator-keys/tests/wallet_blacklist.rs b/creator-keys/tests/wallet_blacklist.rs index 9ec332f5..4f61842a 100644 --- a/creator-keys/tests/wallet_blacklist.rs +++ b/creator-keys/tests/wallet_blacklist.rs @@ -274,3 +274,28 @@ fn test_blacklist_wallet_rejected_when_no_admin_configured() { let result = client.try_blacklist_wallet(&caller, &target); assert_eq!(result, Err(Ok(ContractError::Unauthorized))); } + +// --------------------------------------------------------------------------- +// blacklist is scoped per-wallet and does not affect unrelated wallets +// --------------------------------------------------------------------------- + +#[test] +fn test_blacklist_does_not_affect_other_wallets() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = set_protocol_admin(&env, &client); + set_key_price_for_tests(&env, &client, 100); + let creator = register_test_creator(&env, &client, "alice"); + + let blocked_buyer = Address::generate(&env); + let allowed_buyer = Address::generate(&env); + + client.blacklist_wallet(&admin, &blocked_buyer); + + let result = client.try_buy_key(&creator, &blocked_buyer, &100, &None); + assert_eq!(result, Err(Ok(ContractError::WalletBlacklisted))); + + let supply = client.buy_key(&creator, &allowed_buyer, &100, &None); + assert_eq!(supply, 1); + assert_eq!(client.get_key_balance(&creator, &allowed_buyer), 1); +} From cee3442cd31cba7ab862526c4e0547b65fe285b7 Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:06:09 +0100 Subject: [PATCH 8/8] fix(blacklist): wrap blacklist_wallet signature to satisfy cargo fmt CI's format check failed because the parameter list exceeded rustfmt's line width. Verified locally with cargo fmt --check, cargo clippy -D warnings, and cargo test --workspace (all pass). --- creator-keys/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index 6bbe622d..2dead686 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -2259,7 +2259,11 @@ impl CreatorKeysContract { /// Blocks a wallet from buying, selling, or registering as a creator. /// /// Only the protocol admin may call this. Emits a `blacklist` event. - pub fn blacklist_wallet(env: Env, admin: Address, wallet: Address) -> Result<(), ContractError> { + pub fn blacklist_wallet( + env: Env, + admin: Address, + wallet: Address, + ) -> Result<(), ContractError> { admin.require_auth(); assert_is_admin(&env, &admin)?; env.storage()