From aba06f60fe21635197cf6d330fa7e70f4a22af23 Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Thu, 19 Oct 2023 15:33:07 +0200 Subject: [PATCH] First commiint --- substrate/frame/assets/src/impl_fungibles.rs | 99 ++++++++++++ substrate/frame/assets/src/lib.rs | 33 +++- substrate/frame/assets/src/mock.rs | 2 + substrate/frame/assets/src/tests.rs | 144 +++++++++++++++++- substrate/frame/assets/src/types.rs | 13 +- .../frame/nft-fractionalization/src/mock.rs | 2 + 6 files changed, 289 insertions(+), 4 deletions(-) diff --git a/substrate/frame/assets/src/impl_fungibles.rs b/substrate/frame/assets/src/impl_fungibles.rs index 123abeba8283f..93f184ee199e9 100644 --- a/substrate/frame/assets/src/impl_fungibles.rs +++ b/substrate/frame/assets/src/impl_fungibles.rs @@ -308,3 +308,102 @@ impl, I: 'static> fungibles::InspectEnumerable for Pa Asset::::iter_keys() } } + +impl, I: 'static> fungibles::MutateHold for Pallet {} + +impl, I: 'static> fungibles::InspectHold for Pallet { + type Reason = HoldReason; + + fn total_balance_on_hold(asset: T::AssetId, who: &T::AccountId) -> T::Balance { + Holds::::get(who, asset) + .iter() + .map(|x| x.amount) + .fold(T::Balance::zero(), |acc, x| acc.saturating_add(x)) + } + + fn reducible_total_balance_on_hold( + asset: T::AssetId, + who: &T::AccountId, + _force: Fortitude, + ) -> Self::Balance { + let total_hold = Self::total_balance_on_hold(asset.clone(), who); + let free = Account::::get(asset.clone(), who) + .map(|account| account.balance) + .unwrap_or(Self::Balance::zero()); + // take alternative of unwrap + let ed = Asset::::get(asset).map(|x| x.min_balance).unwrap(); + + if free.saturating_sub(total_hold) < ed { + return total_hold.saturating_sub(ed); + } + total_hold + } + fn balance_on_hold(asset: T::AssetId, reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + Holds::::get(who, asset) + .iter() + .find(|x| &x.id == reason) + .map_or_else(Zero::zero, |x| x.amount) + } + fn hold_available(asset: T::AssetId, reason: &Self::Reason, who: &T::AccountId) -> bool { + let asset_details = Asset::::get(asset.clone()).unwrap(); + let holds = Holds::::get(who, asset); + if !holds.is_full() && asset_details.is_sufficient == true { + return true; + } + + if frame_system::Pallet::::providers(who) == 0 { + return false; + } + + if holds.is_full() && !holds.iter().any(|x| &x.id == reason) { + return false; + } + true + } +} + +impl, I: 'static> fungibles::UnbalancedHold for Pallet { + fn set_balance_on_hold( + asset: T::AssetId, + reason: &Self::Reason, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + let mut holds = Holds::::get(who, asset.clone()); + + if let Some(item) = holds.iter_mut().find(|x| &x.id == reason) { + let delta = item.amount.max(amount) - item.amount.min(amount); + let increase = amount > item.amount; + + if increase { + item.amount = item.amount.checked_add(&delta).ok_or(ArithmeticError::Overflow)? + } else { + item.amount = item.amount.checked_sub(&delta).ok_or(ArithmeticError::Underflow)? + }; + + holds.retain(|x| !x.amount.is_zero()); + } else { + if !amount.is_zero() { + holds + .try_push(IdAmount { id: *reason, amount }) + .map_err(|_| Error::::TooManyHolds)?; + } + } + + let account: Option> = Account::::get(&asset, &who); + + if let None = account { + let mut details = Asset::::get(&asset).ok_or(Error::::Unknown)?; + let new_account = AssetAccountOf:: { + balance: Zero::zero(), + status: AccountStatus::Liquid, + reason: Self::new_account(who, &mut details, None)?, + extra: T::Extra::default(), + }; + Account::::insert(&asset, &who, new_account); + } + + Holds::::insert(who, asset, holds); + Ok(()) + } +} diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index 79e4fe3001872..e30cb54cee99c 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -276,6 +276,9 @@ pub mod pallet { Success = Self::AccountId, >; + /// Overarching hold reason. + type RuntimeHoldReason: From; + /// The origin which may forcibly create or destroy an asset or otherwise alter privileged /// attributes. type ForceOrigin: EnsureOrigin; @@ -306,6 +309,10 @@ pub mod pallet { #[pallet::constant] type StringLimit: Get; + /// The maximum number of holds that can exist on an account at any time. + #[pallet::constant] + type MaxHolds: Get; + /// A hook to allow a per-asset, per-account minimum balance to be enforced. This must be /// respected in all permissionless operations. type Freezer: FrozenBalance; @@ -368,6 +375,26 @@ pub mod pallet { ValueQuery, >; + #[pallet::storage] + /// Holds on account balances. + pub type Holds, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + T::AssetId, + BoundedVec, T::MaxHolds>, + ValueQuery, + >; + + /// A reason for the pallet placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// Transfer hold reason + #[codec(index = 0)] + Transfer, + } + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig, I: 'static = ()> { @@ -571,6 +598,10 @@ pub mod pallet { NotFrozen, /// Callback action resulted in error CallbackFailed, + /// Number of holds exceed `MaxHolds` + TooManyHolds, + /// Error to update holds + HoldsNotUpdated, } #[pallet::call(weight(>::WeightInfo))] @@ -1069,7 +1100,7 @@ pub mod pallet { ensure!(details.status == AssetStatus::Live, Error::::LiveAsset); ensure!(origin == details.owner, Error::::NoPermission); if details.owner == owner { - return Ok(()) + return Ok(()); } let metadata_deposit = Metadata::::get(&id).deposit; diff --git a/substrate/frame/assets/src/mock.rs b/substrate/frame/assets/src/mock.rs index 32ad02da90412..172357c95c531 100644 --- a/substrate/frame/assets/src/mock.rs +++ b/substrate/frame/assets/src/mock.rs @@ -150,6 +150,8 @@ impl Config for Test { type CallbackHandle = AssetsCallbackHandle; type Extra = (); type RemoveItemsLimit = ConstU32<5>; + type MaxHolds = ConstU32<10>; + type RuntimeHoldReason = RuntimeHoldReason; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/substrate/frame/assets/src/tests.rs b/substrate/frame/assets/src/tests.rs index f1b116a0f4a0d..7e60afffffc23 100644 --- a/substrate/frame/assets/src/tests.rs +++ b/substrate/frame/assets/src/tests.rs @@ -22,7 +22,11 @@ use crate::{mock::*, Error}; use frame_support::{ assert_noop, assert_ok, dispatch::GetDispatchInfo, - traits::{fungibles::InspectEnumerable, tokens::Preservation::Protect, Currency}, + traits::{ + fungibles::InspectEnumerable, + tokens::{Fortitude::Polite, Precision::Exact, Preservation::Protect}, + Currency, + }, }; use pallet_balances::Error as BalancesError; use sp_io::storage; @@ -1775,3 +1779,141 @@ fn asset_destroy_refund_existence_deposit() { assert_eq!(Balances::reserved_balance(&admin), 0); }); } + +#[test] +fn unbalanced_trait_set_balance_works() { + new_test_ext().execute_with(|| { + let asset = 0; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset, 1, false, 1)); + let admin = 1; + let dest = 2; // account with own deposit + Balances::make_free_balance_be(&admin, 100); + Balances::make_free_balance_be(&dest, 100); + + assert_eq!(>::balance(asset, &dest), 0); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), asset, dest, 100)); + assert_eq!(>::balance(asset, &dest), 100); + + assert_ok!(>::hold( + asset, + &HoldReason::Transfer.into(), + &dest, + 60 + )); + assert_eq!(>::balance(asset, &dest), 40); + assert_eq!(>::total_balance_on_hold(asset, &dest), 60); + assert_eq!( + >::balance_on_hold( + asset, + &HoldReason::Transfer.into(), + &dest + ), + 60 + ); + + assert_eq!( + >::balance_on_hold( + asset, + &HoldReason::Transfer.into(), + &dest + ), + 60 + ); + + assert_ok!(>::release( + asset, + &HoldReason::Transfer.into(), + &dest, + 30, + Exact + )); + + assert_eq!( + >::balance_on_hold( + asset, + &HoldReason::Transfer.into(), + &dest + ), + 30 + ); + assert_eq!(>::total_balance_on_hold(asset, &dest), 30); + + assert_ok!(>::release( + asset, + &HoldReason::Transfer.into(), + &dest, + 30, + Exact + )); + + assert_eq!( + >::balance_on_hold( + asset, + &HoldReason::Transfer.into(), + &dest + ), + 0 + ); + assert_eq!(>::total_balance_on_hold(asset, &dest), 0); + let holds = Holds::::get(&dest, asset); + assert_eq!(holds.len(), 0); + }); +} + +#[test] +fn transfer_and_hold_works() { + new_test_ext().execute_with(|| { + let asset = 0; + let admin = 1; + let source = 2; // account with own deposit + let dest = 3; // account with own deposit + assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset, admin, true, 1)); + + Balances::make_free_balance_be(&admin, 100); + Balances::make_free_balance_be(&source, 100); + + assert_eq!(>::balance(asset, &source), 0); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), asset, source, 100)); + + assert_eq!(>::balance(asset, &source), 100); + + assert_ok!(>::transfer_and_hold( + asset, + &HoldReason::Transfer.into(), + &source, + &dest, + 60, + Exact, + Protect, + Polite + )); + + assert_eq!(>::balance(asset, &source), 40); + assert_eq!( + >::balance_on_hold( + asset, + &HoldReason::Transfer.into(), + &dest + ), + 60 + ); + assert_eq!(>::total_balance_on_hold(asset, &dest), 60); + + assert_ok!(>::release( + asset, + &HoldReason::Transfer.into(), + &dest, + 20, + Exact + )); + assert_eq!( + >::balance_on_hold( + asset, + &HoldReason::Transfer.into(), + &dest + ), + 40 + ); + assert_eq!(>::balance(asset, &dest), 20); + }); +} diff --git a/substrate/frame/assets/src/types.rs b/substrate/frame/assets/src/types.rs index 67f9bf07f5e7e..f798079565b65 100644 --- a/substrate/frame/assets/src/types.rs +++ b/substrate/frame/assets/src/types.rs @@ -87,6 +87,15 @@ pub struct Approval { pub(super) deposit: DepositBalance, } +/// An identifier and balance. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct IdAmount { + /// An identifier for this item. + pub id: Id, + /// Some amount for this item. + pub amount: Balance, +} + #[test] fn ensure_bool_decodes_to_consumer_or_sufficient() { assert_eq!(false.encode(), ExistenceReason::<(), ()>::Consumer.encode()); @@ -120,7 +129,7 @@ where { pub(crate) fn take_deposit(&mut self) -> Option { if !matches!(self, ExistenceReason::DepositHeld(_)) { - return None + return None; } if let ExistenceReason::DepositHeld(deposit) = sp_std::mem::replace(self, ExistenceReason::DepositRefunded) @@ -133,7 +142,7 @@ where pub(crate) fn take_deposit_from(&mut self) -> Option<(AccountId, Balance)> { if !matches!(self, ExistenceReason::DepositFrom(..)) { - return None + return None; } if let ExistenceReason::DepositFrom(depositor, deposit) = sp_std::mem::replace(self, ExistenceReason::DepositRefunded) diff --git a/substrate/frame/nft-fractionalization/src/mock.rs b/substrate/frame/nft-fractionalization/src/mock.rs index c690f0e580ed6..b3cd258846efb 100644 --- a/substrate/frame/nft-fractionalization/src/mock.rs +++ b/substrate/frame/nft-fractionalization/src/mock.rs @@ -113,6 +113,8 @@ impl pallet_assets::Config for Test { pallet_assets::runtime_benchmarks_enabled! { type BenchmarkHelper = (); } + type RuntimeHoldReason = RuntimeHoldReason; + type MaxHolds = ConstU32<1>; } parameter_types! {