diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index c936edd..4ba64f1 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 de70d4a..2dead68 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,48 @@ 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. diff --git a/creator-keys/tests/wallet_blacklist.rs b/creator-keys/tests/wallet_blacklist.rs new file mode 100644 index 0000000..4f61842 --- /dev/null +++ b/creator-keys/tests/wallet_blacklist.rs @@ -0,0 +1,301 @@ +//! 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); +} + +// --------------------------------------------------------------------------- +// 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" + ); +} + +// --------------------------------------------------------------------------- +// 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)); +} + +// --------------------------------------------------------------------------- +// 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)); +} + +// --------------------------------------------------------------------------- +// 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))); +} + +// --------------------------------------------------------------------------- +// 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); +}