diff --git a/pallets/asset/src/benchmarking.rs b/pallets/asset/src/benchmarking.rs index 63045c78f2..745dddc7b1 100644 --- a/pallets/asset/src/benchmarking.rs +++ b/pallets/asset/src/benchmarking.rs @@ -945,4 +945,70 @@ benchmarks! { .is_empty() ); } + + set_frozen_tokens { + let alice = UserBuilder::::default().generate_did().build("Alice"); + let asset_id = create_sample_asset::(&alice, true); + let alice_portfolio = create_portfolio::(&alice, "SenderPortfolio"); + }: _(alice.origin, asset_id, alice_portfolio, ONE_UNIT) + + get_holders_frozen_balance { + let alice = UserBuilder::::default().generate_did().build("Alice"); + let asset_id = create_sample_asset::(&alice, true); + let alice_asset_holder = AssetHolder::try_from(alice.account().encode()).unwrap(); + + Pallet::::set_frozen_tokens( + alice.origin.into(), + asset_id, + alice_asset_holder.clone(), + ONE_UNIT + ) + .unwrap(); + }: { + assert_eq!( + Pallet::::get_holders_frozen_balance( + &alice_asset_holder, + &asset_id, + ), + ONE_UNIT + ); + } + + transfer_is_allowed_for_holder_best_case { + // No statistics or compliance rules are set + let alice = UserBuilder::::default().generate_did().build("Alice"); + let bob = UserBuilder::::default().generate_did().build("Bob"); + let mut weight_meter = WeightMeter::max_limit_no_minimum(); + + let (sender, receiver, _, asset_id) = + setup_asset_transfer::(&alice, &bob, None, None, true, true, 0, true, true); + }: { + assert!( + Pallet::::transfer_is_allowed_for_holder( + &sender, + &asset_id, + true, + &mut weight_meter + ) + ); + } + + transfer_is_allowed_for_holder_worst_case { + // Max Statistics and Compliance rules are set + let alice = UserBuilder::::default().generate_did().build("Alice"); + let bob = UserBuilder::::default().generate_did().build("Bob"); + let mut weight_meter = WeightMeter::max_limit_no_minimum(); + + let (sender, receiver, _, asset_id) = + setup_asset_transfer::(&alice, &bob, None, None, false, false, 0, true, true); + }: { + assert!( + Pallet::::transfer_is_allowed_for_holder( + &sender, + &asset_id, + true, + &mut weight_meter + ) + ); + } } diff --git a/pallets/asset/src/lib.rs b/pallets/asset/src/lib.rs index 8b6122525e..d198c0ef3a 100644 --- a/pallets/asset/src/lib.rs +++ b/pallets/asset/src/lib.rs @@ -359,6 +359,13 @@ pub mod pallet { amount_spent: Balance, remaining_allowance: Balance, }, + /// The asset's frozen balance was set for an asset holder. + FrozenBalanceSet { + caller_did: IdentityId, + asset_holder: AssetHolder, + asset_id: AssetId, + frozen_balance: Balance, + }, } /// Map each [`Ticker`] to its registration details ([`TickerRegistration`]). @@ -627,6 +634,18 @@ pub mod pallet { ValueQuery, >; + /// Tracks the amount of frozen tokens for each asset held by the account. + #[pallet::storage] + pub type FrozenBalance = StorageDoubleMap< + _, + Twox64Concat, + AccountId32, + Blake2_128Concat, + AssetId, + Balance, + ValueQuery, + >; + /// Storage version. #[pallet::storage] pub type StorageVersion = StorageValue<_, Version, ValueQuery>; @@ -1802,6 +1821,18 @@ pub mod pallet { Ok(()) } + + /// Freezes `amount` of `asset_id` tokens from `asset_holder`. + #[pallet::call_index(38)] + #[pallet::weight(::WeightInfo::set_frozen_tokens())] + pub fn set_frozen_tokens( + origin: OriginFor, + asset_id: AssetId, + asset_holder: AssetHolder, + amount: Balance, + ) -> DispatchResult { + Self::base_set_frozen_tokens(origin, asset_id, asset_holder, amount) + } } #[pallet::error] @@ -1959,6 +1990,10 @@ pub mod pallet { fn issue_without_statistics() -> Weight; fn asset_transfer_report_best_case() -> Weight; fn asset_transfer_report_worst_case() -> Weight; + fn set_frozen_tokens() -> Weight; + fn get_holders_frozen_balance() -> Weight; + fn transfer_is_allowed_for_holder_best_case() -> Weight; + fn transfer_is_allowed_for_holder_worst_case() -> Weight; } } @@ -2177,7 +2212,8 @@ impl Pallet { Error::::UnexpectedNonFungibleToken ); - let new_balance = Self::ensure_sufficient_balance(&caller_holding_ctx, &asset_id, value)?; + let new_balance = + Self::ensure_sufficient_balance(&caller_holding_ctx, &asset_id, value, false)?; Self::set_holders_balance(caller_holding_ctx.clone(), asset_id, new_balance)?; asset_details.total_supply = asset_details @@ -2338,6 +2374,7 @@ impl Pallet { None, None, holder_did, + true, weight_meter, )?; @@ -2703,6 +2740,7 @@ impl Pallet { instruction_id, instruction_memo, caller_did, + false, weight_meter, )?; @@ -2933,6 +2971,29 @@ impl Pallet { Ok(PostDispatchInfo::from(Some(weight_meter.consumed()))) } + + /// Sets the frozen transfer amount for an account on a given asset. + fn base_set_frozen_tokens( + origin: T::RuntimeOrigin, + asset_id: AssetId, + asset_holder: AssetHolder, + amount: Balance, + ) -> DispatchResult { + let caller_did = ExternalAgents::::ensure_perms(origin, &asset_id)?; + + let asset_details = Self::try_get_asset_details(&asset_id)?; + ensure!( + asset_details.asset_type.is_fungible(), + Error::::UnexpectedNonFungibleToken + ); + + if let AssetHolder::Portfolio(receiver_portfolio_id) = &asset_holder { + PortfolioPallet::::ensure_portfolio_validity(receiver_portfolio_id)?; + } + + Self::unverified_set_frozen_tokens(caller_did, asset_holder, asset_id, amount); + Ok(()) + } } //========================================================================== @@ -3267,7 +3328,13 @@ impl Pallet { ); // Verifies that both portfolios exist an that the sender portfolio has sufficient balance - Self::ensure_valid_holdings(sender, receiver, &asset_id, transfer_value)?; + Self::ensure_valid_holdings( + sender, + receiver, + &asset_id, + transfer_value, + is_controller_transfer, + )?; // Controllers are exempt from statistics, compliance and frozen rules. if is_controller_transfer { @@ -3380,7 +3447,8 @@ impl Pallet { asset_transfer_errors.push(Error::::InsufficientBalance.into()); } } else { - if let Err(e) = Self::ensure_sufficient_balance(sender, asset_id, transfer_value) { + if let Err(e) = Self::ensure_sufficient_balance(sender, asset_id, transfer_value, false) + { asset_transfer_errors.push(e); } } @@ -3627,7 +3695,7 @@ impl Pallet { asset_id: AssetId, balance_to_add: Balance, ) -> DispatchResult { - let _ = Self::ensure_sufficient_balance(&asset_holder, &asset_id, balance_to_add)?; + let _ = Self::ensure_sufficient_balance(&asset_holder, &asset_id, balance_to_add, false)?; match asset_holder { AssetHolder::Portfolio(portfolio_id) => { PortfolioPallet::::unchecked_lock_tokens(portfolio_id, asset_id, balance_to_add); @@ -3668,6 +3736,7 @@ impl Pallet { receiver: &AssetHolder, asset_id: &AssetId, value: Balance, + is_controller_transfer: bool, ) -> DispatchResult { match receiver { AssetHolder::Portfolio(receiver_portfolio_id) => { @@ -3678,7 +3747,7 @@ impl Pallet { } } - let _ = Self::ensure_sufficient_balance(sender, asset_id, value)?; + let _ = Self::ensure_sufficient_balance(sender, asset_id, value, is_controller_transfer)?; Ok(()) } @@ -3689,21 +3758,29 @@ impl Pallet { holder: &AssetHolder, asset_id: &AssetId, value: Balance, + is_controller_transfer: bool, ) -> Result { Self::ensure_granular(asset_id, value)?; let current_balance = Self::get_holders_balance(holder, asset_id); let locked_balance = Self::get_holders_locked_balance(holder, asset_id); + let available_balance = { + if is_controller_transfer { + current_balance.saturating_sub(locked_balance) + } else { + let frozen_balance = Self::get_holders_frozen_balance(holder, asset_id); + let unavailable_balance = locked_balance.saturating_add(frozen_balance); + current_balance.saturating_sub(unavailable_balance) + } + }; + + ensure!(available_balance >= value, Error::::InsufficientBalance); + let final_balance = current_balance .checked_sub(value) .ok_or(Error::::InsufficientBalance)?; - ensure!( - final_balance >= locked_balance, - Error::::InsufficientBalance - ); - Ok(final_balance) } @@ -3809,6 +3886,47 @@ impl Pallet { } Ok(()) } + + /// Returns the frozen balance for `asset_holder` and `asset_id`. + pub fn get_holders_frozen_balance(asset_holder: &AssetHolder, asset_id: &AssetId) -> Balance { + match asset_holder { + AssetHolder::Portfolio(portfolio_id) => { + pallet_portfolio::Pallet::::get_portfolio_frozen_balance(portfolio_id, asset_id) + } + AssetHolder::Account(account_id) => FrozenBalance::::get(account_id, asset_id), + } + } + + /// Returns `true` if the asset holder can send/receive the asset based on the asset's compliance rules. + /// Note: No balance and no statistics checks are performed. + pub fn transfer_is_allowed_for_holder( + asset_holder: &AssetHolder, + asset_id: &AssetId, + holder_is_the_sender: bool, + weight_meter: &mut WeightMeter, + ) -> bool { + if Self::ensure_asset_exists(asset_id).is_err() { + return false; + } + + if Self::ensure_asset_is_not_frozen(&asset_id).is_err() { + return false; + } + + let holder_did = { + match pallet_identity::Pallet::::asset_holder_did(&asset_holder) { + Ok(did) => did, + Err(_) => return false, + } + }; + + T::ComplianceManager::is_holder_compliant( + holder_did, + asset_id, + holder_is_the_sender, + weight_meter, + ) + } } //========================================================================== @@ -3973,6 +4091,7 @@ impl Pallet { instruction_id: Option, instruction_memo: Option, caller_did: IdentityId, + is_controller_transfer: bool, weight_meter: &mut WeightMeter, ) -> DispatchResult { // Gets the current balance and advances the checkpoint @@ -3997,6 +4116,19 @@ impl Pallet { BalanceOf::::insert(asset_id, sender_did, sender_new_balance); BalanceOf::::insert(asset_id, receiver_did, receiver_new_balance); + if is_controller_transfer { + let frozen_balance = Self::get_holders_frozen_balance(&sender, &asset_id); + if frozen_balance > 0 { + let new_frozen_balance = frozen_balance.saturating_sub(transfer_value); + Self::unverified_set_frozen_tokens( + caller_did, + sender.clone(), + asset_id, + new_frozen_balance, + ); + } + } + // Updates the balances in the portfolio pallet Self::transfer_holders_balance(sender.clone(), receiver.clone(), asset_id, transfer_value)?; @@ -4176,14 +4308,11 @@ impl Pallet { Error::::InsufficientBalance ); - match &receiver { - AssetHolder::Portfolio(receiver_pid) => { - PortfolioPallet::::ensure_portfolio_validity(receiver_pid)? - } - AssetHolder::Account(_) => {} + if let AssetHolder::Portfolio(receiver_pid) = &receiver { + PortfolioPallet::::ensure_portfolio_validity(receiver_pid)?; } - Self::ensure_sufficient_balance(&sender, &asset_id, transfer_value)?; + Self::ensure_sufficient_balance(&sender, &asset_id, transfer_value, false)?; Statistics::::verify_transfer_restrictions( asset_id, @@ -4204,11 +4333,43 @@ impl Pallet { Some(inst_id), inst_memo, caller_did, + false, weight_meter, )?; Ok(()) } + + fn unverified_set_frozen_tokens( + caller_did: IdentityId, + asset_holder: AssetHolder, + asset_id: AssetId, + amount: Balance, + ) { + match &asset_holder { + AssetHolder::Account(account) => { + if amount.is_zero() { + FrozenBalance::::remove(account, &asset_id); + } else { + FrozenBalance::::insert(account, asset_id, amount); + } + } + AssetHolder::Portfolio(portfolio) => { + pallet_portfolio::Pallet::::set_portfolio_frozen_balance( + portfolio.clone(), + asset_id, + amount, + ); + } + } + + Self::deposit_event(Event::FrozenBalanceSet { + caller_did, + asset_holder, + asset_id, + frozen_balance: amount, + }); + } } //========================================================================== diff --git a/pallets/compliance-manager/src/lib.rs b/pallets/compliance-manager/src/lib.rs index 39189339b6..63f821bf05 100644 --- a/pallets/compliance-manager/src/lib.rs +++ b/pallets/compliance-manager/src/lib.rs @@ -889,6 +889,45 @@ impl ComplianceFnConfig for Pallet { ) } + fn is_holder_compliant( + holder_did: IdentityId, + asset_id: &AssetId, + holder_is_the_sender: bool, + weight_meter: &mut WeightMeter, + ) -> bool { + let asset_compliance = AssetCompliances::::get(asset_id); + + if asset_compliance.paused || asset_compliance.requirements.is_empty() { + return true; + } + + for requirement in &asset_compliance.requirements { + let req_conditions = { + if holder_is_the_sender { + &requirement.sender_conditions + } else { + &requirement.receiver_conditions + } + }; + + match Self::are_all_conditions_satisfied( + asset_id, + holder_did, + req_conditions, + weight_meter, + ) { + Ok(condition_satisfied) => { + if condition_satisfied { + return true; + } + } + Err(_) => return false, + } + } + + false + } + #[cfg(feature = "runtime-benchmarks")] fn setup_asset_compliance( caller_did: IdentityId, diff --git a/pallets/portfolio/src/lib.rs b/pallets/portfolio/src/lib.rs index 9bb5f64fc0..f33fcd6406 100644 --- a/pallets/portfolio/src/lib.rs +++ b/pallets/portfolio/src/lib.rs @@ -322,6 +322,18 @@ pub mod pallet { pub type AllowedCustodians = StorageDoubleMap<_, Identity, IdentityId, Identity, IdentityId, bool, ValueQuery>; + /// Amount of assets frozen in a portfolio. + #[pallet::storage] + pub type PortfolioFrozenAssets = StorageDoubleMap< + _, + Twox64Concat, + PortfolioId, + Blake2_128Concat, + AssetId, + Balance, + ValueQuery, + >; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -1197,6 +1209,24 @@ impl Pallet { Self::set_portfolio_locked_balance(portfolio, asset_id, current_locked - amount); Ok(()) } + + /// Sets the frozen balance of `asset_id` in `portfolio` to `new_frozen_balance`. + pub fn set_portfolio_frozen_balance( + portfolio: PortfolioId, + asset_id: AssetId, + new_frozen_balance: Balance, + ) { + if new_frozen_balance.is_zero() { + PortfolioFrozenAssets::::remove(&portfolio, &asset_id); + } else { + PortfolioFrozenAssets::::insert(portfolio, asset_id, new_frozen_balance); + } + } + + /// Returns the frozen balance of `asset_id` in `portfolio`. + pub fn get_portfolio_frozen_balance(portfolio: &PortfolioId, asset_id: &AssetId) -> Balance { + PortfolioFrozenAssets::::get(portfolio, asset_id) + } } impl PortfolioFnTrait for Pallet { diff --git a/pallets/precompiles/src/interface/erc7943.rs b/pallets/precompiles/src/interface/erc7943.rs index 9401b87463..7524c97ce6 100644 --- a/pallets/precompiles/src/interface/erc7943.rs +++ b/pallets/precompiles/src/interface/erc7943.rs @@ -101,4 +101,125 @@ impl FungibleAssetInterface { &true, )) } + + /// Freezes a specific amount of tokens for a given account. + pub(crate) fn set_frozen_tokens( + asset_id: AssetId, + call: &IFungibleAsset::setFrozenTokensCall, + env: &mut impl Ext, + ) -> Result, Error> { + env.charge(::WeightInfo::set_frozen_tokens())?; + + let caller = Common::::caller(env)?; + let acc_to_freeze = Common::::asset_holder(call.account)?; + let amount = Common::::to_balance(call.amount)?; + + Common::::call_runtime( + env, + caller.runtime_origin(), + pallet_asset::Call::::set_frozen_tokens { + asset_id, + asset_holder: acc_to_freeze, + amount, + }, + )?; + + Common::::deposit_event( + env, + IFungibleAssetEvents::Frozen(IFungibleAsset::Frozen { + account: call.account.into(), + amount: call.amount, + }), + )?; + + Ok(IFungibleAsset::setFrozenTokensCall::abi_encode_returns( + &true, + )) + } + + /// Returns the amount of frozen tokens for a given account. + pub(crate) fn get_frozen_tokens( + asset_id: AssetId, + call: &IFungibleAsset::getFrozenTokensCall, + env: &mut impl Ext, + ) -> Result, Error> { + env.charge(::WeightInfo::get_holders_frozen_balance())?; + + let from = Common::::asset_holder(call.account)?; + + let frozen_tokens = pallet_asset::Pallet::::get_holders_frozen_balance(&from, &asset_id); + let frozen_tokens = Common::::to_u256(frozen_tokens)?; + + Ok(IFungibleAsset::getFrozenTokensCall::abi_encode_returns( + &frozen_tokens, + )) + } + + /// Returns `true` if the account is allowed to send tokens according to token rules. + pub(crate) fn can_send( + asset_id: AssetId, + call: &IFungibleAsset::canSendCall, + env: &mut impl Ext, + ) -> Result, Error> { + let transfer_is_allowed_for_holder_worst_case_weight = + ::WeightInfo::transfer_is_allowed_for_holder_worst_case(); + let charged = env.charge(transfer_is_allowed_for_holder_worst_case_weight)?; + + let sender = Common::::asset_holder(call.account)?; + + let mut weight_meter = WeightMeter::max_limit_no_minimum(); + let allowed = pallet_asset::Pallet::::transfer_is_allowed_for_holder( + &sender, + &asset_id, + true, + &mut weight_meter, + ); + + let transfer_is_allowed_weight = + ::WeightInfo::transfer_is_allowed_for_holder_best_case(); + let compliance_weight = weight_meter.consumed(); + let real_consumed_weight = transfer_is_allowed_weight.saturating_add(compliance_weight); + + if real_consumed_weight.ref_time() + < transfer_is_allowed_for_holder_worst_case_weight.ref_time() + { + env.adjust_gas(charged, real_consumed_weight); + } + + Ok(IFungibleAsset::canSendCall::abi_encode_returns(&allowed)) + } + + /// Returns `true` if the account is allowed to receive tokens according to token rules. + pub(crate) fn can_receive( + asset_id: AssetId, + call: &IFungibleAsset::canReceiveCall, + env: &mut impl Ext, + ) -> Result, Error> { + let transfer_is_allowed_for_holder_worst_case_weight = + ::WeightInfo::transfer_is_allowed_for_holder_worst_case(); + let charged = env.charge(transfer_is_allowed_for_holder_worst_case_weight)?; + + let receiver = Common::::asset_holder(call.account)?; + + let mut weight_meter = WeightMeter::max_limit_no_minimum(); + let allowed = pallet_asset::Pallet::::transfer_is_allowed_for_holder( + &receiver, + &asset_id, + false, + &mut weight_meter, + ); + + let transfer_is_allowed_weight = + ::WeightInfo::transfer_is_allowed_for_holder_best_case(); + let compliance_weight = weight_meter.consumed(); + let real_consumed_weight = transfer_is_allowed_weight.saturating_add(compliance_weight); + + if real_consumed_weight.ref_time() + < transfer_is_allowed_for_holder_worst_case_weight.ref_time() + { + env.adjust_gas(charged, real_consumed_weight); + } + + Ok(IFungibleAsset::canReceiveCall::abi_encode_returns(&allowed)) + } } diff --git a/pallets/precompiles/src/interface/mod.rs b/pallets/precompiles/src/interface/mod.rs index 7ae638e618..2b50d2d941 100644 --- a/pallets/precompiles/src/interface/mod.rs +++ b/pallets/precompiles/src/interface/mod.rs @@ -74,6 +74,7 @@ impl Precompile for FungibleAssetInterface { | IFungibleAssetCalls::burn(_) | IFungibleAssetCalls::permit(_) | IFungibleAssetCalls::forcedTransfer(_) + | IFungibleAssetCalls::setFrozenTokens(_) if env.is_read_only() => { Err(Common::::state_change_denied()) @@ -106,6 +107,14 @@ impl Precompile for FungibleAssetInterface { // ERC7943 functions IFungibleAssetCalls::canTransfer(call) => Self::can_transfer(asset_id, call, env), IFungibleAssetCalls::forcedTransfer(call) => Self::forced_transfer(asset_id, call, env), + IFungibleAssetCalls::canSend(call) => Self::can_send(asset_id, call, env), + IFungibleAssetCalls::canReceive(call) => Self::can_receive(asset_id, call, env), + IFungibleAssetCalls::getFrozenTokens(call) => { + Self::get_frozen_tokens(asset_id, call, env) + } + IFungibleAssetCalls::setFrozenTokens(call) => { + Self::set_frozen_tokens(asset_id, call, env) + } } } } diff --git a/pallets/runtime/tests/src/portfolio.rs b/pallets/runtime/tests/src/portfolio.rs index ab700f760c..270a5a629c 100644 --- a/pallets/runtime/tests/src/portfolio.rs +++ b/pallets/runtime/tests/src/portfolio.rs @@ -303,6 +303,7 @@ fn do_move_asset_from_portfolio(memo: Option) { &PortfolioId::user_portfolio(bob.did, PortfolioNumber(666)).into(), &asset_id, 1, + false, ), Error::PortfolioDoesNotExist ); diff --git a/pallets/runtime/tests/src/sto_test.rs b/pallets/runtime/tests/src/sto_test.rs index fdd6401a09..c124ecebf9 100644 --- a/pallets/runtime/tests/src/sto_test.rs +++ b/pallets/runtime/tests/src/sto_test.rs @@ -123,6 +123,7 @@ fn raise_happy_path() { None, None, IdentityId::default(), + false, &mut weight_meter )); @@ -365,6 +366,7 @@ fn raise_unhappy_path() { None, None, IdentityId::default(), + false, &mut weight_meter )); diff --git a/pallets/settlement/src/lib.rs b/pallets/settlement/src/lib.rs index 89e8202dad..4b0303328b 100644 --- a/pallets/settlement/src/lib.rs +++ b/pallets/settlement/src/lib.rs @@ -1605,7 +1605,12 @@ impl Pallet { FundDescription::Fungible { asset_id, amount } => { ensure!(amount > 0, Error::::ZeroAmount); Asset::::ensure_asset_is_not_frozen(&asset_id)?; - Asset::::ensure_sufficient_balance(&resolved_from, &asset_id, amount)?; + Asset::::ensure_sufficient_balance( + &resolved_from, + &asset_id, + amount, + false, + )?; Asset::::transfer_holders_balance( resolved_from.clone(), to.clone(), diff --git a/pallets/weights/src/pallet_asset.rs b/pallets/weights/src/pallet_asset.rs index f6acc6931c..f18dce7db6 100644 --- a/pallets/weights/src/pallet_asset.rs +++ b/pallets/weights/src/pallet_asset.rs @@ -877,4 +877,58 @@ impl pallet_asset::WeightInfo for SubstrateWeight { // Minimum execution time: 194_666 nanoseconds. Weight::from_parts(198_650_000, 0).saturating_add(DbWeight::get().reads(25)) } + // Storage: `Identity::KeyRecords` (r:1 w:0) + // Proof: `Identity::KeyRecords` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + // Storage: `ExternalAgents::GroupOfAgent` (r:1 w:0) + // Proof: `ExternalAgents::GroupOfAgent` (`max_values`: None, `max_size`: Some(77), added: 2552, mode: `MaxEncodedLen`) + // Storage: `Permissions::CurrentPalletName` (r:1 w:0) + // Proof: `Permissions::CurrentPalletName` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + // Storage: `Permissions::CurrentDispatchableName` (r:1 w:0) + // Proof: `Permissions::CurrentDispatchableName` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + // Storage: `Asset::Assets` (r:1 w:0) + // Proof: `Asset::Assets` (`max_values`: None, `max_size`: None, mode: `Measured`) + // Storage: `Portfolio::Portfolios` (r:1 w:0) + // Proof: `Portfolio::Portfolios` (`max_values`: None, `max_size`: None, mode: `Measured`) + // Storage: `Portfolio::PortfolioFrozenAssets` (r:0 w:1) + // Proof: `Portfolio::PortfolioFrozenAssets` (`max_values`: None, `max_size`: Some(97), added: 2572, mode: `MaxEncodedLen`) + fn set_frozen_tokens() -> Weight { + // Minimum execution time: 62_991 nanoseconds. + Weight::from_parts(64_868_000, 0) + .saturating_add(DbWeight::get().reads(6)) + .saturating_add(DbWeight::get().writes(1)) + } + // Storage: `Asset::FrozenBalance` (r:1 w:0) + // Proof: `Asset::FrozenBalance` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + fn get_holders_frozen_balance() -> Weight { + // Minimum execution time: 15_089 nanoseconds. + Weight::from_parts(15_834_000, 0).saturating_add(DbWeight::get().reads(1)) + } + // Storage: `Asset::Assets` (r:1 w:0) + // Proof: `Asset::Assets` (`max_values`: None, `max_size`: None, mode: `Measured`) + // Storage: `Identity::KeyRecords` (r:1 w:0) + // Proof: `Identity::KeyRecords` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + // Storage: `Asset::Frozen` (r:1 w:0) + // Proof: `Asset::Frozen` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + // Storage: `ComplianceManager::AssetCompliances` (r:1 w:0) + // Proof: `ComplianceManager::AssetCompliances` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn transfer_is_allowed_for_holder_best_case() -> Weight { + // Minimum execution time: 80_396 nanoseconds. + Weight::from_parts(82_062_000, 0).saturating_add(DbWeight::get().reads(4)) + } + // Storage: `Asset::Assets` (r:1 w:0) + // Proof: `Asset::Assets` (`max_values`: None, `max_size`: None, mode: `Measured`) + // Storage: `Identity::KeyRecords` (r:1 w:0) + // Proof: `Identity::KeyRecords` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + // Storage: `Asset::Frozen` (r:1 w:0) + // Proof: `Asset::Frozen` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + // Storage: `ComplianceManager::AssetCompliances` (r:1 w:0) + // Proof: `ComplianceManager::AssetCompliances` (`max_values`: None, `max_size`: None, mode: `Measured`) + // Storage: `Timestamp::Now` (r:1 w:0) + // Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + // Storage: `Identity::Claims` (r:1 w:0) + // Proof: `Identity::Claims` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn transfer_is_allowed_for_holder_worst_case() -> Weight { + // Minimum execution time: 92_745 nanoseconds. + Weight::from_parts(94_474_000, 0).saturating_add(DbWeight::get().reads(6)) + } } diff --git a/precompiles/src/interfaces/FungibleAssetStub.bin b/precompiles/src/interfaces/FungibleAssetStub.bin index 45fcf4d3c4..7490fb2805 100644 Binary files a/precompiles/src/interfaces/FungibleAssetStub.bin and b/precompiles/src/interfaces/FungibleAssetStub.bin differ diff --git a/precompiles/src/interfaces/FungibleAssetStub.sol b/precompiles/src/interfaces/FungibleAssetStub.sol index 0cd924e6c2..fa4478acf5 100644 --- a/precompiles/src/interfaces/FungibleAssetStub.sol +++ b/precompiles/src/interfaces/FungibleAssetStub.sol @@ -32,12 +32,6 @@ interface IFungibleAsset { /// a call to {approve}. `value` is the new allowance. event Approval(address indexed owner, address indexed spender, uint256 value); - /// @notice Emitted when tokens are taken from one address and transferred to another. - /// @param from The address from which tokens were taken. - /// @param to The address to which seized tokens were transferred. - /// @param amount The amount seized. - event ForcedTransfer(address indexed from, address indexed to, uint256 amount); - /// @dev Returns the value of tokens in existence. function totalSupply() external view returns (uint256); @@ -162,6 +156,19 @@ interface IFungibleAsset { // ============================================================ // ERC-7943 // ============================================================ + + /// @notice Emitted when tokens are taken from one address and transferred to another. + /// @param from The address from which tokens were taken. + /// @param to The address to which seized tokens were transferred. + /// @param amount The amount seized. + event ForcedTransfer(address indexed from, address indexed to, uint256 amount); + + /// @notice Emitted when `setFrozenTokens` is called, changing the frozen `amount` of tokens for `account`. + /// @param account The address of the account whose tokens are being frozen. + /// @param amount The amount of tokens frozen after the change. + event Frozen(address indexed account, uint256 amount); + + /// @notice Checks if a transfer is possible according to token rules. /// @dev This involves compliance checks. /// @param from The address sending tokens. @@ -176,6 +183,32 @@ interface IFungibleAsset { /// @param amount The amount to force transfer. /// @return True if the transfer executed correctly. Reverts on failure. function forcedTransfer(address from, uint256 amount) external returns (bool); + + /// @notice Changes the frozen status of `amount` tokens belonging to `account`. + /// @dev Overwrites the current value, similar to an `approve` function. + /// Requires specific authorization. Frozen tokens cannot be transferred by the account. + /// @param account The address of the account whose tokens are to be frozen. + /// @param amount The amount of tokens to freeze. It can be greater than the account balance. + /// @return True if the freezing executed correctly. Reverts on failure. + function setFrozenTokens(address account, uint256 amount) external returns (bool); + + /// @notice Checks the frozen status/amount. + /// @param account The address of the account. + /// @dev It could return an amount higher than the account's balance. + /// @return The amount of tokens currently frozen for `account`. + function getFrozenTokens(address account) external view returns (uint256); + + /// @notice Checks if a specific account is allowed to send tokens according to token rules. + /// @dev This is often used for allowlist/KYC/KYB/AML checks. + /// @param account The address to check. + /// @return True if the account is allowed to send, false otherwise. + function canSend(address account) external view returns (bool); + + /// @notice Checks if a specific account is allowed to receive tokens according to token rules. + /// @dev This is often used for allowlist/KYC/KYB/AML checks. + /// @param account The address to check. + /// @return True if the account is allowed to receive, false otherwise. + function canReceive(address account) external view returns (bool); } contract FungibleAssetStub is IFungibleAsset { @@ -283,4 +316,25 @@ contract FungibleAssetStub is IFungibleAsset { amount; revert NotExecutable(); } + + function setFrozenTokens(address account, uint256 amount) external override returns (bool) { + account; + amount; + revert NotExecutable(); + } + + function getFrozenTokens(address account) external view override returns (uint256) { + account; + revert NotExecutable(); + } + + function canSend(address account) external view override returns (bool) { + account; + revert NotExecutable(); + } + + function canReceive(address account) external view override returns (bool) { + account; + revert NotExecutable(); + } } diff --git a/primitives/src/traits.rs b/primitives/src/traits.rs index 88addd22f1..61d9576827 100644 --- a/primitives/src/traits.rs +++ b/primitives/src/traits.rs @@ -132,6 +132,14 @@ pub trait ComplianceFnConfig { weight_meter: &mut WeightMeter, ) -> Result; + /// Returns true if the asset holder can send/receive the asset based on the asset's compliance rules. + fn is_holder_compliant( + holder_did: IdentityId, + asset_id: &AssetId, + holder_is_the_sender: bool, + weight_meter: &mut WeightMeter, + ) -> bool; + #[cfg(feature = "runtime-benchmarks")] fn setup_asset_compliance( caler_did: IdentityId,