From fcb499b5acf188c2b332568a9c50a90caff5455c Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:52:09 +0000 Subject: [PATCH 01/10] implemented the transfer restriction --- contracts/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 62be99f9..1a01868a 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "./asset-maintenance", "assetsup", + "opsce", "contrib", "multisig-wallet", "multisig_transfer", From 5c8b3021e47fb1417d6deeb8ff0f8e061ff65802 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:52:13 +0000 Subject: [PATCH 02/10] implemented the transfer restriction --- contracts/opsce/Cargo.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 contracts/opsce/Cargo.toml diff --git a/contracts/opsce/Cargo.toml b/contracts/opsce/Cargo.toml new file mode 100644 index 00000000..c28b16dd --- /dev/null +++ b/contracts/opsce/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "opsce" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["lib", "cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } From a42d6065fe129ab6330b7578347edbcd5b2de840 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:52:15 +0000 Subject: [PATCH 03/10] implemented the transfer restriction --- contracts/opsce/src/lib.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 contracts/opsce/src/lib.rs diff --git a/contracts/opsce/src/lib.rs b/contracts/opsce/src/lib.rs new file mode 100644 index 00000000..e69de29b From 544b44fa55667f3c08f592a4008ad3b0a126cb9b Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:52:17 +0000 Subject: [PATCH 04/10] implemented the transfer restriction --- contracts/opsce/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/opsce/src/lib.rs b/contracts/opsce/src/lib.rs index e69de29b..bd08d8ef 100644 --- a/contracts/opsce/src/lib.rs +++ b/contracts/opsce/src/lib.rs @@ -0,0 +1,6 @@ +#![no_std] +use soroban_sdk::Env; + +pub mod whitelist; + +pub use whitelist::*; From 4305c2a4c2f7e3472c84b75cca8f630b07783b10 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:52:24 +0000 Subject: [PATCH 05/10] implemented the transfer restriction --- contracts/opsce/src/whitelist.rs | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 contracts/opsce/src/whitelist.rs diff --git a/contracts/opsce/src/whitelist.rs b/contracts/opsce/src/whitelist.rs new file mode 100644 index 00000000..fb194747 --- /dev/null +++ b/contracts/opsce/src/whitelist.rs @@ -0,0 +1,66 @@ +use soroban_sdk::{Address, Env, Vec}; + +/// Store keys are simple strings to avoid coupling with other crates' types +fn whitelist_key(asset_id: u64) -> String { + let mut s = String::from_str(&Env::default(), "opsce:whitelist:"); + s = s + &asset_id.to_string(); + s +} + +fn enabled_key(asset_id: u64) -> String { + let mut s = String::from_str(&Env::default(), "opsce:whitelist_enabled:"); + s = s + &asset_id.to_string(); + s +} + +/// Add an address to the whitelist for `asset_id`. +/// Note: authorization should be handled by the caller (contract wrapper). +pub fn add_to_whitelist(env: &Env, asset_id: u64, address: Address) { + let store = env.storage().persistent(); + + let key = whitelist_key(asset_id); + let mut list: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); + + // Prevent duplicates + if !list.iter().any(|a| a == address) { + list.push_back(address.clone()); + store.set(&key, &list); + } +} + +pub fn remove_from_whitelist(env: &Env, asset_id: u64, address: Address) { + let store = env.storage().persistent(); + + let key = whitelist_key(asset_id); + let mut list: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); + + if let Some(pos) = list.iter().position(|a| a == address) { + list.remove(pos as u32); + store.set(&key, &list); + } +} + +pub fn is_whitelisted(env: &Env, asset_id: u64, address: Address) -> bool { + let store = env.storage().persistent(); + let key = whitelist_key(asset_id); + let list: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); + list.iter().any(|a| a == address) +} + +pub fn set_whitelist_enabled(env: &Env, asset_id: u64, enabled: bool) { + let store = env.storage().persistent(); + let key = enabled_key(asset_id); + store.set(&key, &enabled); +} + +pub fn is_whitelist_enabled(env: &Env, asset_id: u64) -> bool { + let store = env.storage().persistent(); + let key = enabled_key(asset_id); + store.get(&key).unwrap_or(false) +} + +pub fn get_whitelist(env: &Env, asset_id: u64) -> Vec
{ + let store = env.storage().persistent(); + let key = whitelist_key(asset_id); + store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)) +} From b2f99563145aa47d6ef031ce8214994a01232f4a Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:52:41 +0000 Subject: [PATCH 06/10] implemented the transfer restriction --- contracts/assetsup/Cargo.toml | 1 + contracts/opsce/src/whitelist.rs | 36 ++++++++++++++++++-------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/contracts/assetsup/Cargo.toml b/contracts/assetsup/Cargo.toml index 6ea6dc4d..5066a1d8 100644 --- a/contracts/assetsup/Cargo.toml +++ b/contracts/assetsup/Cargo.toml @@ -9,6 +9,7 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } +opsce = { path = "../opsce" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/opsce/src/whitelist.rs b/contracts/opsce/src/whitelist.rs index fb194747..31c44e7a 100644 --- a/contracts/opsce/src/whitelist.rs +++ b/contracts/opsce/src/whitelist.rs @@ -1,16 +1,20 @@ -use soroban_sdk::{Address, Env, Vec}; +use soroban_sdk::{Address, Env, String, Vec}; -/// Store keys are simple strings to avoid coupling with other crates' types -fn whitelist_key(asset_id: u64) -> String { - let mut s = String::from_str(&Env::default(), "opsce:whitelist:"); - s = s + &asset_id.to_string(); - s +// Use a tuple key `(namespace, kind, asset_id)` to avoid serialization coupling +fn whitelist_key<'a>(env: &'a Env, asset_id: u64) -> (String, String, u64) { + ( + String::from_str(env, "opsce"), + String::from_str(env, "whitelist"), + asset_id, + ) } -fn enabled_key(asset_id: u64) -> String { - let mut s = String::from_str(&Env::default(), "opsce:whitelist_enabled:"); - s = s + &asset_id.to_string(); - s +fn enabled_key<'a>(env: &'a Env, asset_id: u64) -> (String, String, u64) { + ( + String::from_str(env, "opsce"), + String::from_str(env, "whitelist_enabled"), + asset_id, + ) } /// Add an address to the whitelist for `asset_id`. @@ -18,7 +22,7 @@ fn enabled_key(asset_id: u64) -> String { pub fn add_to_whitelist(env: &Env, asset_id: u64, address: Address) { let store = env.storage().persistent(); - let key = whitelist_key(asset_id); + let key = whitelist_key(env, asset_id); let mut list: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); // Prevent duplicates @@ -31,7 +35,7 @@ pub fn add_to_whitelist(env: &Env, asset_id: u64, address: Address) { pub fn remove_from_whitelist(env: &Env, asset_id: u64, address: Address) { let store = env.storage().persistent(); - let key = whitelist_key(asset_id); + let key = whitelist_key(env, asset_id); let mut list: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); if let Some(pos) = list.iter().position(|a| a == address) { @@ -42,25 +46,25 @@ pub fn remove_from_whitelist(env: &Env, asset_id: u64, address: Address) { pub fn is_whitelisted(env: &Env, asset_id: u64, address: Address) -> bool { let store = env.storage().persistent(); - let key = whitelist_key(asset_id); + let key = whitelist_key(env, asset_id); let list: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); list.iter().any(|a| a == address) } pub fn set_whitelist_enabled(env: &Env, asset_id: u64, enabled: bool) { let store = env.storage().persistent(); - let key = enabled_key(asset_id); + let key = enabled_key(env, asset_id); store.set(&key, &enabled); } pub fn is_whitelist_enabled(env: &Env, asset_id: u64) -> bool { let store = env.storage().persistent(); - let key = enabled_key(asset_id); + let key = enabled_key(env, asset_id); store.get(&key).unwrap_or(false) } pub fn get_whitelist(env: &Env, asset_id: u64) -> Vec
{ let store = env.storage().persistent(); - let key = whitelist_key(asset_id); + let key = whitelist_key(env, asset_id); store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)) } From e556a06a92f98189328c1ead42c0e39b80d661c1 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:53:12 +0000 Subject: [PATCH 07/10] implemented the transfer restriction --- .../assetsup/src/transfer_restrictions.rs | 68 ++++++------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/contracts/assetsup/src/transfer_restrictions.rs b/contracts/assetsup/src/transfer_restrictions.rs index 16a89d5e..dff964a3 100644 --- a/contracts/assetsup/src/transfer_restrictions.rs +++ b/contracts/assetsup/src/transfer_restrictions.rs @@ -1,6 +1,7 @@ use crate::error::Error; use crate::types::{TokenDataKey, TransferRestriction}; use soroban_sdk::{Address, Env, Vec}; +use opsce::whitelist as ops_whitelist; /// Set transfer restrictions for an asset pub fn set_transfer_restriction( @@ -25,18 +26,8 @@ pub fn set_transfer_restriction( /// Add an address to the whitelist pub fn add_to_whitelist(env: &Env, asset_id: u64, address: Address) -> Result<(), Error> { - let store = env.storage().persistent(); - - let key = TokenDataKey::Whitelist(asset_id); - let mut whitelist: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); - - // Check if already in whitelist - if whitelist.iter().any(|a| a == address) { - return Ok(()); - } - - whitelist.push_back(address.clone()); - store.set(&key, &whitelist); + // Delegate storage to opsce whitelist module + ops_whitelist::add_to_whitelist(env, asset_id, address.clone()); // Emit event: (asset_id, address) env.events() @@ -47,61 +38,41 @@ pub fn add_to_whitelist(env: &Env, asset_id: u64, address: Address) -> Result<() /// Remove an address from the whitelist pub fn remove_from_whitelist(env: &Env, asset_id: u64, address: Address) -> Result<(), Error> { - let store = env.storage().persistent(); - - let key = TokenDataKey::Whitelist(asset_id); - let mut whitelist: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); - - // Find and remove address - if let Some(index) = whitelist.iter().position(|a| a == address) { - whitelist.remove(index as u32); - store.set(&key, &whitelist); + ops_whitelist::remove_from_whitelist(env, asset_id, address.clone()); - // Emit event: (asset_id, address) - env.events() - .publish(("transfer", "whitelist_removed"), (asset_id, address)); - } + // Emit event: (asset_id, address) + env.events() + .publish(("transfer", "whitelist_removed"), (asset_id, address)); Ok(()) } /// Check if an address is whitelisted pub fn is_whitelisted(env: &Env, asset_id: u64, address: Address) -> Result { - let store = env.storage().persistent(); - - let key = TokenDataKey::Whitelist(asset_id); - let whitelist: Vec
= store.get(&key).flatten().unwrap_or_else(|| Vec::new(env)); - - Ok(whitelist.iter().any(|a| a == address)) + Ok(ops_whitelist::is_whitelisted(env, asset_id, address)) } /// Get whitelist for an asset pub fn get_whitelist(env: &Env, asset_id: u64) -> Result, Error> { - let store = env.storage().persistent(); - - let key = TokenDataKey::Whitelist(asset_id); - Ok(store.get(&key).flatten().unwrap_or_else(|| Vec::new(env))) + Ok(ops_whitelist::get_whitelist(env, asset_id)) } /// Validate if a transfer is allowed based on restrictions pub fn validate_transfer( env: &Env, asset_id: u64, - _from: Address, + from: Address, to: Address, ) -> Result { let store = env.storage().persistent(); - - // Check whitelist: if non-empty, `to` must be whitelisted - let whitelist_key = TokenDataKey::Whitelist(asset_id); - let whitelist: Vec
= store - .get(&whitelist_key) - .flatten() - .unwrap_or_else(|| Vec::new(env)); - - if !whitelist.is_empty() { - let is_listed = whitelist.iter().any(|a| a == to); - if !is_listed { + // If whitelist enforcement is enabled for this asset, require both sender and recipient be whitelisted + if ops_whitelist::is_whitelist_enabled(env, asset_id) { + // Check recipient + if !ops_whitelist::is_whitelisted(env, asset_id, to.clone()) { + return Err(Error::TransferRestrictionFailed); + } + // Check sender + if !ops_whitelist::is_whitelisted(env, asset_id, from.clone()) { return Err(Error::TransferRestrictionFailed); } } @@ -118,8 +89,7 @@ pub fn validate_transfer( // If accredited investor required, check whitelist as MVP proxy if restriction.require_accredited { - let is_listed = whitelist.iter().any(|a| a == to); - if !is_listed { + if !ops_whitelist::is_whitelisted(env, asset_id, to.clone()) { return Err(Error::AccreditedInvestorRequired); } } From f3522cc520faa8c038fd2d9dbb41ecee133eff40 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:54:26 +0000 Subject: [PATCH 08/10] implemented the transfer restriction --- contracts/assetsup/src/lib.rs | 64 ++++++++++++++++--- .../src/tests/transfer_restrictions_new.rs | 55 +++++++++++----- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/contracts/assetsup/src/lib.rs b/contracts/assetsup/src/lib.rs index 7c220273..646de79f 100644 --- a/contracts/assetsup/src/lib.rs +++ b/contracts/assetsup/src/lib.rs @@ -756,24 +756,72 @@ impl AssetUpContract { ) } - /// Add address to whitelist - pub fn add_to_whitelist(env: Env, asset_id: u64, address: Address) -> Result<(), Error> { - transfer_restrictions::add_to_whitelist(&env, asset_id, address) + /// Add address to whitelist (admin only) + pub fn add_to_whitelist( + env: Env, + asset_id: u64, + admin: Address, + address: Address, + ) -> Result<(), Error> { + admin.require_auth(); + let token = tokenization::get_tokenized_asset(&env, asset_id)?; + if token.tokenizer != admin { + return Err(Error::Unauthorized); + } + + opsce::whitelist::add_to_whitelist(&env, asset_id, address.clone()); + env.events() + .publish(("transfer", "whitelist_added"), (asset_id, address)); + Ok(()) } - /// Remove address from whitelist - pub fn remove_from_whitelist(env: Env, asset_id: u64, address: Address) -> Result<(), Error> { - transfer_restrictions::remove_from_whitelist(&env, asset_id, address) + /// Remove address from whitelist (admin only) + pub fn remove_from_whitelist( + env: Env, + asset_id: u64, + admin: Address, + address: Address, + ) -> Result<(), Error> { + admin.require_auth(); + let token = tokenization::get_tokenized_asset(&env, asset_id)?; + if token.tokenizer != admin { + return Err(Error::Unauthorized); + } + + opsce::whitelist::remove_from_whitelist(&env, asset_id, address.clone()); + env.events() + .publish(("transfer", "whitelist_removed"), (asset_id, address)); + Ok(()) } /// Check if address is whitelisted pub fn is_whitelisted(env: Env, asset_id: u64, address: Address) -> Result { - transfer_restrictions::is_whitelisted(&env, asset_id, address) + Ok(opsce::whitelist::is_whitelisted(&env, asset_id, address)) } /// Get whitelist pub fn get_whitelist(env: Env, asset_id: u64) -> Result, Error> { - transfer_restrictions::get_whitelist(&env, asset_id) + Ok(opsce::whitelist::get_whitelist(&env, asset_id)) + } + + /// Enable or disable whitelist enforcement for an asset (admin only) + pub fn set_whitelist_enabled( + env: Env, + asset_id: u64, + admin: Address, + enabled: bool, + ) -> Result<(), Error> { + admin.require_auth(); + let token = tokenization::get_tokenized_asset(&env, asset_id)?; + if token.tokenizer != admin { + return Err(Error::Unauthorized); + } + + opsce::whitelist::set_whitelist_enabled(&env, asset_id, enabled); + env.events() + .publish(("transfer", "whitelist_enabled"), (asset_id, enabled)); + + Ok(()) } // ===================== diff --git a/contracts/assetsup/src/tests/transfer_restrictions_new.rs b/contracts/assetsup/src/tests/transfer_restrictions_new.rs index c7baed2d..e286afab 100644 --- a/contracts/assetsup/src/tests/transfer_restrictions_new.rs +++ b/contracts/assetsup/src/tests/transfer_restrictions_new.rs @@ -7,6 +7,7 @@ use soroban_sdk::{Address, Env, String}; use crate::tokenization; use crate::transfer_restrictions; +use crate::AssetUpContractClient; use crate::types::{AssetType, TransferRestriction}; use crate::AssetUpContract; @@ -68,20 +69,28 @@ fn test_whitelist_operations() { let (is_wl_after_add, list_len, is_wl_after_remove) = env.as_contract(&contract_id, || { setup_tokenized_asset(&env, asset_id, &tokenizer); - - // Add to whitelist - transfer_restrictions::add_to_whitelist(&env, asset_id, whitelisted.clone()).unwrap(); - - let is_wl_add = - transfer_restrictions::is_whitelisted(&env, asset_id, whitelisted.clone()).unwrap(); - let whitelist = transfer_restrictions::get_whitelist(&env, asset_id).unwrap(); + env.mock_all_auths(); + let client = AssetUpContractClient::new(&env, &contract_id); + + // Add to whitelist (admin/tokenizer) + client + .add_to_whitelist(&asset_id, &tokenizer, &whitelisted) + .unwrap(); + + let is_wl_add = client + .is_whitelisted(&asset_id, &whitelisted) + .unwrap(); + let whitelist = client.get_whitelist(&asset_id).unwrap(); let len = whitelist.len(); // Remove from whitelist - transfer_restrictions::remove_from_whitelist(&env, asset_id, whitelisted.clone()).unwrap(); + client + .remove_from_whitelist(&asset_id, &tokenizer, &whitelisted) + .unwrap(); - let is_wl_rem = - transfer_restrictions::is_whitelisted(&env, asset_id, whitelisted.clone()).unwrap(); + let is_wl_rem = client + .is_whitelisted(&asset_id, &whitelisted) + .unwrap(); (is_wl_add, len, is_wl_rem) }); @@ -100,15 +109,19 @@ fn test_whitelist_duplicate_prevention() { let list_len = env.as_contract(&contract_id, || { setup_tokenized_asset(&env, asset_id, &tokenizer); + env.mock_all_auths(); + let client = AssetUpContractClient::new(&env, &contract_id); // Add to whitelist twice - transfer_restrictions::add_to_whitelist(&env, asset_id, whitelisted.clone()).unwrap(); - transfer_restrictions::add_to_whitelist(&env, asset_id, whitelisted.clone()).unwrap(); + client + .add_to_whitelist(&asset_id, &tokenizer, &whitelisted) + .unwrap(); + client + .add_to_whitelist(&asset_id, &tokenizer, &whitelisted) + .unwrap(); // Should still have only 1 entry - transfer_restrictions::get_whitelist(&env, asset_id) - .unwrap() - .len() + client.get_whitelist(&asset_id).unwrap().len() }); assert_eq!(list_len, 1); @@ -178,9 +191,13 @@ fn test_validate_transfer_blocked_when_not_whitelisted() { let (allowed_result, blocked_result) = env.as_contract(&contract_id, || { setup_tokenized_asset(&env, asset_id, &tokenizer); + env.mock_all_auths(); + let client = AssetUpContractClient::new(&env, &contract_id); // Add only `whitelisted` to the whitelist - transfer_restrictions::add_to_whitelist(&env, asset_id, whitelisted.clone()).unwrap(); + client + .add_to_whitelist(&asset_id, &tokenizer, &whitelisted) + .unwrap(); // Transfer to whitelisted address should be allowed let allowed = transfer_restrictions::validate_transfer( @@ -247,7 +264,11 @@ fn test_validate_transfer_accredited_required_uses_whitelist() { geographic_allowed: soroban_sdk::Vec::new(&env), }; transfer_restrictions::set_transfer_restriction(&env, asset_id, restriction).unwrap(); - transfer_restrictions::add_to_whitelist(&env, asset_id, accredited.clone()).unwrap(); + env.mock_all_auths(); + let client = AssetUpContractClient::new(&env, &contract_id); + client + .add_to_whitelist(&asset_id, &tokenizer, &accredited) + .unwrap(); let ok = transfer_restrictions::validate_transfer( &env, From c05a42095af92825fadd9f8709504a96392155d6 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:55:34 +0000 Subject: [PATCH 09/10] implemented the transfer restriction --- .../assetsup/src/tests/integration_full.rs | 4 +-- .../src/tests/transfer_restrictions.rs | 28 +++++++++---------- contracts/contrib/src/tests/tokenization.rs | 3 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/contracts/assetsup/src/tests/integration_full.rs b/contracts/assetsup/src/tests/integration_full.rs index a3527da9..47524e82 100644 --- a/contracts/assetsup/src/tests/integration_full.rs +++ b/contracts/assetsup/src/tests/integration_full.rs @@ -120,8 +120,8 @@ fn test_transfer_restrictions_workflow() { // Set transfer restrictions client.set_transfer_restriction(&asset_id, &true); - // Add investor1 to whitelist - client.add_to_whitelist(&asset_id, &investor1); + // Add investor1 to whitelist (tokenizer is `owner`) + client.add_to_whitelist(&asset_id, &owner, &investor1); // Transfer to whitelisted address should succeed client.transfer_tokens(&asset_id, &owner, &investor1, &100000i128); diff --git a/contracts/assetsup/src/tests/transfer_restrictions.rs b/contracts/assetsup/src/tests/transfer_restrictions.rs index 96fd0523..4018901d 100644 --- a/contracts/assetsup/src/tests/transfer_restrictions.rs +++ b/contracts/assetsup/src/tests/transfer_restrictions.rs @@ -25,8 +25,8 @@ fn test_add_to_whitelist() { // Initially not whitelisted assert!(!client.is_whitelisted(&1u64, &user2)); - // Add to whitelist - client.add_to_whitelist(&1u64, &user2); + // Add to whitelist (tokenizer is `user1`) + client.add_to_whitelist(&1u64, &user1, &user2); assert!(client.is_whitelisted(&1u64, &user2)); } @@ -51,12 +51,12 @@ fn test_remove_from_whitelist() { &AssetType::Physical, ); - // Add to whitelist - client.add_to_whitelist(&1u64, &user2); + // Add to whitelist (tokenizer is `user1`) + client.add_to_whitelist(&1u64, &user1, &user2); assert!(client.is_whitelisted(&1u64, &user2)); // Remove from whitelist - client.remove_from_whitelist(&1u64, &user2); + client.remove_from_whitelist(&1u64, &user1, &user2); assert!(!client.is_whitelisted(&1u64, &user2)); } @@ -80,9 +80,9 @@ fn test_get_whitelist() { &AssetType::Physical, ); - // Add multiple addresses to whitelist - client.add_to_whitelist(&1u64, &user2); - client.add_to_whitelist(&1u64, &user3); + // Add multiple addresses to whitelist (tokenizer is `user1`) + client.add_to_whitelist(&1u64, &user1, &user2); + client.add_to_whitelist(&1u64, &user1, &user3); let whitelist = client.get_whitelist(&1u64); assert_eq!(whitelist.len(), 2); @@ -109,8 +109,8 @@ fn test_add_duplicate_to_whitelist() { ); // Add to whitelist twice - client.add_to_whitelist(&1u64, &user2); - client.add_to_whitelist(&1u64, &user2); + client.add_to_whitelist(&1u64, &user1, &user2); + client.add_to_whitelist(&1u64, &user1, &user2); // Should still only have one entry let whitelist = client.get_whitelist(&1u64); @@ -163,8 +163,8 @@ fn test_transfer_with_whitelist() { &AssetType::Physical, ); - // Add user2 to whitelist - client.add_to_whitelist(&1u64, &user2); + // Add user2 to whitelist (tokenizer is `user1`) + client.add_to_whitelist(&1u64, &user1, &user2); // Transfer should succeed client.transfer_tokens(&1u64, &user1, &user2, &100000i128); @@ -220,8 +220,8 @@ fn test_transfer_to_non_whitelisted_fails() { &AssetType::Physical, ); - // Only user2 is whitelisted - client.add_to_whitelist(&2u64, &user2); + // Only user2 is whitelisted (tokenizer is `user1`) + client.add_to_whitelist(&2u64, &user1, &user2); // Transfer to user3 (not whitelisted) should panic with TransferRestricted client.transfer_tokens(&2u64, &user1, &user3, &100000i128); diff --git a/contracts/contrib/src/tests/tokenization.rs b/contracts/contrib/src/tests/tokenization.rs index b470b59e..7fe320b5 100644 --- a/contracts/contrib/src/tests/tokenization.rs +++ b/contracts/contrib/src/tests/tokenization.rs @@ -98,7 +98,8 @@ fn test_transfer_tokens_blacklisted_recipient() { tokenize(&client, &env, 1, &from); // Add only `allowed` to whitelist — `blocked` is not listed - client.add_to_whitelist(&1u64, &allowed); + // The tokenizer (from) is the admin for whitelist management + client.add_to_whitelist(&1u64, &from, &allowed); // Transfer to non-whitelisted address should panic with TransferRestrictionFailed (#17) client.transfer_tokens(&1u64, &from, &blocked, &100_000i128); From a6b874131ce713be4e925e1cfd73b2806bd3007c Mon Sep 17 00:00:00 2001 From: nafsonig Date: Sat, 30 May 2026 04:57:17 +0000 Subject: [PATCH 10/10] implemented the transfer restriction --- .../assetsup/src/tests/detokenization.rs | 2 +- .../src/tests/transfer_restrictions.rs | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/contracts/assetsup/src/tests/detokenization.rs b/contracts/assetsup/src/tests/detokenization.rs index 06576037..eca984a1 100644 --- a/contracts/assetsup/src/tests/detokenization.rs +++ b/contracts/assetsup/src/tests/detokenization.rs @@ -221,7 +221,7 @@ fn test_detokenization_clears_all_data() { // Set up some data client.transfer_tokens(&1u64, &user1, &user2, &600000i128); - client.add_to_whitelist(&1u64, &user2); + client.add_to_whitelist(&1u64, &user1, &user2); client.enable_revenue_sharing(&1u64); // Propose and execute detokenization diff --git a/contracts/assetsup/src/tests/transfer_restrictions.rs b/contracts/assetsup/src/tests/transfer_restrictions.rs index 4018901d..fcdec0a8 100644 --- a/contracts/assetsup/src/tests/transfer_restrictions.rs +++ b/contracts/assetsup/src/tests/transfer_restrictions.rs @@ -251,3 +251,43 @@ fn test_empty_whitelist_allows_transfer() { client.transfer_tokens(&3u64, &user1, &user2, &100000i128); assert_eq!(client.get_token_balance(&3u64, &user2), 100000); } + +#[test] +fn test_whitelist_enforcement_toggle() { + let env = create_env(); + let (admin, user1, user2, _) = create_mock_addresses(&env); + let client = initialize_contract(&env, &admin); + + env.mock_all_auths(); + + client.tokenize_asset( + &1u64, + &String::from_str(&env, "TST"), + &1000000i128, + &6u32, + &100i128, + &user1, + &String::from_str(&env, "Test Token"), + &String::from_str(&env, "A test tokenized asset"), + &AssetType::Physical, + ); + + // Enable whitelist enforcement + client.set_whitelist_enabled(&1u64, &user1, &true); + + // Add only recipient + client.add_to_whitelist(&1u64, &user1, &user2); + + // Transfer should fail because sender (user1) is not whitelisted + let res = std::panic::catch_unwind(|| { + client.transfer_tokens(&1u64, &user1, &user2, &100000i128); + }); + assert!(res.is_err()); + + // Now whitelist sender as well + client.add_to_whitelist(&1u64, &user1, &user1); + + // Transfer should now succeed + client.transfer_tokens(&1u64, &user1, &user2, &100000i128); + assert_eq!(client.get_token_balance(&1u64, &user2), 100000); +}