From 61fd6183adda9caae6c0fd4795b88ba681244f7f Mon Sep 17 00:00:00 2001 From: ummarig Date: Sat, 30 May 2026 05:54:52 +0000 Subject: [PATCH 1/5] implemented the cross contract --- contracts/opsce/Cargo.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) 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..e69de29b From 9873c868dd37ea18dcd3560091f4f07227b7b251 Mon Sep 17 00:00:00 2001 From: ummarig Date: Sat, 30 May 2026 05:55:22 +0000 Subject: [PATCH 2/5] implemented the cross contract --- contracts/Cargo.toml | 1 + contracts/multisig_transfer/Cargo.toml | 1 + contracts/multisig_transfer/src/storage.rs | 11 +++++++++++ contracts/opsce/Cargo.toml | 11 +++++++++++ contracts/opsce/src/cross_contract.rs | 14 ++++++++++++++ contracts/opsce/src/lib.rs | 5 +++++ 6 files changed, 43 insertions(+) create mode 100644 contracts/opsce/src/cross_contract.rs create mode 100644 contracts/opsce/src/lib.rs diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 62be99f9..1a790a3d 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -6,6 +6,7 @@ members = [ "contrib", "multisig-wallet", "multisig_transfer", + "./opsce", ] [workspace.dependencies] diff --git a/contracts/multisig_transfer/Cargo.toml b/contracts/multisig_transfer/Cargo.toml index 5bb96956..677e23ea 100644 --- a/contracts/multisig_transfer/Cargo.toml +++ b/contracts/multisig_transfer/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/multisig_transfer/src/storage.rs b/contracts/multisig_transfer/src/storage.rs index 9cb5cad2..119e1b28 100644 --- a/contracts/multisig_transfer/src/storage.rs +++ b/contracts/multisig_transfer/src/storage.rs @@ -6,6 +6,7 @@ use crate::types::{ApprovalRule, TransferRequest}; pub enum DataKey { Admin, // Address AssetRegistry, // Address + AssetsUpContract, // Address NextRequestId, // u64 Requests, // Map Rules, // Map, ApprovalRule> @@ -34,6 +35,16 @@ pub fn set_registry(e: &Env, registry: &Address) { .set(&DataKey::AssetRegistry, registry); } +pub fn get_assetsup_contract(e: &Env) -> Option
{ + e.storage().persistent().get(&DataKey::AssetsUpContract) +} + +pub fn set_assetsup_contract(e: &Env, contract_id: &Address) { + e.storage() + .persistent() + .set(&DataKey::AssetsUpContract, contract_id); +} + pub fn next_request_id(e: &Env) -> u64 { e.storage() .persistent() diff --git a/contracts/opsce/Cargo.toml b/contracts/opsce/Cargo.toml index e69de29b..8900cf5a 100644 --- a/contracts/opsce/Cargo.toml +++ b/contracts/opsce/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "opsce" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["lib", "cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } diff --git a/contracts/opsce/src/cross_contract.rs b/contracts/opsce/src/cross_contract.rs new file mode 100644 index 00000000..63b5c337 --- /dev/null +++ b/contracts/opsce/src/cross_contract.rs @@ -0,0 +1,14 @@ +use soroban_sdk::{contractclient, Address, RawVal}; + +#[contractclient] +pub struct AssetsUpClient; + +impl AssetsUpClient { + pub fn transfer_tokens( + &self, + asset_id: u64, + from: Address, + to: Address, + amount: i128, + ) -> Result<(), RawVal>; +} diff --git a/contracts/opsce/src/lib.rs b/contracts/opsce/src/lib.rs new file mode 100644 index 00000000..6180a1ad --- /dev/null +++ b/contracts/opsce/src/lib.rs @@ -0,0 +1,5 @@ +#![no_std] + +pub mod cross_contract; + +pub use cross_contract::AssetsUpClient; From 2cf9bffc3ee594c302739433fd50503c01222c1c Mon Sep 17 00:00:00 2001 From: ummarig Date: Sat, 30 May 2026 05:56:30 +0000 Subject: [PATCH 3/5] implemented the cross contract --- contracts/multisig_transfer/src/errors.rs | 4 +- contracts/multisig_transfer/src/lib.rs | 26 ++++ contracts/multisig_transfer/src/tests.rs | 139 ++++++++++++++++++++++ contracts/multisig_transfer/src/types.rs | 2 + 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 contracts/multisig_transfer/src/tests.rs diff --git a/contracts/multisig_transfer/src/errors.rs b/contracts/multisig_transfer/src/errors.rs index 6e06e761..40fdf23d 100644 --- a/contracts/multisig_transfer/src/errors.rs +++ b/contracts/multisig_transfer/src/errors.rs @@ -27,5 +27,7 @@ pub enum MultiSigError { NotEnoughApprovals = 16, ExecuteTooEarly = 17, - RegistryCallFailed = 18, + AssetsUpContractNotConfigured = 18, + AssetTransferFailed = 19, + RegistryCallFailed = 20, } diff --git a/contracts/multisig_transfer/src/lib.rs b/contracts/multisig_transfer/src/lib.rs index 5cb3fc17..7b22c443 100644 --- a/contracts/multisig_transfer/src/lib.rs +++ b/contracts/multisig_transfer/src/lib.rs @@ -13,6 +13,7 @@ mod utils; use approvals::*; use errors::MultiSigError; +use opsce::AssetsUpClient; use types::{ApprovalRule, RequestStatus, TransferRequest}; #[contract] @@ -49,6 +50,16 @@ impl MultiSigTransferContract { Ok(()) } + pub fn configure_assetsup_contract( + e: Env, + caller: Address, + assetsup_contract_id: Address, + ) -> Result<(), MultiSigError> { + utils::require_admin(&e, &caller)?; + storage::set_assetsup_contract(&e, &assetsup_contract_id); + Ok(()) + } + // ---------------------------- // Create transfer request // ---------------------------- @@ -58,6 +69,8 @@ impl MultiSigTransferContract { caller: Address, asset_id: BytesN<32>, asset_category: BytesN<32>, + token_id: u64, + amount: i128, new_owner: Address, notes_hash: BytesN<32>, expires_at: u64, @@ -104,6 +117,8 @@ impl MultiSigTransferContract { request_id, asset_id: asset_id.clone(), asset_category: asset_category.clone(), + token_id, + amount, current_owner: caller.clone(), // placeholder until registry owner wired new_owner: new_owner.clone(), initiator: caller.clone(), @@ -267,6 +282,14 @@ impl MultiSigTransferContract { } } + let assetsup_contract = storage::get_assetsup_contract(&e) + .ok_or(MultiSigError::AssetsUpContractNotConfigured)?; + + let assetsup_client = AssetsUpClient::new(&e, &assetsup_contract); + assetsup_client + .transfer_tokens(req.token_id, req.current_owner.clone(), req.new_owner.clone(), req.amount) + .map_err(|_| MultiSigError::AssetTransferFailed)?; + // registry transfer ownership registry::transfer_owner(&e, ®istry_addr, &req.asset_id, &req.new_owner)?; @@ -361,3 +384,6 @@ impl MultiSigTransferContract { Ok(rule.approvers) } } + +#[cfg(test)] +mod tests; diff --git a/contracts/multisig_transfer/src/tests.rs b/contracts/multisig_transfer/src/tests.rs new file mode 100644 index 00000000..b7f57c8d --- /dev/null +++ b/contracts/multisig_transfer/src/tests.rs @@ -0,0 +1,139 @@ +use super::*; +use soroban_sdk::{contract, contracterror, contractimpl, symbol_short, testutils::Address as _, Address, BytesN, Env, Vec}; + +#[contract] +pub struct FakeAssetsUpContract; + +#[contractimpl] +impl FakeAssetsUpContract { + pub fn transfer_tokens( + env: Env, + asset_id: u64, + from: Address, + to: Address, + amount: i128, + ) -> Result<(), FakeAssetsUpError> { + from.require_auth(); + env.events().publish((symbol_short!("asset_xfer"),), (asset_id, from.clone(), to, amount)); + Ok(()) + } +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum FakeAssetsUpError { + TransferFailed = 1, +} + +#[contract] +pub struct FailingAssetsUpContract; + +#[contractimpl] +impl FailingAssetsUpContract { + pub fn transfer_tokens( + _env: Env, + _asset_id: u64, + _from: Address, + _to: Address, + _amount: i128, + ) -> Result<(), FakeAssetsUpError> { + Err(FakeAssetsUpError::TransferFailed) + } +} + +fn setup_multisig_and_contracts(env: &Env) -> (MultisigTransferContractClient<'_>, Address) { + env.mock_all_auths(); + let contract_id = env.register(MultiSigTransferContract, ()); + let client = MultisigTransferContractClient::new(env, &contract_id); + let admin = Address::generate(env); + let asset_registry = Address::generate(env); + + client.initialize(&admin, &asset_registry); + (client, admin) +} + +#[test] +fn test_execute_transfer_invokes_assetsup_transfer_tokens() { + let env = Env::default(); + let (client, admin) = setup_multisig_and_contracts(&env); + let fake_assetsup_id = env.register(FakeAssetsUpContract, ()); + + client.configure_assetsup_contract(&admin, &fake_assetsup_id); + let owner = Address::generate(&env); + let new_owner = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let asset_category = BytesN::from_array(&env, &[2u8; 32]); + let notes_hash = BytesN::from_array(&env, &[3u8; 32]); + let token_id = 1u64; + let amount = 1000i128; + + let rule = ApprovalRule { + category: asset_category.clone(), + required_approvals: 0, + approvers: Vec::new(&env), + approval_timeout_secs: 3600, + auto_approve: true, + priority: 0, + }; + client.configure_approval_rule(&admin, rule); + + let request_id = client + .create_transfer_request( + &owner, + &asset_id, + &asset_category, + &token_id, + &amount, + &new_owner, + ¬es_hash, + &(env.ledger().timestamp() + 3600), + &None, + ); + + client.execute_transfer(&owner, &request_id); + + let executed_request = client.get_request(&request_id).unwrap(); + assert_eq!(executed_request.status, RequestStatus::Executed); +} + +#[test] +fn test_execute_transfer_reverts_when_assetsup_transfer_fails() { + let env = Env::default(); + let (client, admin) = setup_multisig_and_contracts(&env); + let fake_assetsup_id = env.register(FailingAssetsUpContract, ()); + + client.configure_assetsup_contract(&admin, &fake_assetsup_id); + let owner = Address::generate(&env); + let new_owner = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[4u8; 32]); + let asset_category = BytesN::from_array(&env, &[5u8; 32]); + let notes_hash = BytesN::from_array(&env, &[6u8; 32]); + let token_id = 1u64; + let amount = 500i128; + + let rule = ApprovalRule { + category: asset_category.clone(), + required_approvals: 0, + approvers: Vec::new(&env), + approval_timeout_secs: 3600, + auto_approve: true, + priority: 0, + }; + client.configure_approval_rule(&admin, rule); + + let request_id = client + .create_transfer_request( + &owner, + &asset_id, + &asset_category, + &token_id, + &amount, + &new_owner, + ¬es_hash, + &(env.ledger().timestamp() + 3600), + &None, + ); + + let execution_result = client.execute_transfer(&owner, &request_id); + assert!(execution_result.is_err()); +} diff --git a/contracts/multisig_transfer/src/types.rs b/contracts/multisig_transfer/src/types.rs index ff46442e..766f6546 100644 --- a/contracts/multisig_transfer/src/types.rs +++ b/contracts/multisig_transfer/src/types.rs @@ -17,6 +17,8 @@ pub struct TransferRequest { pub asset_id: BytesN<32>, pub asset_category: BytesN<32>, + pub token_id: u64, + pub amount: i128, pub current_owner: Address, pub new_owner: Address, From d075378f0565455f7697368b174ec3e32ed6d061 Mon Sep 17 00:00:00 2001 From: ummarig Date: Sat, 30 May 2026 06:06:13 +0000 Subject: [PATCH 4/5] implemented the tokenization --- contracts/assetsup/src/error.rs | 2 + contracts/assetsup/src/tokenization.rs | 16 ++- contracts/opsce/src/tokenization_tests.rs | 162 ++++++++++++++++++++++ 3 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 contracts/opsce/src/tokenization_tests.rs diff --git a/contracts/assetsup/src/error.rs b/contracts/assetsup/src/error.rs index 2d8008a8..0c9caad1 100644 --- a/contracts/assetsup/src/error.rs +++ b/contracts/assetsup/src/error.rs @@ -19,6 +19,8 @@ pub enum Error { InvalidTokenSupply = 12, InvalidTokenDecimals = 13, InsufficientBalance = 14, + MaxSupplyExceeded = 15, + InvalidTokenTransfer = 16, InsufficientLockedTokens = 15, TokensAreLocked = 16, TransferRestrictionFailed = 17, diff --git a/contracts/assetsup/src/tokenization.rs b/contracts/assetsup/src/tokenization.rs index 7402eb8d..b3c26e24 100644 --- a/contracts/assetsup/src/tokenization.rs +++ b/contracts/assetsup/src/tokenization.rs @@ -125,9 +125,15 @@ pub fn mint_tokens( return Err(Error::Unauthorized); } - // Update total supply - tokenized_asset.total_supply += amount; - tokenized_asset.tokens_in_circulation += amount; + let new_total_supply = tokenized_asset + .total_supply + .checked_add(amount) + .ok_or(Error::MaxSupplyExceeded)?; + tokenized_asset.total_supply = new_total_supply; + tokenized_asset.tokens_in_circulation = tokenized_asset + .tokens_in_circulation + .checked_add(amount) + .ok_or(Error::MaxSupplyExceeded)?; // Update tokenizer's ownership let holder_key = TokenDataKey::TokenHolder(asset_id, minter.clone()); @@ -244,6 +250,10 @@ pub fn transfer_tokens( let key = TokenDataKey::TokenizedAsset(asset_id); let tokenized_asset: TokenizedAsset = store.get(&key).ok_or(Error::AssetNotTokenized)?; + if from == to { + return Err(Error::InvalidTokenTransfer); + } + // Check if from address has locked tokens let lock_key = TokenDataKey::TokenLockedUntil(asset_id, from.clone()); if let Some(lock_time) = store.get::<_, u64>(&lock_key) { diff --git a/contracts/opsce/src/tokenization_tests.rs b/contracts/opsce/src/tokenization_tests.rs new file mode 100644 index 00000000..6b53c6d9 --- /dev/null +++ b/contracts/opsce/src/tokenization_tests.rs @@ -0,0 +1,162 @@ +use crate::{AssetUpContract, AssetUpContractClient, AssetType, Error, TokenMetadata}; +use soroban_sdk::{testutils::Address as _, Address, Env, String, Vec}; + +fn make_metadata(env: &Env) -> TokenMetadata { + TokenMetadata { + name: String::from_str(env, "AssetToken"), + description: String::from_str(env, "Tokenized asset for testing"), + asset_type: AssetType::Digital, + ipfs_uri: None, + legal_docs_hash: None, + valuation_report_hash: None, + accredited_investor_required: false, + geographic_restrictions: Vec::new(env), + } +} + +#[test] +fn test_tokenize_asset_success_and_retokenization_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(AssetUpContract, ()); + let client = AssetUpContractClient::new(&env, &contract_id); + let admin = Address::random(&env); + let tokenizer = Address::random(&env); + + client.initialize(&admin); + + let asset_id = 1u64; + let symbol = String::from_str(&env, "TEST"); + let metadata = make_metadata(&env); + + let tokenized = client + .tokenize_asset(&asset_id, &symbol, &1_000i128, &8u32, &10i128, &tokenizer, &metadata) + .unwrap(); + + assert_eq!(tokenized.asset_id, asset_id); + assert_eq!(tokenized.total_supply, 1_000i128); + assert_eq!(tokenized.tokenizer, tokenizer); + + let retry = client.tokenize_asset(&asset_id, &symbol, &1_000i128, &8u32, &10i128, &tokenizer, &metadata); + assert_eq!(retry, Err(Error::AssetAlreadyTokenized)); +} + +#[test] +fn test_mint_tokens_success_and_max_supply_exceeded_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(AssetUpContract, ()); + let client = AssetUpContractClient::new(&env, &contract_id); + let admin = Address::random(&env); + let tokenizer = Address::random(&env); + + client.initialize(&admin); + + let asset_id = 2u64; + let symbol = String::from_str(&env, "MINT"); + let metadata = make_metadata(&env); + + client + .tokenize_asset(&asset_id, &symbol, &1_000i128, &8u32, &10i128, &tokenizer, &metadata) + .unwrap(); + + let minted = client.mint_tokens(&asset_id, &500i128, &tokenizer).unwrap(); + assert_eq!(minted.total_supply, 1_500i128); + + let overflow_amount = i128::MAX - 1_500i128 + 1; + let result = client.mint_tokens(&asset_id, &overflow_amount, &tokenizer); + assert_eq!(result, Err(Error::MaxSupplyExceeded)); +} + +#[test] +fn test_burn_tokens_success_and_insufficient_balance_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(AssetUpContract, ()); + let client = AssetUpContractClient::new(&env, &contract_id); + let admin = Address::random(&env); + let tokenizer = Address::random(&env); + + client.initialize(&admin); + + let asset_id = 3u64; + let symbol = String::from_str(&env, "BURN"); + let metadata = make_metadata(&env); + + client + .tokenize_asset(&asset_id, &symbol, &1_000i128, &8u32, &10i128, &tokenizer, &metadata) + .unwrap(); + + let burned = client.burn_tokens(&asset_id, &200i128, &tokenizer).unwrap(); + assert_eq!(burned.total_supply, 800i128); + + let result = client.burn_tokens(&asset_id, &1_000i128, &tokenizer); + assert_eq!(result, Err(Error::InsufficientBalance)); +} + +#[test] +fn test_transfer_tokens_success_insufficient_balance_and_self_transfer_prevented() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(AssetUpContract, ()); + let client = AssetUpContractClient::new(&env, &contract_id); + let admin = Address::random(&env); + let tokenizer = Address::random(&env); + let holder = Address::random(&env); + + client.initialize(&admin); + + let asset_id = 4u64; + let symbol = String::from_str(&env, "XFER"); + let metadata = make_metadata(&env); + + client + .tokenize_asset(&asset_id, &symbol, &1_000i128, &8u32, &10i128, &tokenizer, &metadata) + .unwrap(); + + client + .transfer_tokens(&asset_id, &tokenizer, &holder, &300i128) + .unwrap(); + + let balance = client.get_token_balance(&asset_id, &holder).unwrap(); + assert_eq!(balance, 300i128); + + let fail_balance = client.transfer_tokens(&asset_id, &holder, &tokenizer, &500i128); + assert_eq!(fail_balance, Err(Error::InsufficientBalance)); + + let self_transfer = client.transfer_tokens(&asset_id, &holder, &holder, &100i128); + assert_eq!(self_transfer, Err(Error::InvalidTokenTransfer)); +} + +#[test] +fn test_get_token_balance_returns_correct_value_after_mints_and_transfers() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(AssetUpContract, ()); + let client = AssetUpContractClient::new(&env, &contract_id); + let admin = Address::random(&env); + let tokenizer = Address::random(&env); + let recipient = Address::random(&env); + + client.initialize(&admin); + + let asset_id = 5u64; + let symbol = String::from_str(&env, "BAL"); + let metadata = make_metadata(&env); + + client + .tokenize_asset(&asset_id, &symbol, &1_000i128, &8u32, &10i128, &tokenizer, &metadata) + .unwrap(); + + client.mint_tokens(&asset_id, &200i128, &tokenizer).unwrap(); + client.transfer_tokens(&asset_id, &tokenizer, &recipient, &250i128).unwrap(); + client.transfer_tokens(&asset_id, &recipient, &tokenizer, &50i128).unwrap(); + + assert_eq!(client.get_token_balance(&asset_id, &tokenizer).unwrap(), 1_000i128 - 250i128 + 50i128 + 200i128); + assert_eq!(client.get_token_balance(&asset_id, &recipient).unwrap(), 200i128); +} From e6a34bf743057b21465a2ff78f917b6a94d33996 Mon Sep 17 00:00:00 2001 From: ummarig Date: Sat, 30 May 2026 06:06:18 +0000 Subject: [PATCH 5/5] implemented the tokenization --- contracts/assetsup/src/tests/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/assetsup/src/tests/mod.rs b/contracts/assetsup/src/tests/mod.rs index a44a81fd..50ab6005 100644 --- a/contracts/assetsup/src/tests/mod.rs +++ b/contracts/assetsup/src/tests/mod.rs @@ -11,6 +11,8 @@ mod initialization; mod detokenization; mod dividends; mod tokenization; +#[path = "../../../opsce/src/tokenization_tests.rs"] +mod tokenization_tests; mod transfer_restrictions; mod voting;