diff --git a/integration/src/asset_helper.rs b/integration/src/asset_helper.rs index b17b3e7f27..54fa88db64 100644 --- a/integration/src/asset_helper.rs +++ b/integration/src/asset_helper.rs @@ -1,6 +1,8 @@ use anyhow::Result; use std::collections::BTreeSet; +#[cfg(feature = "current_release")] +use polymesh_api::types::polymesh_primitives::settlement::InstructionId; use polymesh_api::types::polymesh_primitives::{ asset::{AssetName, AssetType}, identity_id::{PortfolioId, PortfolioKind}, @@ -9,35 +11,73 @@ use polymesh_api::types::polymesh_primitives::{ use crate::*; +/// Get Pending Transfer ID from the transaction results. +#[cfg(feature = "current_release")] +pub async fn get_pending_transfer_id( + res: &mut TransactionResults, +) -> Result> { + if let Some(events) = res.events().await? { + for rec in &events.0 { + match &rec.event { + RuntimeEvent::Asset(AssetEvent::CreatedAssetTransfer { + pending_transfer_id, + .. + }) => { + return Ok(pending_transfer_id.clone()); + } + _ => (), + } + } + } + Ok(None) +} + /// Asset Helper. pub struct AssetHelper { pub api: Api, pub asset_id: AssetId, pub issuer: User, - pub issuer_venue_id: VenueId, + pub issuer_venue_id: Option, pub issuer_did: IdentityId, } impl AssetHelper { - /// Create a new asset, mint some tokens, and pause compliance rules. + /// Create a new asset and mint some tokens. pub async fn new( api: &Api, issuer: &mut User, name: &str, mint: u128, signers: Vec, + ) -> Result { + Self::new_full(api, issuer, name, mint, signers, true).await + } + + /// Create a new asset and mint some tokens. + pub async fn new_full( + api: &Api, + issuer: &mut User, + name: &str, + mint: u128, + signers: Vec, + need_venue: bool, ) -> Result { // Create a new venue. - let mut venue_res = api - .call() - .settlement() - .create_venue( - VenueDetails(format!("Venue for {name}").into()), - signers, - VenueType::Other, - )? - .submit_and_watch(issuer) - .await?; + let venue_res = if need_venue { + Some( + api.call() + .settlement() + .create_venue( + VenueDetails(format!("Venue for {name}").into()), + signers, + VenueType::Other, + )? + .submit_and_watch(issuer) + .await?, + ) + } else { + None + }; // Create a new asset. let mut asset_res = api @@ -66,22 +106,19 @@ impl AssetHelper { .submit_and_watch(issuer) .await?; - // Pause compliance rules to allow transfers. - let mut pause_res = api - .call() - .compliance_manager() - .pause_asset_compliance(asset_id)? - .submit_and_watch(issuer) - .await?; - - // Wait for mint and pause to complete. + // Wait for mint to complete. mint_res.ok().await?; - pause_res.ok().await?; // Get the venue ID from the response. - let issuer_venue_id = get_venue_id(&mut venue_res) - .await? - .expect("Venue ID not found"); + let issuer_venue_id = if let Some(mut venue_res) = venue_res { + Some( + get_venue_id(&mut venue_res) + .await? + .expect("Venue ID not found"), + ) + } else { + None + }; Ok(Self { api: api.clone(), @@ -155,7 +192,7 @@ impl AssetHelper { .call() .settlement() .add_and_affirm_instruction( - Some(self.issuer_venue_id), + self.issuer_venue_id, SettlementType::SettleManual(0), None, None, diff --git a/integration/tests/asset_transfers.rs b/integration/tests/asset_transfers.rs new file mode 100644 index 0000000000..59e3cf1a7b --- /dev/null +++ b/integration/tests/asset_transfers.rs @@ -0,0 +1,218 @@ +// >=v7.4 +#[cfg(feature = "current_release")] +mod asset_transfer_tests { + use anyhow::Result; + + use integration::*; + + /// Test for an asset transfer requiring receiver affirmation. + #[tokio::test] + async fn asset_transfer_with_receiver_affirm() -> Result<()> { + let mut tester = PolymeshTester::new().await?; + let mut users = tester + .users(&["AssetIssuer1", "Investor1"]) + .await? + .into_iter(); + let mut asset_issuer = users.next().expect("Asset issuer"); + let mut investor = users.next().expect("Investor"); + + // Create a new asset and mint some tokens. + let asset_helper = AssetHelper::new_full( + &tester.api, + &mut asset_issuer, + "TestAsset", + 1_000_000, + vec![], + false, + ) + .await?; + let asset_id = asset_helper.asset_id; + + // Create an asset transfer from the asset issuer to the investor. + let mut transfer_res = tester + .api + .call() + .asset() + .transfer_asset(asset_id, investor.account(), 10, None)? + .submit_and_watch(&mut asset_issuer) + .await?; + + // Get the pending transfer ID from the response. + let pending_transfer_id = get_pending_transfer_id(&mut transfer_res) + .await? + .expect("Pending Transfer ID not found"); + + // The receiver needs to affirm the transfer + let mut affirm_res = tester + .api + .call() + .asset() + .receiver_affirm_asset_transfer(pending_transfer_id)? + .submit_and_watch(&mut investor) + .await?; + + // Wait for the receiver affirmation to complete. + affirm_res.ok().await?; + + Ok(()) + } + + /// Test the receiver rejecting an asset transfer. + #[tokio::test] + async fn asset_transfer_rejected_by_receiver() -> Result<()> { + let mut tester = PolymeshTester::new().await?; + let mut users = tester + .users(&["AssetIssuer1", "Investor1"]) + .await? + .into_iter(); + let mut asset_issuer = users.next().expect("Asset issuer"); + let mut investor = users.next().expect("Investor"); + + // Create a new asset and mint some tokens. + let asset_helper = AssetHelper::new_full( + &tester.api, + &mut asset_issuer, + "TestAsset", + 1_000_000, + vec![], + false, + ) + .await?; + let asset_id = asset_helper.asset_id; + + // Create an asset transfer from the asset issuer to the investor. + let mut transfer_res = tester + .api + .call() + .asset() + .transfer_asset(asset_id, investor.account(), 10, None)? + .submit_and_watch(&mut asset_issuer) + .await?; + + // Get the pending transfer ID from the response. + let pending_transfer_id = get_pending_transfer_id(&mut transfer_res) + .await? + .expect("Pending Transfer ID not found"); + + // The receiver rejects the transfer + let mut reject_res = tester + .api + .call() + .asset() + .reject_asset_transfer(pending_transfer_id)? + .submit_and_watch(&mut investor) + .await?; + + // Wait for the receiver rejection to complete. + reject_res.ok().await?; + + Ok(()) + } + + /// The sender rejects the asset transfer before the receiver affirms it. + #[tokio::test] + async fn asset_transfer_rejected_by_sender() -> Result<()> { + let mut tester = PolymeshTester::new().await?; + let mut users = tester + .users(&["AssetIssuer1", "Investor1"]) + .await? + .into_iter(); + let mut asset_issuer = users.next().expect("Asset issuer"); + let investor = users.next().expect("Investor"); + + // Create a new asset and mint some tokens. + let asset_helper = AssetHelper::new_full( + &tester.api, + &mut asset_issuer, + "TestAsset", + 1_000_000, + vec![], + false, + ) + .await?; + let asset_id = asset_helper.asset_id; + + // Create an asset transfer from the asset issuer to the investor. + let mut transfer_res = tester + .api + .call() + .asset() + .transfer_asset(asset_id, investor.account(), 10, None)? + .submit_and_watch(&mut asset_issuer) + .await?; + + // Get the pending transfer ID from the response. + let pending_transfer_id = get_pending_transfer_id(&mut transfer_res) + .await? + .expect("Pending Transfer ID not found"); + + // The sender rejects the transfer + let mut reject_res = tester + .api + .call() + .asset() + .reject_asset_transfer(pending_transfer_id)? + .submit_and_watch(&mut asset_issuer) + .await?; + + // Wait for the sender rejection to complete. + reject_res.ok().await?; + + Ok(()) + } + + /// Test for an asset transfer with pre-approved receiver affirmation. + #[tokio::test] + async fn asset_transfer_with_receiver_pre_approved() -> Result<()> { + let mut tester = PolymeshTester::new().await?; + let mut users = tester + .users(&["AssetIssuer1", "Investor1"]) + .await? + .into_iter(); + let mut asset_issuer = users.next().expect("Asset issuer"); + let mut investor = users.next().expect("Investor"); + + // Create a new asset and mint some tokens. + let asset_helper = AssetHelper::new_full( + &tester.api, + &mut asset_issuer, + "TestAsset", + 1_000_000, + vec![], + false, + ) + .await?; + let asset_id = asset_helper.asset_id; + + // Receiver pre-approves asset transfers. + let mut pre_approve_res = tester + .api + .call() + .asset() + .pre_approve_asset(asset_id)? + .submit_and_watch(&mut investor) + .await?; + + // Wait for pre-approval to complete. + pre_approve_res.ok().await?; + + // Create an asset transfer from the asset issuer to the investor. + let mut transfer_res = tester + .api + .call() + .asset() + .transfer_asset(asset_id, investor.account(), 10, None)? + .submit_and_watch(&mut asset_issuer) + .await?; + + // Try to get the pending transfer ID from the response. + let pending_transfer_id = get_pending_transfer_id(&mut transfer_res).await?; + + assert!( + pending_transfer_id.is_none(), + "Pending Transfer ID should not be found for pre-approved transfer" + ); + + Ok(()) + } +} diff --git a/integration/tests/settlements.rs b/integration/tests/settlements.rs index 23c0aa66e4..d34c32b770 100644 --- a/integration/tests/settlements.rs +++ b/integration/tests/settlements.rs @@ -231,7 +231,7 @@ async fn offchain_settlement() -> Result<()> { .call() .settlement() .add_and_affirm_instruction( - Some(venue_id), + venue_id, SettlementType::SettleManual(0), None, None, diff --git a/pallets/asset/src/benchmarking.rs b/pallets/asset/src/benchmarking.rs index 071822ed21..0114cdac67 100644 --- a/pallets/asset/src/benchmarking.rs +++ b/pallets/asset/src/benchmarking.rs @@ -127,7 +127,7 @@ pub(crate) fn create_and_issue_sample_asset(asset_owner: &User::issue( asset_owner.origin().into(), asset_id, - (1_000_000 * POLY).into(), + (ONE_UNIT * POLY).into(), PortfolioKind::Default, ) .unwrap(); @@ -145,6 +145,7 @@ pub fn setup_asset_transfer( pause_compliance: bool, pause_restrictions: bool, n_mediators: u8, + move_to_sender_portfolio: bool, ) -> (PortfolioId, PortfolioId, Vec>, AssetId) { let sender_portfolio = create_portfolio::(sender, sender_portfolio_name.unwrap_or("SenderPortfolio")); @@ -153,7 +154,10 @@ pub fn setup_asset_transfer( // Creates the asset let asset_id = create_and_issue_sample_asset::(sender); - move_from_default_portfolio::(sender, asset_id, ONE_UNIT * POLY, sender_portfolio); + if move_to_sender_portfolio { + // Moves some asset to the sender portfolio + move_from_default_portfolio::(sender, asset_id, ONE_UNIT * POLY, sender_portfolio); + } // Sets mandatory mediators let mut asset_mediators = Vec::new(); @@ -664,7 +668,7 @@ benchmarks! { let mut weight_meter = WeightMeter::max_limit_no_minimum(); let (sender_portfolio, receiver_portfolio, _, asset_id) = - setup_asset_transfer::(&alice, &bob, None, None, true, true, 0); + setup_asset_transfer::(&alice, &bob, None, None, true, true, 0, true); }: { Pallet::::base_transfer( sender_portfolio, @@ -777,4 +781,54 @@ benchmarks! { .unwrap(); }: _(RawOrigin::Root, asset_metadata_name, asset_metadata_spec) + transfer_asset_base_weight { + let alice = UserBuilder::::default().generate_did().build("Alice"); + let bob = UserBuilder::::default().generate_did().build("Bob"); + + // Setup the transfer with worse case conditions. + // Don't move the assets from the default portfolio. + let (_sender_portfolio, _receiver_portfolio, _, asset_id) = + setup_asset_transfer::(&alice, &bob, None, None, true, true, 0, false); + }: { + Pallet::::base_transfer_asset( + alice.origin.into(), + asset_id, + bob.account(), + ONE_UNIT, + None, + // Only benchmark the base cost. + true, + ) + .unwrap(); + } + + receiver_affirm_asset_transfer_base_weight { + let alice = UserBuilder::::default().generate_did().build("Alice"); + let bob = UserBuilder::::default().generate_did().build("Bob"); + + // Setup the transfer with worse case conditions. + // Don't move the assets from the default portfolio. + let (_sender_portfolio, _receiver_portfolio, _, asset_id) = + setup_asset_transfer::(&alice, &bob, None, None, true, true, 0, false); + + let mut weight_meter = WeightMeter::max_limit_no_minimum(); + let instruction_id = T::SettlementFn::transfer_asset_and_try_execute( + alice.origin.into(), + bob.account(), + asset_id, + ONE_UNIT, + None, + &mut weight_meter, + false, + ).expect("Transfer setup must work"); + let instruction_id = instruction_id.expect("Pending transfer must have an ID"); + }: { + Pallet::::base_receiver_affirm_asset_transfer( + bob.origin.into(), + instruction_id, + // Only benchmark the base cost. + true, + ) + .expect("Receiver affirm must work"); + } } diff --git a/pallets/asset/src/lib.rs b/pallets/asset/src/lib.rs index 1681fc0574..bb4a49b8fd 100644 --- a/pallets/asset/src/lib.rs +++ b/pallets/asset/src/lib.rs @@ -89,12 +89,14 @@ mod types; use codec::{Decode, Encode}; use core::mem; use currency::*; -use frame_support::dispatch::{DispatchError, DispatchResult}; +use frame_support::dispatch::{ + DispatchError, DispatchResult, DispatchResultWithPostInfo, PostDispatchInfo, +}; use frame_support::ensure; use frame_support::traits::{Currency, Get, UnixTime}; use frame_support::weights::Weight; use frame_support::BoundedBTreeSet; -use frame_system::ensure_root; +use frame_system::{ensure_root, ensure_signed}; use sp_io::hashing::blake2_128; use sp_runtime::traits::Zero; use sp_std::collections::btree_set::BTreeSet; @@ -117,7 +119,9 @@ use polymesh_primitives::asset_metadata::{ }; use polymesh_primitives::constants::*; use polymesh_primitives::settlement::InstructionId; -use polymesh_primitives::traits::{AssetFnConfig, AssetFnTrait, ComplianceFnConfig, NFTTrait}; +use polymesh_primitives::traits::{ + AssetFnConfig, AssetFnTrait, ComplianceFnConfig, NFTTrait, SettlementFnTrait, +}; use polymesh_primitives::{ extract_auth, storage_migrate_on, storage_migration_ver, AssetIdentifier, Balance, Document, DocumentId, IdentityId, Memo, PortfolioId, PortfolioKind, PortfolioUpdateReason, Ticker, @@ -191,8 +195,12 @@ pub mod pallet { type WeightInfo: WeightInfo; + /// The implementation of nft functions. type NFTFn: NFTTrait; + /// The implementation of settlement asset transfer functions. + type SettlementFn: SettlementFnTrait; + /// Maximum number of mediators for an asset. #[pallet::constant] type MaxAssetMediators: Get; @@ -330,6 +338,15 @@ pub mod pallet { /// Asset Global Metadata Spec has been Updated. /// Parameters: [`AssetMetadataName`] of the metadata, [`AssetMetadataSpec`] of the metadata. GlobalMetadataSpecUpdated(AssetMetadataName, AssetMetadataSpec), + /// An asset transfer has been created. + CreatedAssetTransfer { + asset_id: AssetId, + from: T::AccountId, + to: T::AccountId, + amount: Balance, + memo: Option, + pending_transfer_id: Option, + }, } /// Map each [`Ticker`] to its registration details ([`TickerRegistration`]). @@ -1566,6 +1583,116 @@ pub mod pallet { ) -> DispatchResult { Self::base_update_global_metadata_spec(origin, asset_metadata_name, asset_metadata_spec) } + + /// Transfer assets from the caller's default portfolio to the target address's default portfolio. + /// + /// The settlement engine is used to perform the asset transfer. + /// + /// # Arguments + /// * `origin` - The origin of the call, which will be the sender of the assets. + /// * `asset_id` - The [`AssetId`] associated to the asset. + /// * `to` - The target address to which the assets will be sent. + /// * `amount` - The [`Balance`] of tokens that will be transferred. + /// * `memo` - An optional [`Memo`] that can be attached to the transfer instruction. + /// + /// # Permissions + /// * Asset + /// * Portfolio + /// + /// # Events + /// * `CreatedAssetTransfer` - When an asset transfer instruction is created. + /// * `InstructionCreated` - The asset transfer settlement was created. + /// * `InstructionAffirmed` - The asset transfer settlement was affirmed by the caller as the sender. + /// * `InstructionAutomaticallyAffirmed` - If the receiver pre-approved the asset, the instruction is automatically affirmed for the receiver. + /// * `InstructionExecuted` - The asset transfer settlement was executed successfully (if the receiver pre-approved the asset). + /// + /// # Errors + /// * `InsufficientBalance` - If the sender's balance is not sufficient to cover the transfer amount. + /// * `InvalidTransfer` - If the transfer validation check fails. + /// * `ReceiverIdentityNotFound` - If the receiver's identity is not found. + /// * `UnexpectedOFFChainAsset` - If the asset could not be found on-chain. + /// * `MissingIdentity` - The caller doesn't have an identity. + #[pallet::call_index(34)] + #[pallet::weight(::SettlementFn::transfer_and_try_execute_weight_meter(::WeightInfo::transfer_asset_base_weight(), true).limit())] + pub fn transfer_asset( + origin: OriginFor, + asset_id: AssetId, + to: T::AccountId, + amount: Balance, + memo: Option, + ) -> DispatchResultWithPostInfo { + Self::base_transfer_asset( + origin, + asset_id, + to, + amount, + memo, + #[cfg(feature = "runtime-benchmarks")] + false, + ) + } + + /// Receiver affirms a pending asset transfer. + /// + /// If someone tries to transfer asset to an account that requires receiver affirmations, then the receiver will need to affirm the transfer + /// before the transfer is executed. + /// + /// # Arguments + /// * `origin` - The origin of the call, which will be the receiver of the assets. + /// * `transfer_id` - The [`InstructionId`] associated to the pending transfer. + /// + /// # Permissions + /// * Asset + /// * Portfolio + /// + /// # Events + /// * `InstructionAffirmed` - The asset transfer settlement was affirmed by the caller as the receiver. + /// * `InstructionExecuted` - The asset transfer settlement was executed successfully. + /// * `AssetBalanceUpdated` - The asset balance was updated for both the sender and receiver portfolios. + /// + /// # Errors + /// * `UnknownInstruction` - If the instruction associated to the given transfer ID does not exist. + /// * `InvalidTransfer` - If the transfer validation check fails. + #[pallet::call_index(35)] + #[pallet::weight(::SettlementFn::receiver_affirm_transfer_and_try_execute_weight_meter(::WeightInfo::receiver_affirm_asset_transfer_base_weight(), true).limit())] + pub fn receiver_affirm_asset_transfer( + origin: OriginFor, + transfer_id: InstructionId, + ) -> DispatchResultWithPostInfo { + Self::base_receiver_affirm_asset_transfer( + origin, + transfer_id, + #[cfg(feature = "runtime-benchmarks")] + false, + ) + } + + /// Reject a pending asset transfer. + /// + /// If someone tries to transfer asset to an account that requires receiver affirmations, then the receiver can reject the transfer. + /// The sender can also reject the transfer before the receiver affirms it. + /// + /// # Arguments + /// * `origin` - The origin of the call, which can be either the sender or receiver. + /// * `transfer_id` - The [`InstructionId`] associated to the pending transfer. + /// + /// # Permissions + /// * Asset + /// * Portfolio + /// + /// # Events + /// * `InstructionRejected` - The asset transfer settlement was rejected by the caller. + /// + /// # Errors + /// * `InvalidInstructionStatusForRejection` - Either the instruction doesn't exist or it has already been executed or rejected. + #[pallet::call_index(36)] + #[pallet::weight(::SettlementFn::reject_transfer_weight_meter(true).limit())] + pub fn reject_asset_transfer( + origin: OriginFor, + transfer_id: InstructionId, + ) -> DispatchResultWithPostInfo { + Self::base_reject_asset_transfer(origin, transfer_id) + } } #[pallet::error] @@ -1707,6 +1834,8 @@ pub mod pallet { fn link_ticker_to_asset_id() -> Weight; fn unlink_ticker_from_asset_id() -> Weight; fn update_global_metadata_spec() -> Weight; + fn transfer_asset_base_weight() -> Weight; + fn receiver_affirm_asset_transfer_base_weight() -> Weight; } } @@ -2561,6 +2690,81 @@ impl Pallet { Ok(()) } + + pub fn base_transfer_asset( + origin: T::RuntimeOrigin, + asset_id: AssetId, + to: T::AccountId, + amount: Balance, + memo: Option, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> DispatchResultWithPostInfo { + let from = ensure_signed(origin.clone())?; + let mut weight_meter = ::SettlementFn::transfer_and_try_execute_weight_meter( + ::WeightInfo::transfer_asset_base_weight(), + true, + ); + + // Create the transfer instruction via the settlement engine and affirm it as the sender. + let instruction_id = T::SettlementFn::transfer_asset_and_try_execute( + origin, + to.clone(), + asset_id, + amount, + memo.clone(), + &mut weight_meter, + #[cfg(feature = "runtime-benchmarks")] + bench_base_weight, + )?; + + // Emit a transfer event. + Self::deposit_event(Event::CreatedAssetTransfer { + asset_id, + from, + to, + amount, + memo, + pending_transfer_id: instruction_id, + }); + + Ok(PostDispatchInfo::from(Some(weight_meter.consumed()))) + } + + pub fn base_receiver_affirm_asset_transfer( + origin: T::RuntimeOrigin, + transfer_id: InstructionId, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> DispatchResultWithPostInfo { + let mut weight_meter = + ::SettlementFn::receiver_affirm_transfer_and_try_execute_weight_meter( + ::WeightInfo::receiver_affirm_asset_transfer_base_weight(), + true, + ); + + // Affirm the transfer as the receiver and execute it. + T::SettlementFn::receiver_affirm_transfer_and_try_execute( + origin, + transfer_id, + true, + &mut weight_meter, + #[cfg(feature = "runtime-benchmarks")] + bench_base_weight, + )?; + + Ok(PostDispatchInfo::from(Some(weight_meter.consumed()))) + } + + pub fn base_reject_asset_transfer( + origin: T::RuntimeOrigin, + transfer_id: InstructionId, + ) -> DispatchResultWithPostInfo { + let mut weight_meter = ::SettlementFn::reject_transfer_weight_meter(true); + + // Reject the transfer. + T::SettlementFn::reject_transfer(origin, transfer_id, true, &mut weight_meter)?; + + Ok(PostDispatchInfo::from(Some(weight_meter.consumed()))) + } } //========================================================================== diff --git a/pallets/runtime/common/src/runtime.rs b/pallets/runtime/common/src/runtime.rs index 4bb6d0fed0..513c69f3aa 100644 --- a/pallets/runtime/common/src/runtime.rs +++ b/pallets/runtime/common/src/runtime.rs @@ -388,6 +388,7 @@ macro_rules! misc_pallet_impls { type AssetMetadataTypeDefMaxLength = AssetMetadataTypeDefMaxLength; type WeightInfo = polymesh_weights::pallet_asset::SubstrateWeight; type NFTFn = pallet_nft::Pallet; + type SettlementFn = pallet_settlement::Pallet; type MaxAssetMediators = MaxAssetMediators; } diff --git a/pallets/settlement/src/benchmarking.rs b/pallets/settlement/src/benchmarking.rs index 62ff9851ae..74cb2c253d 100644 --- a/pallets/settlement/src/benchmarking.rs +++ b/pallets/settlement/src/benchmarking.rs @@ -148,6 +148,7 @@ where pause_compliance, pause_restrictions, 4, + true, ); asset_mediators.append(&mut mediators); portfolios.sdr_portfolios.push(sdr_portfolio.clone()); @@ -794,6 +795,7 @@ benchmarks! { inst_id, None, Some(AssetCount::new(f, n, o)), + false, &mut WeightMeter::max_limit_no_minimum() ) .unwrap() diff --git a/pallets/settlement/src/lib.rs b/pallets/settlement/src/lib.rs index 30a2cd5434..9472facfaf 100644 --- a/pallets/settlement/src/lib.rs +++ b/pallets/settlement/src/lib.rs @@ -81,11 +81,11 @@ use polymesh_primitives::settlement::{ MediatorAffirmationStatus, Receipt, ReceiptDetails, ReceiptMetadata, SettlementType, Venue, VenueDetails, VenueId, VenueType, }; +use polymesh_primitives::traits::{AssetOrNft, PortfolioSubTrait, SettlementFnTrait}; use polymesh_primitives::with_transaction; use polymesh_primitives::SystematicIssuers::Settlement as SettlementDID; use polymesh_primitives::{ - storage_migration_ver, traits::PortfolioSubTrait, Balance, IdentityId, Memo, NFTs, PortfolioId, - SecondaryKey, WeightMeter, + storage_migration_ver, Balance, IdentityId, Memo, NFTs, PortfolioId, SecondaryKey, WeightMeter, }; type System = frame_system::Pallet; @@ -568,6 +568,8 @@ pub mod pallet { FailedAssetTransferringConditions, /// Locked instructions can't have affirmations withdrawn. InvalidInstructionStatusForWithdrawal, + /// Receiver identity not found. + ReceiverIdentityNotFound, } storage_migration_ver!(3); @@ -1135,7 +1137,14 @@ pub mod pallet { Self::reject_instruction_minimum_weight(), ::WeightInfo::reject_instruction(Some(AssetCount::new(10, 100, 10))), )?; - Self::base_reject_instruction(origin, id, Some(portfolio), None, &mut weight_meter) + Self::base_reject_instruction( + origin, + id, + Some(portfolio), + None, + false, + &mut weight_meter, + ) } /// Root callable extrinsic, used as an internal call to execute a scheduled settlement instruction. @@ -1248,6 +1257,7 @@ pub mod pallet { id, Some(portfolio), number_of_assets, + false, &mut weight_meter, ) .map_err(|e| e.error)?; @@ -1419,6 +1429,7 @@ pub mod pallet { instruction_id, None, number_of_assets, + false, &mut weight_meter, ) } @@ -2502,11 +2513,18 @@ impl Pallet { inst_id: InstructionId, caller_pid: Option, input_asset_count: Option, + use_default_portfolio: bool, weight_meter: &mut WeightMeter, ) -> DispatchResultWithPostInfo { let origin_data = pallet_identity::Pallet::::ensure_origin_call_permissions(origin)?; let caller_did = origin_data.primary_did; + let caller_pid = if use_default_portfolio { + Some(PortfolioId::default_portfolio(caller_did)) + } else { + caller_pid + }; + let inst_legs: Vec<_> = InstructionLegs::::iter_prefix(&inst_id).collect(); let inst_asset_count = AssetCount::from_legs(&inst_legs); @@ -2979,9 +2997,10 @@ impl Pallet { minimum_weight: Weight, weight_limit: Weight, ) -> Result { - WeightMeter::from_limit(minimum_weight, weight_limit).map_err(|_| { + // Check that the provided weight limit is greater than the minimum required weight + WeightMeter::from_limit(minimum_weight, weight_limit).ok_or_else(|| { DispatchErrorWithPostInfo { - post_info: Some(weight_limit).into(), + post_info: Some(minimum_weight).into(), error: Error::::InputWeightIsLessThanMinimum.into(), } }) @@ -3587,4 +3606,262 @@ impl Pallet { Ok(weight_meter.consumed()) } + + fn fungible_asset_count(is_fungible: bool) -> AssetCount { + let (fungible, non_fungible) = if is_fungible { (1, 0) } else { (0, 1) }; + AssetCount::new(fungible, non_fungible, 0) + } + + /// Attempts to execute an instruction. + fn base_try_execute_instruction( + origin: OriginFor, + instruction_id: InstructionId, + is_fungible: bool, + weight_meter: &mut WeightMeter, + ) -> DispatchResult { + let asset_count = Self::fungible_asset_count(is_fungible); + + Self::base_manual_execution( + origin, + instruction_id, + None, + &asset_count, + true, + weight_meter, + ) + .map_err(|e| e.error)?; + Ok(()) + } + + /// Initiates a transfer instruction for fungible or non-fungible assets. + fn base_transfer_and_try_execute( + origin: OriginFor, + to: T::AccountId, + asset_or_nft: AssetOrNft, + memo: Option, + weight_meter: &mut WeightMeter, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> Result, DispatchError> { + let origin_data = + pallet_identity::Pallet::::ensure_origin_call_permissions(origin.clone())?; + + let from_portfolio = PortfolioId::default_portfolio(origin_data.primary_did); + let to_did = pallet_identity::Pallet::::get_identity(&to) + .ok_or(Error::::ReceiverIdentityNotFound)?; + let to_portfolio = PortfolioId::default_portfolio(to_did); + + // Prepare the leg depending on whether it's a fungible or non-fungible transfer + let (leg, is_fungible) = match asset_or_nft { + AssetOrNft::Asset { asset_id, amount } => ( + Leg::Fungible { + sender: from_portfolio, + receiver: to_portfolio, + asset_id, + amount, + }, + true, + ), + AssetOrNft::Nft { asset_id, nft_id } => ( + Leg::NonFungible { + sender: from_portfolio, + receiver: to_portfolio, + nfts: NFTs::new_unverified(asset_id, vec![nft_id]), + }, + false, + ), + }; + + // Consume weight for the transfer and affirmation + Self::check_accrue(weight_meter, Self::transfer_weight(is_fungible))?; + + #[cfg(feature = "runtime-benchmarks")] + { + if bench_base_weight { + return Ok(Some(InstructionId(0))); + } + } + + // Create the instruction with the prepared leg + let instruction_id = Self::base_add_instruction( + origin_data.primary_did, + None, + SettlementType::SettleManual(System::::block_number()), + None, + None, + vec![leg], + memo, + None, + )?; + + // Affirm the instruction on behalf of the sender. + Self::unsafe_affirm_instruction( + origin_data.primary_did, + instruction_id, + [from_portfolio].into(), + origin_data.secondary_key.as_ref(), + None, + )?; + + let instruction_id = if InstructionAffirmsPending::::get(instruction_id) == 0 { + // If there are no pending affirmations, execute the instruction immediately. + Self::base_try_execute_instruction(origin, instruction_id, is_fungible, weight_meter)?; + + // The instruction was executed immediately, no need for receiver affirmation. + None + } else { + // The receiver's affirmation is still pending. + Some(instruction_id) + }; + + Ok(instruction_id) + } + + /// Receiver affirms the transfer of fungible or non-fungible assets. + fn base_receiver_affirm_transfer_and_try_execute( + origin: OriginFor, + instruction_id: InstructionId, + is_fungible: bool, + weight_meter: &mut WeightMeter, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> DispatchResult { + let origin_data = + pallet_identity::Pallet::::ensure_origin_call_permissions(origin.clone())?; + let to_portfolio = PortfolioId::default_portfolio(origin_data.primary_did); + + // Consume weight for the receiver affirmation + Self::check_accrue( + weight_meter, + Self::receiver_affirm_transfer_weight(is_fungible), + )?; + + // The affirmation count ensures that we are only affirming as the receiver. + let affirmation_count = AffirmationCount::new( + // No sender assets to affirm + AssetCount::default(), + // One receiver asset to affirm + Self::fungible_asset_count(is_fungible), + 0, + ); + + #[cfg(feature = "runtime-benchmarks")] + { + if bench_base_weight { + // Consume weight for trying to execute the instruction + Self::check_accrue(weight_meter, Self::try_execute_weight(is_fungible))?; + return Ok(()); + } + } + + // Affirm the instruction on behalf of the receiver. + Self::base_affirm_instruction( + origin.clone(), + instruction_id, + [to_portfolio].into(), + Some(affirmation_count), + )?; + + // Try to execute the instruction. + Self::base_try_execute_instruction(origin, instruction_id, is_fungible, weight_meter)?; + + Ok(()) + } +} + +impl SettlementFnTrait for Pallet { + /// Initiates a transfer instruction for fungible or non-fungible assets. + fn transfer_and_try_execute( + origin: OriginFor, + to: T::AccountId, + asset_or_nft: AssetOrNft, + memo: Option, + weight_meter: &mut WeightMeter, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> Result, DispatchErrorWithPostInfo> { + let instruction_id = Self::base_transfer_and_try_execute( + origin, + to, + asset_or_nft, + memo, + weight_meter, + #[cfg(feature = "runtime-benchmarks")] + bench_base_weight, + ) + .map_err(|error| DispatchErrorWithPostInfo { + post_info: Some(weight_meter.consumed()).into(), + error, + })?; + + Ok(instruction_id) + } + + /// Receiver affirms the transfer of fungible or non-fungible assets. + fn receiver_affirm_transfer_and_try_execute( + origin: OriginFor, + instruction_id: InstructionId, + is_fungible: bool, + weight_meter: &mut WeightMeter, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> DispatchResultWithPostInfo { + Self::base_receiver_affirm_transfer_and_try_execute( + origin, + instruction_id, + is_fungible, + weight_meter, + #[cfg(feature = "runtime-benchmarks")] + bench_base_weight, + ) + .map_err(|error| DispatchErrorWithPostInfo { + post_info: Some(weight_meter.consumed()).into(), + error, + })?; + + Ok(PostDispatchInfo::from(Some(weight_meter.consumed()))) + } + + /// Get the transfer weight based on the type of asset. + fn transfer_weight(is_fungible: bool) -> Weight { + let (fungible, non_fungible) = if is_fungible { (1, 0) } else { (0, 1) }; + ::WeightInfo::add_instruction(fungible, non_fungible, 0).saturating_add( + ::WeightInfo::affirm_instruction(fungible, non_fungible), + ) + } + + /// Get the try execute weight based on the type of asset. + fn try_execute_weight(is_fungible: bool) -> Weight { + let (fungible, non_fungible) = if is_fungible { (1, 0) } else { (0, 1) }; + ::WeightInfo::execute_manual_instruction(fungible, non_fungible, 0) + } + + /// Get the receiver affirm transfer weight based on the type of asset. + fn receiver_affirm_transfer_weight(is_fungible: bool) -> Weight { + let (fungible, non_fungible) = if is_fungible { (1, 0) } else { (0, 1) }; + ::WeightInfo::affirm_instruction_rcv(fungible, non_fungible) + } + + /// Reject a transfer instruction. + fn reject_transfer( + origin: OriginFor, + instruction_id: InstructionId, + is_fungible: bool, + weight_meter: &mut WeightMeter, + ) -> DispatchResultWithPostInfo { + let asset_count = Self::fungible_asset_count(is_fungible); + Self::base_reject_instruction( + origin, + instruction_id, + None, + Some(asset_count), + true, + weight_meter, + ) + } + + /// Get the reject transfer weight meter. + fn reject_transfer_weight_meter(is_fungible: bool) -> WeightMeter { + let asset_count = Self::fungible_asset_count(is_fungible); + WeightMeter::from_limit_unchecked( + Self::reject_instruction_minimum_weight(), + ::WeightInfo::reject_instruction(Some(asset_count)), + ) + } } diff --git a/pallets/sto/src/benchmarking.rs b/pallets/sto/src/benchmarking.rs index 21fcf3ad9e..3ff00fca59 100644 --- a/pallets/sto/src/benchmarking.rs +++ b/pallets/sto/src/benchmarking.rs @@ -42,6 +42,7 @@ where false, false, 0, + true, ); let (investor_raising_portfolio, fundraiser_raising_portfolio, _, raising_asset_id) = setup_asset_transfer( @@ -52,6 +53,7 @@ where false, false, 0, + true, ); let trusted_user = UserBuilder::::default() diff --git a/pallets/weights/src/pallet_asset.rs b/pallets/weights/src/pallet_asset.rs index 055d766b62..9affe8717f 100644 --- a/pallets/weights/src/pallet_asset.rs +++ b/pallets/weights/src/pallet_asset.rs @@ -941,4 +941,28 @@ impl pallet_asset::WeightInfo for SubstrateWeight { .saturating_add(DbWeight::get().reads(2)) .saturating_add(DbWeight::get().writes(1)) } + // Storage: Identity KeyRecords (r:2 w:0) + // Proof: Identity KeyRecords (max_values: None, max_size: Some(73), added: 2548, mode: MaxEncodedLen) + // Storage: Timestamp Now (r:1 w:0) + // Proof: Timestamp Now (max_values: Some(1), max_size: Some(8), added: 503, mode: MaxEncodedLen) + // Storage: CddServiceProviders ActiveMembers (r:1 w:0) + // Proof Skipped: CddServiceProviders ActiveMembers (max_values: Some(1), max_size: None, mode: Measured) + // Storage: Identity Claims (r:2 w:0) + // Proof Skipped: Identity Claims (max_values: None, max_size: None, mode: Measured) + fn transfer_asset_base_weight() -> Weight { + // Minimum execution time: 28_885 nanoseconds. + Weight::from_ref_time(30_178_000).saturating_add(DbWeight::get().reads(6)) + } + // Storage: Identity KeyRecords (r:1 w:0) + // Proof: Identity KeyRecords (max_values: None, max_size: Some(73), added: 2548, mode: MaxEncodedLen) + // Storage: Timestamp Now (r:1 w:0) + // Proof: Timestamp Now (max_values: Some(1), max_size: Some(8), added: 503, mode: MaxEncodedLen) + // Storage: CddServiceProviders ActiveMembers (r:1 w:0) + // Proof Skipped: CddServiceProviders ActiveMembers (max_values: Some(1), max_size: None, mode: Measured) + // Storage: Identity Claims (r:2 w:0) + // Proof Skipped: Identity Claims (max_values: None, max_size: None, mode: Measured) + fn receiver_affirm_asset_transfer_base_weight() -> Weight { + // Minimum execution time: 24_004 nanoseconds. + Weight::from_ref_time(26_399_000).saturating_add(DbWeight::get().reads(5)) + } } diff --git a/primitives/src/traits.rs b/primitives/src/traits.rs index 7f92eb084f..2d0caf1369 100644 --- a/primitives/src/traits.rs +++ b/primitives/src/traits.rs @@ -27,7 +27,9 @@ use crate::{asset::NonFungibleType, NFTCollectionKeys}; mod asset; pub mod group; +mod settlement; pub use asset::*; +pub use settlement::*; // Polymesh note: This was specifically added for Polymesh pub trait CddAndFeeDetails { diff --git a/primitives/src/traits/settlement.rs b/primitives/src/traits/settlement.rs new file mode 100644 index 0000000000..efd50d5436 --- /dev/null +++ b/primitives/src/traits/settlement.rs @@ -0,0 +1,114 @@ +use frame_support::dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo}; +use frame_support::weights::Weight; +use frame_system::{pallet_prelude::OriginFor, Config}; + +use crate::{asset::AssetId, settlement::InstructionId, Balance, Memo, NFTId, WeightMeter}; + +/// Enum representing either a fungible asset or a non-fungible token. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum AssetOrNft { + Asset { asset_id: AssetId, amount: Balance }, + Nft { asset_id: AssetId, nft_id: NFTId }, +} + +/// Trait defining settlement functions for transferring assets. +pub trait SettlementFnTrait { + /// Creates a transfer instruction for a fungible asset and attempts to execute it (if no pending affirmations). + fn transfer_asset_and_try_execute( + origin: OriginFor, + to: T::AccountId, + asset_id: AssetId, + amount: Balance, + memo: Option, + weight_meter: &mut WeightMeter, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> Result, DispatchErrorWithPostInfo> { + Self::transfer_and_try_execute( + origin, + to, + AssetOrNft::Asset { asset_id, amount }, + memo, + weight_meter, + #[cfg(feature = "runtime-benchmarks")] + bench_base_weight, + ) + } + + /// Creates a transfer instruction for a non-fungible asset and attempts to execute it (if no pending affirmations). + fn transfer_nft_and_try_execute( + origin: OriginFor, + to: T::AccountId, + asset_id: AssetId, + nft_id: NFTId, + memo: Option, + weight_meter: &mut WeightMeter, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> Result, DispatchErrorWithPostInfo> { + Self::transfer_and_try_execute( + origin, + to, + AssetOrNft::Nft { asset_id, nft_id }, + memo, + weight_meter, + #[cfg(feature = "runtime-benchmarks")] + bench_base_weight, + ) + } + + /// Creates a transfer instruction for fungible or non-fungible assets and attempts to execute it (if no pending affirmations). + fn transfer_and_try_execute( + origin: OriginFor, + to: T::AccountId, + asset_or_nft: AssetOrNft, + memo: Option, + weight_meter: &mut WeightMeter, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> Result, DispatchErrorWithPostInfo>; + + /// Receiver affirms the transfer of fungible or non-fungible assets and attempts to execute it. + fn receiver_affirm_transfer_and_try_execute( + origin: OriginFor, + instruction_id: InstructionId, + is_fungible: bool, + weight_meter: &mut WeightMeter, + #[cfg(feature = "runtime-benchmarks")] bench_base_weight: bool, + ) -> DispatchResultWithPostInfo; + + /// Reject a transfer instruction. + fn reject_transfer( + origin: OriginFor, + instruction_id: InstructionId, + is_fungible: bool, + weight_meter: &mut WeightMeter, + ) -> DispatchResultWithPostInfo; + + /// Get the transfer weight based on the type of asset. + fn transfer_weight(is_fungible: bool) -> Weight; + + /// Get the try execute weight based on the type of asset. + fn try_execute_weight(is_fungible: bool) -> Weight; + + /// Get the transfer and try execute weight based on the type of asset. + fn transfer_and_try_execute_weight_meter(base: Weight, is_fungible: bool) -> WeightMeter { + let minimum_charge = Self::transfer_weight(is_fungible).saturating_add(base); + let limit = minimum_charge.saturating_add(Self::try_execute_weight(is_fungible)); + WeightMeter::from_limit_unchecked(minimum_charge, limit) + } + + /// Get the receiver affirm transfer weight based on the type of asset. + fn receiver_affirm_transfer_weight(is_fungible: bool) -> Weight; + + /// Get the receiver affirm transfer and try execute weight based on the type of asset. + fn receiver_affirm_transfer_and_try_execute_weight_meter( + base: Weight, + is_fungible: bool, + ) -> WeightMeter { + let minimum_charge = + Self::receiver_affirm_transfer_weight(is_fungible).saturating_add(base); + let limit = minimum_charge.saturating_add(Self::try_execute_weight(is_fungible)); + WeightMeter::from_limit_unchecked(minimum_charge, limit) + } + + /// Get the reject transfer weight meter. + fn reject_transfer_weight_meter(is_fungible: bool) -> WeightMeter; +} diff --git a/primitives/src/weight_meter.rs b/primitives/src/weight_meter.rs index 7c53a141a6..037942f898 100644 --- a/primitives/src/weight_meter.rs +++ b/primitives/src/weight_meter.rs @@ -24,15 +24,31 @@ pub struct WeightMeter { } impl WeightMeter { + /// Creates [`Self`] from a limit for the maximal consumable weight and a minimum charge of `minimum_charge` without checking if the limit is less than the minimum charge. + pub fn from_limit_unchecked(minimum_charge: Weight, mut limit: Weight) -> Self { + if limit.checked_sub(&minimum_charge).is_none() { + // Log a warning to help with debugging issues with the minimum charge. + log::warn!( + "WeightMeter limit {:?} is less than minimum charge {:?}, setting limit to minimum charge", + limit, + minimum_charge + ); + limit = minimum_charge; + } + + Self { + minimum_charge, + meter: FrameWeightMeter::from_limit(limit), + } + } + /// Creates [`Self`] from a limit for the maximal consumable weight and a minimum charge of `minimum_charge`. - pub fn from_limit(minimum_charge: Weight, limit: Weight) -> Result { - if limit.ref_time() < minimum_charge.ref_time() { - return Err(String::from( - "The limit must be higher than the minimum_charge", - )); + pub fn from_limit(minimum_charge: Weight, limit: Weight) -> Option { + if limit.checked_sub(&minimum_charge).is_none() { + return None; } - Ok(Self { + Some(Self { minimum_charge, meter: FrameWeightMeter::from_limit(limit), })