Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions contracts/assetsup/src/tests/transfer_restrictions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,58 @@ fn test_empty_whitelist() {
let whitelist = client.get_whitelist(&1u64);
assert_eq!(whitelist.len(), 0);
}

#[test]
#[should_panic]
fn test_transfer_to_non_whitelisted_fails() {
use soroban_sdk::testutils::Address as _;
let env = create_env();
let (admin, user1, user2, _) = create_mock_addresses(&env);
let user3 = soroban_sdk::Address::generate(&env);
let client = initialize_contract(&env, &admin);

env.mock_all_auths();

client.tokenize_asset(
&2u64,
&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,
);

// Only user2 is whitelisted
client.add_to_whitelist(&2u64, &user2);

// Transfer to user3 (not whitelisted) should panic with TransferRestricted
client.transfer_tokens(&2u64, &user1, &user3, &100000i128);
}

#[test]
fn test_empty_whitelist_allows_transfer() {
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(
&3u64,
&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,
);

// No whitelist — transfer should succeed
client.transfer_tokens(&3u64, &user1, &user2, &100000i128);
assert_eq!(client.get_token_balance(&3u64, &user2), 100000);
}
101 changes: 101 additions & 0 deletions contracts/assetsup/src/tests/transfer_restrictions_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,104 @@ fn test_get_transfer_restriction() {
assert!(before_err);
assert!(after_require_accredited);
}

#[test]
fn test_validate_transfer_blocked_when_not_whitelisted() {
let env = Env::default();
let contract_id = env.register(AssetUpContract, ());
let tokenizer = Address::generate(&env);
let whitelisted = Address::generate(&env);
let not_whitelisted = Address::generate(&env);
let asset_id = 901u64;

let (allowed_result, blocked_result) = env.as_contract(&contract_id, || {
setup_tokenized_asset(&env, asset_id, &tokenizer);

// Add only `whitelisted` to the whitelist
transfer_restrictions::add_to_whitelist(&env, asset_id, whitelisted.clone()).unwrap();

// Transfer to whitelisted address should be allowed
let allowed = transfer_restrictions::validate_transfer(
&env,
asset_id,
tokenizer.clone(),
whitelisted.clone(),
);

// Transfer to non-whitelisted address should be blocked
let blocked = transfer_restrictions::validate_transfer(
&env,
asset_id,
tokenizer.clone(),
not_whitelisted.clone(),
);

(allowed, blocked)
});

assert!(allowed_result.is_ok());
assert!(blocked_result.is_err());
}

#[test]
fn test_validate_transfer_empty_whitelist_allows_all() {
let env = Env::default();
let contract_id = env.register(AssetUpContract, ());
let tokenizer = Address::generate(&env);
let recipient = Address::generate(&env);
let asset_id = 902u64;

let result = env.as_contract(&contract_id, || {
setup_tokenized_asset(&env, asset_id, &tokenizer);

// No whitelist entries — transfer should be allowed
transfer_restrictions::validate_transfer(
&env,
asset_id,
tokenizer.clone(),
recipient.clone(),
)
});

assert!(result.is_ok());
assert!(result.unwrap());
}

#[test]
fn test_validate_transfer_accredited_required_uses_whitelist() {
let env = Env::default();
let contract_id = env.register(AssetUpContract, ());
let tokenizer = Address::generate(&env);
let accredited = Address::generate(&env);
let non_accredited = Address::generate(&env);
let asset_id = 903u64;

let (ok_result, err_result) = env.as_contract(&contract_id, || {
setup_tokenized_asset(&env, asset_id, &tokenizer);

// Set accredited requirement; whitelist acts as the accredited registry
let restriction = TransferRestriction {
require_accredited: true,
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();

let ok = transfer_restrictions::validate_transfer(
&env,
asset_id,
tokenizer.clone(),
accredited.clone(),
);
let err = transfer_restrictions::validate_transfer(
&env,
asset_id,
tokenizer.clone(),
non_accredited.clone(),
);
(ok, err)
});

assert!(ok_result.is_ok());
assert!(err_result.is_err());
}
36 changes: 22 additions & 14 deletions contracts/assetsup/src/transfer_restrictions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,32 +88,40 @@ pub fn validate_transfer(
env: &Env,
asset_id: u64,
_from: Address,
_to: Address,
to: Address,
) -> Result<bool, Error> {
let store = env.storage().persistent();

// Check whitelist: if non-empty, `to` must be whitelisted
let whitelist_key = TokenDataKey::Whitelist(asset_id);
let whitelist: Vec<Address> = 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 {
return Err(Error::TransferRestrictionFailed);
}
}

let restriction_key = TokenDataKey::TransferRestriction(asset_id);

// If no restrictions, allow transfer
// If no restrictions config, allow transfer
let restriction: TransferRestriction = match store.get(&restriction_key) {
Some(Some(r)) => r,
_ => {
return Ok(true); // No restrictions
return Ok(true);
}
};

// Check if accredited investor is required
// If accredited investor required, check whitelist as MVP proxy
if restriction.require_accredited {
// In production, would check external oracle or data
// For now, we assume this is checked at authorization level
// This is a placeholder that would integrate with identity/KYC service
}

// Check geographic restrictions
if !restriction.geographic_allowed.is_empty() {
// In production, would check sender and receiver locations
// For now, we assume this is checked at authorization level
// This is a placeholder that would integrate with location service
let is_listed = whitelist.iter().any(|a| a == to);
if !is_listed {
return Err(Error::AccreditedInvestorRequired);
}
}

Ok(true)
Expand Down