diff --git a/packages/tokens/src/rwa/storage.rs b/packages/tokens/src/rwa/storage.rs index 3407be87..5f5b492e 100644 --- a/packages/tokens/src/rwa/storage.rs +++ b/packages/tokens/src/rwa/storage.rs @@ -593,35 +593,41 @@ impl RWA { emit_identity_verifier_set(e, identity_verifier); } - // ################## OVERRIDDEN FUNCTIONS ################## - - /// This is a wrapper around [`Base::update()`] to enable - /// the compatibility across [`crate::fungible::FungibleToken`] - /// with [`crate::rwa::RWAToken`] + /// This function performs all the checks that are required + /// for a transfer but does not require authorization. It is used by + /// [`Self::transfer`] and [`Self::transfer_from`] overrides. /// - /// The main differences are: - /// - checks for if the contract is paused - /// - checks for if the addresses are frozen - /// - checks for if the from address have enough free tokens (unfrozen - /// tokens) - /// - enforces identity verification for both addresses - /// - enforces compliance rules for the transfer - /// - triggers `transferred` hook call from the compliance contract + /// # Arguments /// - /// Please refer to [`Base::update`] for the inline documentation. - pub fn transfer(e: &Env, from: &Address, to: &Address, amount: i128) { - from.require_auth(); - + /// * `e` - Access to the Soroban environment. + /// * `from` - The address of the sender. + /// * `to` - The address of the receiver. + /// * `amount` - The amount of tokens to transfer. + /// + /// # Errors + /// + /// * [`PausableError::EnforcedPause`] - If the contract is paused. + /// * [`RWAError::AddressFrozen`] - If either the sender or receiver is + /// frozen. + /// * [`RWAError::InsufficientFreeTokens`] - If the sender does not have + /// enough free tokens. + /// * refer to [`Self::identity_verifier`] errors. + /// * refer to [`Self::compliance`] errors. + /// * refer to [`IdentityVerifierClient::verify_identity`] errors. + /// * refer to [`Base::update`] errors. + /// + /// # Events + /// + /// * topics - `["transfer", from: Address, to: Address]` + /// * data - `["to_muxed_id: Option, amount: i128"]` + pub fn validate_transfer(e: &Env, from: &Address, to: &Address, amount: i128) { // Check if contract is paused if paused(e) { panic_with_error!(e, PausableError::EnforcedPause); } // Check if addresses are frozen - if Self::is_frozen(e, from) { - panic_with_error!(e, RWAError::AddressFrozen); - } - if Self::is_frozen(e, to) { + if Self::is_frozen(e, from) || Self::is_frozen(e, to) { panic_with_error!(e, RWAError::AddressFrozen); } @@ -645,22 +651,67 @@ impl RWA { if !can_transfer { panic_with_error!(e, RWAError::TransferNotCompliant); } + } + + // ################## OVERRIDDEN FUNCTIONS ################## + + /// `transfer` override with added compliance and identity verification + /// checks. + /// + /// This is ultimately a wrapper around [`Base::update()`] to enable + /// the compatibility across [`crate::fungible::FungibleToken`] + /// with [`crate::rwa::RWAToken`] + /// + /// The main differences are: + /// - checks for if the contract is paused + /// - checks for if the addresses are frozen + /// - checks for if the from address have enough free tokens (unfrozen + /// tokens) + /// - enforces identity verification for both addresses + /// - enforces compliance rules for the transfer + /// - triggers `transferred` hook call from the compliance contract + /// + /// Please refer to [`Base::update`] and [`Self::validate_transfer`] for the + /// inline documentation. + pub fn transfer(e: &Env, from: &Address, to: &Address, amount: i128) { + from.require_auth(); + + Self::validate_transfer(e, from, to, amount); Base::update(e, Some(from), Some(to), amount); + let compliance_client = ComplianceClient::new(e, &Self::compliance(e)); compliance_client.transferred(from, to, &amount, &e.current_contract_address()); - emit_transfer(e, from, to, None, amount); } - /// This is a wrapper around [`Base::update()`] to enable + /// `transfer_from` override with added compliance and identity verification + /// checks. + /// + /// This is ultimately a wrapper around [`Base::update()`] to enable /// the compatibility across [`crate::fungible::FungibleToken`] /// with [`crate::rwa::RWAToken`] /// - /// Please refer to [`Base::update`] and [`Self::transfer`] for the inline - /// documentation. + /// The main differences are: + /// - checks for if the contract is paused + /// - checks for if the addresses are frozen + /// - checks for if the from address have enough free tokens (unfrozen + /// tokens) + /// - enforces identity verification for both addresses + /// - enforces compliance rules for the transfer + /// - triggers `transferred` hook call from the compliance contract + /// + /// Please refer to [`Base::update`] and [`Self::validate_transfer`] for the + /// inline documentation. pub fn transfer_from(e: &Env, spender: &Address, from: &Address, to: &Address, amount: i128) { + spender.require_auth(); + Base::spend_allowance(e, from, spender, amount); - Self::transfer(e, from, to, amount); + + Base::update(e, Some(from), Some(to), amount); + + let compliance_client = ComplianceClient::new(e, &Self::compliance(e)); + compliance_client.transferred(from, to, &amount, &e.current_contract_address()); + emit_transfer(e, from, to, None, amount); } } diff --git a/packages/tokens/src/rwa/test.rs b/packages/tokens/src/rwa/test.rs index d00baff1..f854bf61 100644 --- a/packages/tokens/src/rwa/test.rs +++ b/packages/tokens/src/rwa/test.rs @@ -4,6 +4,7 @@ use soroban_sdk::{ contract, contractimpl, panic_with_error, symbol_short, testutils::Address as _, Address, Env, String, }; +use stellar_contract_utils::pausable; use crate::{ fungible::ContractOverrides, @@ -854,3 +855,50 @@ fn transfer_fails_when_insufficient_free_tokens() { RWA::transfer(&e, &from, &to, 50); }); } + +#[test] +#[should_panic(expected = "Error(Contract, #1000)")] +fn transfer_fails_when_contract_paused() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockRWAContract, ()); + let from = Address::generate(&e); + let to = Address::generate(&e); + + e.as_contract(&address, || { + setup_all_contracts(&e); + + RWA::mint(&e, &from, 100); + + // Pause the contract + pausable::pause(&e); + + // Try to transfer - should fail with EnforcedPause error + RWA::transfer(&e, &from, &to, 50); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #305)")] +fn transfer_fails_when_not_compliant() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockRWAContract, ()); + let from = Address::generate(&e); + let to = Address::generate(&e); + + e.as_contract(&address, || { + let _ = set_and_return_identity_verifier(&e); + let compliance = set_and_return_compliance(&e); + + RWA::mint(&e, &from, 100); + + // Set compliance to reject transfers + e.as_contract(&compliance, || { + e.storage().persistent().set(&symbol_short!("tx_ok"), &false); + }); + + // Try to transfer - should fail with TransferNotCompliant error + RWA::transfer(&e, &from, &to, 50); + }); +}