diff --git a/libs/types/src/permissions.rs b/libs/types/src/permissions.rs index 57eee60d24..5d054d1486 100644 --- a/libs/types/src/permissions.rs +++ b/libs/types/src/permissions.rs @@ -36,6 +36,7 @@ pub enum PoolRole { LoanAdmin, TrancheInvestor(TrancheId, Seconds), PODReadAccess, + FrozenTrancheInvestor(TrancheId), } #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, TypeInfo, Debug)] @@ -110,6 +111,7 @@ pub struct PermissionedCurrencyHolderInfo { pub struct TrancheInvestorInfo { tranche_id: TrancheId, permissioned_till: Seconds, + is_frozen: bool, } #[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)] @@ -214,6 +216,7 @@ where PoolRole::PODReadAccess => { self.pool_admin.contains(PoolAdminRoles::POD_READ_ACCESS) } + PoolRole::FrozenTrancheInvestor(id) => self.tranche_investor.contains_frozen(id), }, Role::PermissionedCurrencyRole(permissioned_currency_role) => { match permissioned_currency_role { @@ -255,6 +258,7 @@ where PoolRole::PODReadAccess => { Ok(self.pool_admin.remove(PoolAdminRoles::POD_READ_ACCESS)) } + PoolRole::FrozenTrancheInvestor(id) => self.tranche_investor.unfreeze(id), }, Role::PermissionedCurrencyRole(permissioned_currency_role) => { match permissioned_currency_role { @@ -289,6 +293,7 @@ where PoolRole::PODReadAccess => { Ok(self.pool_admin.insert(PoolAdminRoles::POD_READ_ACCESS)) } + PoolRole::FrozenTrancheInvestor(id) => self.tranche_investor.freeze(id), }, Role::PermissionedCurrencyRole(permissioned_currency_role) => { match permissioned_currency_role { @@ -410,6 +415,12 @@ where }) } + pub fn contains_frozen(&self, tranche: TrancheId) -> bool { + self.info + .iter() + .any(|info| info.tranche_id == tranche && info.is_frozen) + } + #[allow(clippy::result_unit_err)] pub fn remove(&mut self, tranche: TrancheId, delta: Seconds) -> Result<(), ()> { if let Some(index) = self.info.iter().position(|info| info.tranche_id == tranche) { @@ -443,10 +454,27 @@ where .try_push(TrancheInvestorInfo { tranche_id: tranche, permissioned_till: validity, + is_frozen: false, }) .map_err(|_| ()) } } + + #[allow(clippy::result_unit_err)] + pub fn freeze(&mut self, tranche: TrancheId) -> Result<(), ()> { + if let Some(investor) = self.info.iter_mut().find(|t| t.tranche_id == tranche) { + investor.is_frozen = true; + } + Ok(()) + } + + #[allow(clippy::result_unit_err)] + pub fn unfreeze(&mut self, tranche: TrancheId) -> Result<(), ()> { + if let Some(investor) = self.info.iter_mut().find(|t| t.tranche_id == tranche) { + investor.is_frozen = false; + } + Ok(()) + } } #[cfg(test)] @@ -652,3 +680,60 @@ mod tests { assert!(!roles.exists(Role::PoolRole(PoolRole::PODReadAccess))); } } + +pub mod v0 { + use super::*; + + #[derive(Encode, Decode, TypeInfo, Clone, Eq, PartialEq, Debug, MaxEncodedLen)] + pub struct PermissionRoles> { + pool_admin: PoolAdminRoles, + currency_admin: CurrencyAdminRoles, + permissioned_asset_holder: PermissionedCurrencyHolders, + tranche_investor: TrancheInvestors, + } + + #[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)] + pub struct TrancheInvestors> { + info: BoundedVec, MaxTranches>, + _phantom: PhantomData<(Now, MinDelay)>, + } + + #[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)] + pub struct TrancheInvestorInfo { + tranche_id: TrancheId, + permissioned_till: Seconds, + } + + impl> + TrancheInvestors + { + fn migrate(self) -> super::TrancheInvestors { + super::TrancheInvestors:: { + info: BoundedVec::truncate_from( + self.info + .into_iter() + .map(|info| super::TrancheInvestorInfo { + tranche_id: info.tranche_id, + permissioned_till: info.permissioned_till, + is_frozen: false, + }) + .collect(), + ), + _phantom: self._phantom, + } + } + } + + impl> + PermissionRoles + { + pub fn migrate(self) -> super::PermissionRoles { + super::PermissionRoles { + pool_admin: self.pool_admin, + currency_admin: self.currency_admin, + permissioned_asset_holder: self.permissioned_asset_holder, + tranche_investor: self.tranche_investor.migrate(), + } + } + } +} diff --git a/pallets/investments/src/lib.rs b/pallets/investments/src/lib.rs index ef9ecd6967..f12a427d66 100644 --- a/pallets/investments/src/lib.rs +++ b/pallets/investments/src/lib.rs @@ -627,6 +627,13 @@ impl Pallet { who: T::AccountId, investment_id: T::InvestmentId, ) -> DispatchResultWithPostInfo { + // Frozen investors must not be able to collect tranche tokens + T::PreConditions::check(OrderType::Investment { + who: who.clone(), + investment_id, + amount: Default::default(), + })?; + let _ = T::Accountant::info(investment_id).map_err(|_| Error::::UnknownInvestment)?; let (collected_investment, post_dispatch_info) = InvestOrders::::try_mutate( &who, diff --git a/pallets/investments/src/mock.rs b/pallets/investments/src/mock.rs index 57b7a287e1..d88907e971 100644 --- a/pallets/investments/src/mock.rs +++ b/pallets/investments/src/mock.rs @@ -46,6 +46,7 @@ use sp_std::{ }; pub use crate as pallet_investments; +use crate::OrderType; pub type AccountId = u64; @@ -134,18 +135,31 @@ impl pallet_investments::Config for Runtime { type CollectedRedemptionHook = NoopCollectHook; type InvestmentId = InvestmentId; type MaxOutstandingCollects = MaxOutstandingCollect; - type PreConditions = Always; + type PreConditions = AlwaysWithOneException; type RuntimeEvent = RuntimeEvent; type Tokens = OrmlTokens; type WeightInfo = (); } -pub struct Always; -impl PreConditions for Always { +pub(crate) const ERR_PRE_CONDITION: DispatchError = + DispatchError::Other("PreCondition mock fails on u64::MAX account on purpose"); + +pub struct AlwaysWithOneException; +impl PreConditions for AlwaysWithOneException +where + T: Into> + Clone, +{ type Result = DispatchResult; - fn check(_: T) -> Self::Result { - Ok(()) + fn check(order: T) -> Self::Result { + match order.clone().into() { + OrderType::Investment { who, .. } | OrderType::Redemption { who, .. } + if who == NOT_INVESTOR => + { + Err(ERR_PRE_CONDITION) + } + _ => Ok(()), + } } } @@ -188,6 +202,9 @@ pub const UNKNOWN_INVESTMENT: InvestmentId = (1, TRANCHE_ID_0); /// The currency id for the AUSD token pub const AUSD_CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(1); +/// The account for which the pre-condition mock fails +pub(crate) const NOT_INVESTOR: u64 = u64::MAX; + impl TestExternalitiesBuilder { // Build a genesis storage key/value store pub(crate) fn build() -> TestExternalities { diff --git a/pallets/investments/src/tests.rs b/pallets/investments/src/tests.rs index 771836b257..f322aec830 100644 --- a/pallets/investments/src/tests.rs +++ b/pallets/investments/src/tests.rs @@ -2955,3 +2955,24 @@ fn collecting_over_max_works() { } }) } + +#[test] +fn collecting_investment_without_preconditions_fails() { + TestExternalitiesBuilder::build().execute_with(|| { + assert_noop!( + Investments::collect_investments(RuntimeOrigin::signed(NOT_INVESTOR), INVESTMENT_0_0), + ERR_PRE_CONDITION + ); + }) +} + +/// NOTE: Collecting redemptions does not check pre-conditions +#[test] +fn collecting_redemption_without_preconditions_success() { + TestExternalitiesBuilder::build().execute_with(|| { + assert_ok!(Investments::collect_redemptions( + RuntimeOrigin::signed(NOT_INVESTOR), + INVESTMENT_0_0 + ),); + }) +} diff --git a/pallets/liquidity-pools/src/defensive_weights.rs b/pallets/liquidity-pools/src/defensive_weights.rs index 78501303a9..6c49292d45 100644 --- a/pallets/liquidity-pools/src/defensive_weights.rs +++ b/pallets/liquidity-pools/src/defensive_weights.rs @@ -19,9 +19,14 @@ pub trait WeightInfo { fn update_member() -> Weight; fn transfer() -> Weight; fn set_domain_router() -> Weight; + fn add_currency() -> Weight; + fn allow_investment_currency() -> Weight; + fn disallow_investment_currency() -> Weight; fn schedule_upgrade() -> Weight; fn cancel_upgrade() -> Weight; fn update_tranche_token_metadata() -> Weight; + fn freeze_investor() -> Weight; + fn unfreeze_investor() -> Weight; } // NOTE: We use temporary weights here. `execute_epoch` is by far our heaviest @@ -29,112 +34,79 @@ pub trait WeightInfo { // should be enough. const N: u64 = 4; +/// NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will +/// be replaced with real benchmark soon. + +fn default_defensive_weight() -> Weight { + Weight::from_parts(124_979_771, 19974) + .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) + .saturating_add(RocksDbWeight::get().reads(8)) + .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) + .saturating_add(RocksDbWeight::get().writes(8)) + .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) + .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) +} + impl WeightInfo for () { fn set_domain_router() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() } fn add_pool() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() } fn add_tranche() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() } fn update_token_price() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() } fn update_member() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() } fn transfer() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() } fn schedule_upgrade() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() } fn cancel_upgrade() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() } fn update_tranche_token_metadata() -> Weight { - // NOTE: Defensive hardcoded weight taken from pool_system::execute_epoch. Will - // be replaced with real benchmark soon. - Weight::from_parts(124_979_771, 19974) - .saturating_add(Weight::from_parts(58_136_652, 0).saturating_mul(N)) - .saturating_add(RocksDbWeight::get().reads(8)) - .saturating_add(RocksDbWeight::get().reads((7_u64).saturating_mul(N))) - .saturating_add(RocksDbWeight::get().writes(8)) - .saturating_add(RocksDbWeight::get().writes((6_u64).saturating_mul(N))) - .saturating_add(Weight::from_parts(0, 17774).saturating_mul(N)) + default_defensive_weight() + } + + fn freeze_investor() -> Weight { + default_defensive_weight() + } + + fn unfreeze_investor() -> Weight { + default_defensive_weight() + } + + fn add_currency() -> Weight { + // Reads: 2x AssetRegistry + // Writes: MessageNonceStore, MessageQueue + RocksDbWeight::get().reads_writes(2, 2) + } + + fn allow_investment_currency() -> Weight { + // Reads: 2x AssetRegistry + // Writes: MessageNonceStore, MessageQueue + RocksDbWeight::get().reads_writes(2, 2) + } + + fn disallow_investment_currency() -> Weight { + // Reads: 2x AssetRegistry + // Writes: MessageNonceStore, MessageQueue + RocksDbWeight::get().reads_writes(2, 2) } } diff --git a/pallets/liquidity-pools/src/inbound.rs b/pallets/liquidity-pools/src/inbound.rs index 2b7d5b442a..b2c904ca25 100644 --- a/pallets/liquidity-pools/src/inbound.rs +++ b/pallets/liquidity-pools/src/inbound.rs @@ -11,14 +11,8 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -use cfg_traits::{ - investments::ForeignInvestment, liquidity_pools::OutboundMessageHandler, Permissions, - TimeAsSecs, -}; -use cfg_types::{ - domain_address::{Domain, DomainAddress}, - permissions::{PermissionScope, PoolRole, Role}, -}; +use cfg_traits::{investments::ForeignInvestment, liquidity_pools::OutboundMessageHandler}; +use cfg_types::domain_address::{Domain, DomainAddress}; use frame_support::{ ensure, traits::{fungibles::Mutate, tokens::Preservation, OriginTrait}, @@ -69,14 +63,11 @@ where let local_representation_of_receiver = T::DomainAddressToAccountId::convert(receiver.clone()); - ensure!( - T::Permission::has( - PermissionScope::Pool(pool_id), - local_representation_of_receiver.clone(), - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::Time::now())), - ), - Error::::UnauthorizedTransfer - ); + Self::validate_investor_can_transfer( + local_representation_of_receiver.clone(), + pool_id, + tranche_id, + )?; let invest_id = Self::derive_invest_id(pool_id, tranche_id)?; diff --git a/pallets/liquidity-pools/src/lib.rs b/pallets/liquidity-pools/src/lib.rs index 3f047f923a..07bc8dd632 100644 --- a/pallets/liquidity-pools/src/lib.rs +++ b/pallets/liquidity-pools/src/lib.rs @@ -335,6 +335,12 @@ pub mod pallet { /// The account derived from the [Domain] and [DomainAddress] has not /// been whitelisted as a TrancheInvestor. InvestorDomainAddressNotAMember, + /// The account derived from the [Domain] and [DomainAddress] is frozen + /// and cannot transfer tranche tokens therefore. + InvestorDomainAddressFrozen, + /// The account derived from the [Domain] and [DomainAddress] is not + /// frozen and cannot be unfrozen therefore. + InvestorDomainAddressNotFrozen, /// Only the PoolAdmin can execute a given operation. NotPoolAdmin, /// The domain hook address could not be found. @@ -349,7 +355,9 @@ pub mod pallet { where ::AccountId: From<[u8; 32]> + Into<[u8; 32]>, { - /// Add a pool to a given domain + /// Add a pool to a given domain. + /// + /// Origin: Pool admin #[pallet::weight(T::WeightInfo::add_pool())] #[pallet::call_index(2)] pub fn add_pool( @@ -383,7 +391,9 @@ pub mod pallet { Ok(()) } - /// Add a tranche to a given domain + /// Add a tranche to a given domain. + /// + /// Origin: Pool admin #[pallet::weight(T::WeightInfo::add_tranche())] #[pallet::call_index(3)] pub fn add_tranche( @@ -488,7 +498,8 @@ pub mod pallet { Ok(()) } - /// Update a member + /// Inform the recipient domain about a new or changed investor + /// validity. #[pallet::weight(T::WeightInfo::update_member())] #[pallet::call_index(5)] pub fn update_member( @@ -558,18 +569,14 @@ pub mod pallet { let who = ensure_signed(origin.clone())?; ensure!(!amount.is_zero(), Error::::InvalidTransferAmount); - ensure!( - T::Permission::has( - PermissionScope::Pool(pool_id), - T::DomainAddressToAccountId::convert(domain_address.clone()), - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::Time::now())) - ), - Error::::UnauthorizedTransfer - ); + Self::validate_investor_can_transfer( + T::DomainAddressToAccountId::convert(domain_address.clone()), + pool_id, + tranche_id, + )?; // Ensure pool and tranche exist and derive invest id let invest_id = Self::derive_invest_id(pool_id, tranche_id)?; - T::PreTransferFilter::check((who.clone(), domain_address.clone(), invest_id.into()))?; // Transfer to the domain account for bookkeeping @@ -670,7 +677,9 @@ pub mod pallet { /// Add a currency to the set of known currencies on the domain derived /// from the given currency. - #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())] + /// + /// Origin: Anyone because transmitted data is queried from chain. + #[pallet::weight(T::WeightInfo::add_currency())] #[pallet::call_index(8)] pub fn add_currency(origin: OriginFor, currency_id: T::CurrencyId) -> DispatchResult { let who = ensure_signed(origin)?; @@ -693,16 +702,17 @@ pub mod pallet { /// Allow a currency to be used as a pool currency and to invest in a /// pool on the domain derived from the given currency. + /// + /// Origin: Pool admin for now + /// NOTE: In the future should be permissioned by new trait, see spec + /// #[pallet::call_index(9)] - #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())] + #[pallet::weight(T::WeightInfo::allow_investment_currency())] pub fn allow_investment_currency( origin: OriginFor, pool_id: T::PoolId, currency_id: T::CurrencyId, ) -> DispatchResult { - // TODO(future): In the future, should be permissioned by trait which - // does not exist yet. - // See spec: https://centrifuge.hackmd.io/SERpps-URlG4hkOyyS94-w?view#fn-add_pool_currency let who = ensure_signed(origin)?; ensure!( @@ -728,8 +738,11 @@ pub mod pallet { Ok(()) } - /// Schedule an upgrade of an EVM-based liquidity pool contract instance - #[pallet::weight(::WeightInfo::schedule_upgrade())] + /// Schedule an upgrade of an EVM-based liquidity pool contract + /// instance. + /// + /// Origin: root + #[pallet::weight(T::WeightInfo::schedule_upgrade())] #[pallet::call_index(10)] pub fn schedule_upgrade( origin: OriginFor, @@ -746,6 +759,8 @@ pub mod pallet { } /// Schedule an upgrade of an EVM-based liquidity pool contract instance + /// + /// Origin: root #[pallet::weight(T::WeightInfo::cancel_upgrade())] #[pallet::call_index(11)] pub fn cancel_upgrade( @@ -766,7 +781,7 @@ pub mod pallet { /// /// NOTE: Pulls the metadata from the `AssetRegistry` and thus requires /// the pool admin to have updated the tranche tokens metadata there - /// beforehand. + /// beforehand. Therefore, no restrictions on calling origin. #[pallet::weight(T::WeightInfo::update_tranche_token_metadata())] #[pallet::call_index(12)] pub fn update_tranche_token_metadata( @@ -797,8 +812,10 @@ pub mod pallet { /// Disallow a currency to be used as a pool currency and to invest in a /// pool on the domain derived from the given currency. + /// + /// Origin: Pool admin #[pallet::call_index(13)] - #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())] + #[pallet::weight(T::WeightInfo::disallow_investment_currency())] pub fn disallow_investment_currency( origin: OriginFor, pool_id: T::PoolId, @@ -828,6 +845,122 @@ pub mod pallet { Ok(()) } + + /// Block a remote investor from performing investment tasks until lock + /// is removed. + /// + /// NOTE: Assumes the remote investor's permissions have been updated to + /// reflect frozenness beforehand. + /// + /// Origin: Pool admin + #[pallet::call_index(14)] + #[pallet::weight(T::WeightInfo::freeze_investor())] + pub fn freeze_investor( + origin: OriginFor, + pool_id: T::PoolId, + tranche_id: T::TrancheId, + domain_address: DomainAddress, + ) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + let local_address = T::DomainAddressToAccountId::convert(domain_address.clone()); + + ensure!( + T::PoolInspect::pool_exists(pool_id), + Error::::PoolNotFound + ); + ensure!( + T::PoolInspect::tranche_exists(pool_id, tranche_id), + Error::::TrancheNotFound + ); + + ensure!( + T::Permission::has( + PermissionScope::Pool(pool_id), + who.clone(), + Role::PoolRole(PoolRole::PoolAdmin) + ), + Error::::NotPoolAdmin + ); + Self::validate_investor_status( + local_address.clone(), + pool_id, + tranche_id, + T::Time::now(), + true, + )?; + + T::OutboundMessageHandler::handle( + who, + domain_address.domain(), + Message::UpdateRestriction { + pool_id: pool_id.into(), + tranche_id: tranche_id.into(), + update: UpdateRestrictionMessage::Freeze { + address: domain_address.address(), + }, + }, + )?; + + Ok(()) + } + + /// Unblock a previously locked remote investor from performing + /// investment tasks. + /// + /// NOTE: Assumes the remote investor's permissions have been updated to + /// reflect an unfrozen state beforehand. + /// + /// Origin: Pool admin + #[pallet::call_index(15)] + #[pallet::weight(T::WeightInfo::unfreeze_investor())] + pub fn unfreeze_investor( + origin: OriginFor, + pool_id: T::PoolId, + tranche_id: T::TrancheId, + domain_address: DomainAddress, + ) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + let local_address = T::DomainAddressToAccountId::convert(domain_address.clone()); + + ensure!( + T::PoolInspect::pool_exists(pool_id), + Error::::PoolNotFound + ); + ensure!( + T::PoolInspect::tranche_exists(pool_id, tranche_id), + Error::::TrancheNotFound + ); + + ensure!( + T::Permission::has( + PermissionScope::Pool(pool_id), + who.clone(), + Role::PoolRole(PoolRole::PoolAdmin) + ), + Error::::NotPoolAdmin + ); + Self::validate_investor_status( + local_address.clone(), + pool_id, + tranche_id, + T::Time::now(), + false, + )?; + + T::OutboundMessageHandler::handle( + who, + domain_address.domain(), + Message::UpdateRestriction { + pool_id: pool_id.into(), + tranche_id: tranche_id.into(), + update: UpdateRestrictionMessage::Unfreeze { + address: domain_address.address(), + }, + }, + )?; + + Ok(()) + } } impl Pallet { @@ -945,6 +1078,64 @@ pub mod pallet { let domain_address = T::DomainAccountToDomainAddress::convert(domain_account); T::DomainAddressToAccountId::convert(domain_address) } + + /// Checks whether the given address has investor permissions with at + /// least the given validity timestamp. Moreover, checks whether the + /// investor is frozen or not. + pub fn validate_investor_status( + investor: T::AccountId, + pool_id: T::PoolId, + tranche_id: T::TrancheId, + valid_until: Seconds, + is_frozen: bool, + ) -> DispatchResult { + ensure!( + T::Permission::has( + PermissionScope::Pool(pool_id), + investor.clone(), + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, valid_until)) + ), + Error::::InvestorDomainAddressNotAMember + ); + ensure!( + is_frozen + == T::Permission::has( + PermissionScope::Pool(pool_id), + investor, + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) + ), + Error::::InvestorDomainAddressFrozen + ); + + Ok(()) + } + + /// Checks whether the given address has investor permissions at least + /// to the current timestamp and whether it is not frozen. + pub fn validate_investor_can_transfer( + investor: T::AccountId, + pool_id: T::PoolId, + tranche_id: T::TrancheId, + ) -> DispatchResult { + ensure!( + T::Permission::has( + PermissionScope::Pool(pool_id), + investor.clone(), + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::Time::now())) + ), + Error::::UnauthorizedTransfer + ); + ensure!( + !T::Permission::has( + PermissionScope::Pool(pool_id), + investor, + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) + ), + Error::::InvestorDomainAddressFrozen + ); + + Ok(()) + } } impl InboundMessageHandler for Pallet diff --git a/pallets/liquidity-pools/src/mock.rs b/pallets/liquidity-pools/src/mock.rs index 453a23aabb..f76c5e26ab 100644 --- a/pallets/liquidity-pools/src/mock.rs +++ b/pallets/liquidity-pools/src/mock.rs @@ -27,6 +27,16 @@ pub const ALICE_32: [u8; 32] = [2; 32]; pub const ALICE: AccountId = AccountId::new(ALICE_32); pub const ALICE_ETH: [u8; 20] = [2; 20]; pub const ALICE_EVM_DOMAIN_ADDRESS: DomainAddress = DomainAddress::EVM(42, ALICE_ETH); +// TODO(future): Can be removed after domain conversion refactor +pub const ALICE_EVM_LOCAL_ACCOUNT: AccountId = { + let mut arr = [0u8; 32]; + let mut i = 0; + while i < 20 { + arr[i] = ALICE_ETH[i]; + i += 1; + } + AccountId::new(arr) +}; pub const CENTRIFUGE_DOMAIN_ADDRESS: DomainAddress = DomainAddress::Centrifuge(ALICE_32); pub const CONTRACT_ACCOUNT: [u8; 20] = [1; 20]; pub const CONTRACT_ACCOUNT_ID: AccountId = AccountId::new([1; 32]); diff --git a/pallets/liquidity-pools/src/tests.rs b/pallets/liquidity-pools/src/tests.rs index 47596ff243..39d7d3c2ae 100644 --- a/pallets/liquidity-pools/src/tests.rs +++ b/pallets/liquidity-pools/src/tests.rs @@ -213,13 +213,21 @@ mod transfer_tranche_tokens { DomainAddressToAccountId::mock_convert(|_| CONTRACT_ACCOUNT_ID); Time::mock_now(|| NOW); Permissions::mock_has(move |scope, who, role| { - assert_eq!(who, CONTRACT_ACCOUNT_ID); assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); - assert!(matches!( - role, - Role::PoolRole(PoolRole::TrancheInvestor(TRANCHE_ID, NOW_SECS)) - )); - true + assert_eq!(who, CONTRACT_ACCOUNT_ID); + match role { + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, validity)) => { + assert_eq!(tranche_id, TRANCHE_ID); + assert_eq!(validity, NOW_SECS); + true + } + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) => { + assert_eq!(tranche_id, TRANCHE_ID); + // Default mock has unfrozen investor + false + } + _ => false, + } }); Pools::mock_pool_exists(|_| true); Pools::mock_tranche_exists(|_, _| true); @@ -282,7 +290,7 @@ mod transfer_tranche_tokens { } #[test] - fn with_wrong_permissions() { + fn with_missing_investor_permissions() { System::externalities().execute_with(|| { config_mocks(); Permissions::mock_has(|_, _, _| false); @@ -300,6 +308,40 @@ mod transfer_tranche_tokens { }) } + #[test] + fn with_frozen_investor_permissions() { + System::externalities().execute_with(|| { + config_mocks(); + Permissions::mock_has(move |scope, who, role| { + assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); + assert_eq!(who, CONTRACT_ACCOUNT_ID); + match role { + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, validity)) => { + assert_eq!(tranche_id, TRANCHE_ID); + assert_eq!(validity, NOW_SECS); + true + } + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) => { + assert_eq!(tranche_id, TRANCHE_ID); + true + } + _ => false, + } + }); + + assert_noop!( + LiquidityPools::transfer_tranche_tokens( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + EVM_DOMAIN_ADDRESS, + AMOUNT + ), + Error::::InvestorDomainAddressFrozen, + ); + }) + } + #[test] fn with_wrong_pool() { System::externalities().execute_with(|| { @@ -1380,3 +1422,429 @@ mod cancel_upgrade { } } } + +mod freeze { + use sp_runtime::DispatchError; + + use super::*; + use crate::message::UpdateRestrictionMessage; + + fn config_mocks(receiver: DomainAddress) { + DomainAccountToDomainAddress::mock_convert(move |_| receiver.clone()); + DomainAddressToAccountId::mock_convert(move |_| ALICE_EVM_LOCAL_ACCOUNT); + Time::mock_now(|| NOW); + Permissions::mock_has(move |scope, who, role| { + assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); + match role { + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, validity)) => { + assert_eq!(who, ALICE_EVM_LOCAL_ACCOUNT); + assert_eq!(tranche_id, TRANCHE_ID); + assert_eq!(validity, NOW_SECS); + true + } + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) => { + assert_eq!(who, ALICE_EVM_LOCAL_ACCOUNT); + assert_eq!(tranche_id, TRANCHE_ID); + // Default mock has frozen investor + true + } + Role::PoolRole(PoolRole::PoolAdmin) => { + assert_eq!(who, ALICE); + true + } + _ => false, + } + }); + Pools::mock_pool_exists(|_| true); + Pools::mock_tranche_exists(|_, _| true); + Gateway::mock_handle(|sender, destination, msg| { + assert_eq!(sender, ALICE); + assert_eq!(destination, ALICE_EVM_DOMAIN_ADDRESS.domain()); + assert_eq!( + msg, + Message::UpdateRestriction { + pool_id: POOL_ID, + tranche_id: TRANCHE_ID, + update: UpdateRestrictionMessage::Freeze { + address: ALICE_EVM_DOMAIN_ADDRESS.address().into() + } + } + ); + Ok(()) + }); + } + + #[test] + fn success() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + + assert_ok!(LiquidityPools::freeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + )); + }); + } + + mod erroring_out { + use super::*; + + #[test] + fn with_bad_origin_unsigned_none() { + System::externalities().execute_with(|| { + assert_noop!( + LiquidityPools::freeze_investor( + RuntimeOrigin::none(), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + DispatchError::BadOrigin + ); + }); + } + #[test] + fn with_bad_origin_unsigned_root() { + System::externalities().execute_with(|| { + assert_noop!( + LiquidityPools::freeze_investor( + RuntimeOrigin::root(), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + DispatchError::BadOrigin + ); + }); + } + + #[test] + fn with_pool_dne() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Pools::mock_pool_exists(|_| false); + + assert_noop!( + LiquidityPools::freeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::PoolNotFound + ); + }); + } + + #[test] + fn with_tranche_dne() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Pools::mock_tranche_exists(|_, _| false); + + assert_noop!( + LiquidityPools::freeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::TrancheNotFound + ); + }); + } + + #[test] + fn with_origin_not_admin() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Permissions::mock_has(|_, _, _| false); + + assert_noop!( + LiquidityPools::freeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::NotPoolAdmin + ); + }); + } + + #[test] + fn with_investor_not_member() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Permissions::mock_has(move |scope, who, role| { + assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); + match role { + Role::PoolRole(PoolRole::PoolAdmin) => { + assert_eq!(who, ALICE); + true + } + _ => false, + } + }); + + assert_noop!( + LiquidityPools::freeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::InvestorDomainAddressNotAMember + ); + }); + } + + #[test] + fn with_investor_frozen() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Permissions::mock_has(move |scope, who, role| { + assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); + match role { + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, validity)) => { + assert_eq!(who, ALICE_EVM_LOCAL_ACCOUNT); + assert_eq!(tranche_id, TRANCHE_ID); + assert_eq!(validity, NOW_SECS); + true + } + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) => { + assert_eq!(who, ALICE_EVM_LOCAL_ACCOUNT); + assert_eq!(tranche_id, TRANCHE_ID); + false + } + Role::PoolRole(PoolRole::PoolAdmin) => { + assert_eq!(who, ALICE); + true + } + _ => false, + } + }); + + assert_noop!( + LiquidityPools::freeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::InvestorDomainAddressFrozen + ); + }); + } + } +} + +mod unfreeze { + use sp_runtime::DispatchError; + + use super::*; + use crate::message::UpdateRestrictionMessage; + + fn config_mocks(receiver: DomainAddress) { + DomainAccountToDomainAddress::mock_convert(move |_| receiver.clone()); + DomainAddressToAccountId::mock_convert(move |_| ALICE_EVM_LOCAL_ACCOUNT); + Time::mock_now(|| NOW); + Permissions::mock_has(move |scope, who, role| { + assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); + match role { + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, validity)) => { + assert_eq!(who, ALICE_EVM_LOCAL_ACCOUNT); + assert_eq!(tranche_id, TRANCHE_ID); + assert_eq!(validity, NOW_SECS); + true + } + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) => { + assert_eq!(who, ALICE_EVM_LOCAL_ACCOUNT); + assert_eq!(tranche_id, TRANCHE_ID); + // Default mock has unfrozen investor + false + } + Role::PoolRole(PoolRole::PoolAdmin) => { + assert_eq!(who, ALICE); + true + } + _ => false, + } + }); + Pools::mock_pool_exists(|_| true); + Pools::mock_tranche_exists(|_, _| true); + Gateway::mock_handle(|sender, destination, msg| { + assert_eq!(sender, ALICE); + assert_eq!(destination, ALICE_EVM_DOMAIN_ADDRESS.domain()); + assert_eq!( + msg, + Message::UpdateRestriction { + pool_id: POOL_ID, + tranche_id: TRANCHE_ID, + update: UpdateRestrictionMessage::Unfreeze { + address: ALICE_EVM_DOMAIN_ADDRESS.address().into() + } + } + ); + Ok(()) + }); + } + + #[test] + fn success() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + + assert_ok!(LiquidityPools::unfreeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + )); + }); + } + + mod erroring_out { + use super::*; + + #[test] + fn with_bad_origin_unsigned_none() { + System::externalities().execute_with(|| { + assert_noop!( + LiquidityPools::unfreeze_investor( + RuntimeOrigin::none(), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + DispatchError::BadOrigin + ); + }); + } + #[test] + fn with_bad_origin_unsigned_root() { + System::externalities().execute_with(|| { + assert_noop!( + LiquidityPools::unfreeze_investor( + RuntimeOrigin::root(), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + DispatchError::BadOrigin + ); + }); + } + + #[test] + fn with_pool_dne() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Pools::mock_pool_exists(|_| false); + + assert_noop!( + LiquidityPools::unfreeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::PoolNotFound + ); + }); + } + + #[test] + fn with_tranche_dne() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Pools::mock_tranche_exists(|_, _| false); + + assert_noop!( + LiquidityPools::unfreeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::TrancheNotFound + ); + }); + } + + #[test] + fn with_origin_not_admin() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Permissions::mock_has(|_, _, _| false); + + assert_noop!( + LiquidityPools::unfreeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::NotPoolAdmin + ); + }); + } + + #[test] + fn with_investor_not_member() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Permissions::mock_has(move |scope, who, role| { + assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); + match role { + Role::PoolRole(PoolRole::PoolAdmin) => { + assert_eq!(who, ALICE); + true + } + _ => false, + } + }); + + assert_noop!( + LiquidityPools::unfreeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::InvestorDomainAddressNotAMember + ); + }); + } + + #[test] + fn with_investor_unfrozen() { + System::externalities().execute_with(|| { + config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + Permissions::mock_has(move |scope, who, role| { + assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); + match role { + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) => { + assert_eq!(who, ALICE_EVM_LOCAL_ACCOUNT); + assert_eq!(tranche_id, TRANCHE_ID); + true + } + _ => true, + } + }); + + assert_noop!( + LiquidityPools::unfreeze_investor( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + ALICE_EVM_DOMAIN_ADDRESS + ), + Error::::InvestorDomainAddressFrozen + ); + }); + } + } +} diff --git a/pallets/liquidity-pools/src/tests/inbound.rs b/pallets/liquidity-pools/src/tests/inbound.rs index 886ceac3c2..bc0dd61cbd 100644 --- a/pallets/liquidity-pools/src/tests/inbound.rs +++ b/pallets/liquidity-pools/src/tests/inbound.rs @@ -92,13 +92,21 @@ mod handle_tranche_tokens_transfer { DomainAddressToAccountId::mock_convert(move |_| ALICE); Time::mock_now(|| NOW); Permissions::mock_has(move |scope, who, role| { - assert_eq!(who, ALICE); assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); - assert!(matches!( - role, - Role::PoolRole(PoolRole::TrancheInvestor(TRANCHE_ID, NOW_SECS)) - )); - true + assert_eq!(who, ALICE); + match role { + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, validity)) => { + assert_eq!(tranche_id, TRANCHE_ID); + assert_eq!(validity, NOW_SECS); + true + } + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)) => { + assert_eq!(tranche_id, TRANCHE_ID); + // Default mock has unfrozen investor + false + } + _ => false, + } }); Pools::mock_pool_exists(|_| true); Pools::mock_tranche_exists(|_, _| true); @@ -206,7 +214,7 @@ mod handle_tranche_tokens_transfer { } #[test] - fn with_wrong_permissions() { + fn without_investor_permissions() { System::externalities().execute_with(|| { config_mocks(CENTRIFUGE_DOMAIN_ADDRESS); Permissions::mock_has(|_, _, _| false); @@ -227,6 +235,28 @@ mod handle_tranche_tokens_transfer { }) } + #[test] + fn inbound_with_frozen_investor_permissions() { + System::externalities().execute_with(|| { + config_mocks(CENTRIFUGE_DOMAIN_ADDRESS); + Permissions::mock_has(|_, _, _| true); + + assert_noop!( + LiquidityPools::handle( + EVM_DOMAIN_ADDRESS, + Message::TransferTrancheTokens { + pool_id: POOL_ID, + tranche_id: TRANCHE_ID, + domain: CENTRIFUGE_DOMAIN_ADDRESS.domain().into(), + receiver: ALICE.into(), + amount: AMOUNT, + } + ), + Error::::InvestorDomainAddressFrozen, + ); + }) + } + #[test] fn with_wrong_pool() { System::externalities().execute_with(|| { diff --git a/pallets/permissions/src/lib.rs b/pallets/permissions/src/lib.rs index 4a31981702..9f9b67a17d 100644 --- a/pallets/permissions/src/lib.rs +++ b/pallets/permissions/src/lib.rs @@ -71,7 +71,10 @@ pub mod pallet { type WeightInfo: WeightInfo; } + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); #[pallet::storage] diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index 2434b91e3d..fe0d5dd8f8 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -29,10 +29,7 @@ use cfg_primitives::{ }, LPGatewayQueueMessageNonce, }; -use cfg_traits::{ - investments::OrderManager, Millis, Permissions as PermissionsT, PoolUpdateGuard, PreConditions, - Seconds, -}; +use cfg_traits::{investments::OrderManager, Millis, PoolUpdateGuard, Seconds}; use cfg_types::{ fee_keys::{Fee, FeeKey}, fixed_point::{Quantity, Rate, Ratio}, @@ -80,7 +77,6 @@ use pallet_evm::{ Account as EVMAccount, EnsureAddressNever, EnsureAddressRoot, FeeCalculator, GasWeightMapping, Runner, }; -use pallet_investments::OrderType; use pallet_liquidity_pools_gateway::message::GatewayMessage; pub use pallet_loans::entities::{input::PriceCollectionInput, loans::ActiveLoanInfo}; use pallet_loans::types::cashflow::CashflowPayment; @@ -117,7 +113,7 @@ use runtime_common::{ }, PoolAdmin, Treasurer, }, - permissions::PoolAdminCheck, + permissions::{IsUnfrozenTrancheInvestor, PoolAdminCheck}, remarks::Remark, rewards::SingleCurrencyMovement, transfer_filter::{PreLpTransfer, PreNativeTransfer}, @@ -135,7 +131,7 @@ use sp_runtime::{ Dispatchable, IdentityLookup, PostDispatchInfoOf, UniqueSaturatedInto, Verify, Zero, }, transaction_validity::{TransactionSource, TransactionValidity, TransactionValidityError}, - ApplyExtrinsicResult, DispatchError, DispatchResult, FixedI128, Perbill, Permill, Perquintill, + ApplyExtrinsicResult, DispatchError, FixedI128, Perbill, Permill, Perquintill, }; use sp_staking::currency_to_vote::U128CurrencyToVote; use sp_std::{marker::PhantomData, prelude::*, vec::Vec}; @@ -1666,57 +1662,12 @@ impl pallet_investments::Config for Runtime { type CollectedRedemptionHook = pallet_foreign_investments::CollectedRedemptionHook; type InvestmentId = InvestmentId; type MaxOutstandingCollects = MaxOutstandingCollects; - type PreConditions = IsTrancheInvestor; + type PreConditions = IsUnfrozenTrancheInvestor; type RuntimeEvent = RuntimeEvent; type Tokens = Tokens; type WeightInfo = weights::pallet_investments::WeightInfo; } -/// Checks whether the given `who` has the role -/// of a `TrancheInvestor` for the given pool. -pub struct IsTrancheInvestor(PhantomData<(P, T)>); -impl< - P: PermissionsT, Role = Role>, - T: UnixTime, - > PreConditions> for IsTrancheInvestor -{ - type Result = DispatchResult; - - fn check(order: OrderType) -> Self::Result { - let is_tranche_investor = match order { - OrderType::Investment { - who, - investment_id: (pool_id, tranche_id), - .. - } => P::has( - PermissionScope::Pool(pool_id), - who, - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::now().as_secs())), - ), - OrderType::Redemption { - who, - investment_id: (pool_id, tranche_id), - .. - } => P::has( - PermissionScope::Pool(pool_id), - who, - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::now().as_secs())), - ), - }; - - if is_tranche_investor || cfg!(feature = "runtime-benchmarks") { - Ok(()) - } else { - // TODO: We should adapt the permissions pallets interface to return an error - // instead of a boolean. This makes the redundant "does not have role" error, - // which downstream pallets always need to generate, not needed anymore. - Err(DispatchError::Other( - "Account does not have the TrancheInvestor permission.", - )) - } - } -} - parameter_types! { pub const MaxKeys: u32 = 10; pub const DefaultKeyDeposit: Balance = 100 * AIR; diff --git a/runtime/altair/src/migrations.rs b/runtime/altair/src/migrations.rs index 5152e11c79..c405dda164 100644 --- a/runtime/altair/src/migrations.rs +++ b/runtime/altair/src/migrations.rs @@ -31,4 +31,16 @@ pub type UpgradeAltair1401 = ( 1, 2, >, + // Migrate TrancheInvestor permission role and storage version from v0 to v1 + frame_support::migrations::VersionedMigration< + 0, + 1, + runtime_common::migrations::permissions_v1::Migration< + Runtime, + crate::MinDelay, + crate::MaxTranches, + >, + pallet_permissions::Pallet, + ::DbWeight, + >, ); diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index baa210710e..0d97bad678 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -54,7 +54,7 @@ use frame_support::{ construct_runtime, dispatch::DispatchClass, genesis_builder_helper::{build_config, create_default_config}, - pallet_prelude::{DispatchError, DispatchResult, RuntimeDebug}, + pallet_prelude::{DispatchError, RuntimeDebug}, parameter_types, traits::{ fungible::HoldConsideration, @@ -81,7 +81,6 @@ use pallet_evm::{ Account as EVMAccount, EnsureAddressNever, EnsureAddressRoot, FeeCalculator, GasWeightMapping, Runner, }; -use pallet_investments::OrderType; use pallet_liquidity_pools_gateway::message::GatewayMessage; pub use pallet_loans::entities::{input::PriceCollectionInput, loans::ActiveLoanInfo}; use pallet_loans::types::cashflow::CashflowPayment; @@ -115,7 +114,7 @@ use runtime_common::{ origins::gov::types::{ AllOfCouncil, EnsureRootOr, HalfOfCouncil, ThreeFourthOfCouncil, TwoThirdOfCouncil, }, - permissions::PoolAdminCheck, + permissions::{IsUnfrozenTrancheInvestor, PoolAdminCheck}, rewards::SingleCurrencyMovement, transfer_filter::{PreLpTransfer, PreNativeTransfer}, xcm::AccountIdToLocation, @@ -1623,51 +1622,6 @@ parameter_types! { pub const MaxOutstandingCollects: u32 = 10; } -/// Checks whether the given `who` has the role -/// of a `TrancheInvestor` for the given pool. -pub struct IsTrancheInvestor(PhantomData<(P, T)>); -impl< - P: PermissionsT, Role = Role>, - T: UnixTime, - > PreConditions> for IsTrancheInvestor -{ - type Result = DispatchResult; - - fn check(order: OrderType) -> Self::Result { - let is_tranche_investor = match order { - OrderType::Investment { - who, - investment_id: (pool_id, tranche_id), - .. - } => P::has( - PermissionScope::Pool(pool_id), - who, - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::now().as_secs())), - ), - OrderType::Redemption { - who, - investment_id: (pool_id, tranche_id), - .. - } => P::has( - PermissionScope::Pool(pool_id), - who, - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::now().as_secs())), - ), - }; - - if is_tranche_investor || cfg!(feature = "runtime-benchmarks") { - Ok(()) - } else { - // TODO: We should adapt the permissions pallets interface to return an error - // instead of a boolean. This makes the redundant "does not have role" error, - // which downstream pallets always need to generate, not needed anymore. - Err(DispatchError::Other( - "Account does not have the TrancheInvestor permission.", - )) - } - } -} - impl pallet_investments::Config for Runtime { type Accountant = PoolSystem; type Amount = Balance; @@ -1676,7 +1630,7 @@ impl pallet_investments::Config for Runtime { type CollectedRedemptionHook = pallet_foreign_investments::CollectedRedemptionHook; type InvestmentId = InvestmentId; type MaxOutstandingCollects = MaxOutstandingCollects; - type PreConditions = IsTrancheInvestor; + type PreConditions = IsUnfrozenTrancheInvestor; type RuntimeEvent = RuntimeEvent; type Tokens = Tokens; type WeightInfo = weights::pallet_investments::WeightInfo; diff --git a/runtime/centrifuge/src/migrations.rs b/runtime/centrifuge/src/migrations.rs index d8bd54eab0..d8a378e7e4 100644 --- a/runtime/centrifuge/src/migrations.rs +++ b/runtime/centrifuge/src/migrations.rs @@ -31,4 +31,16 @@ pub type UpgradeCentrifuge1401 = ( 1, 2, >, + // Migrate TrancheInvestor permission role and storage version from v0 to v1 + frame_support::migrations::VersionedMigration< + 0, + 1, + runtime_common::migrations::permissions_v1::Migration< + Runtime, + crate::MinDelay, + crate::MaxTranches, + >, + pallet_permissions::Pallet, + ::DbWeight, + >, ); diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 3ad18a91c4..7acc49d58f 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -40,6 +40,7 @@ pub mod gateway; pub mod migrations; pub mod oracle; pub mod origins; +pub mod permissions; pub mod pool; pub mod remarks; pub mod transfer_filter; @@ -725,44 +726,6 @@ pub mod origin { } } -pub mod permissions { - use cfg_primitives::{AccountId, PoolId}; - use cfg_traits::{Permissions, PreConditions}; - use cfg_types::{ - permissions::{PermissionScope, PoolRole, Role}, - tokens::CurrencyId, - }; - use sp_std::marker::PhantomData; - - /// Check if an account has a pool admin role - pub struct PoolAdminCheck

(PhantomData

); - - impl

PreConditions<(AccountId, PoolId)> for PoolAdminCheck

- where - P: Permissions, Role = Role>, - { - type Result = bool; - - fn check((account_id, pool_id): (AccountId, PoolId)) -> bool { - P::has( - PermissionScope::Pool(pool_id), - account_id, - Role::PoolRole(PoolRole::PoolAdmin), - ) - } - - #[cfg(feature = "runtime-benchmarks")] - fn satisfy((account_id, pool_id): (AccountId, PoolId)) { - P::add( - PermissionScope::Pool(pool_id), - account_id, - Role::PoolRole(PoolRole::PoolAdmin), - ) - .unwrap(); - } - } -} - pub mod rewards { frame_support::parameter_types! { #[derive(scale_info::TypeInfo)] diff --git a/runtime/common/src/migrations/liquidity_pools_gateway.rs b/runtime/common/src/migrations/liquidity_pools_gateway.rs index ab3914c43a..6677c2fc3d 100644 --- a/runtime/common/src/migrations/liquidity_pools_gateway.rs +++ b/runtime/common/src/migrations/liquidity_pools_gateway.rs @@ -73,11 +73,6 @@ where #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { // Extra check to confirm that the storage alias is correct. - assert!( - OutboundMessageNonceStore::::get() > 0, - "{LOG_PREFIX}: OutboundMessageNonce should be > 0" - ); - assert_eq!( OutboundMessageQueue::::iter_keys().count(), 0, diff --git a/runtime/common/src/migrations/mod.rs b/runtime/common/src/migrations/mod.rs index aefb459ac9..8a2dbcc331 100644 --- a/runtime/common/src/migrations/mod.rs +++ b/runtime/common/src/migrations/mod.rs @@ -16,6 +16,7 @@ pub mod foreign_investments_v2; pub mod increase_storage_version; pub mod liquidity_pools_gateway; pub mod nuke; +pub mod permissions_v1; pub mod precompile_account_codes; pub mod restricted_location; pub mod technical_comittee; diff --git a/runtime/common/src/migrations/permissions_v1.rs b/runtime/common/src/migrations/permissions_v1.rs new file mode 100644 index 0000000000..df0ab7da26 --- /dev/null +++ b/runtime/common/src/migrations/permissions_v1.rs @@ -0,0 +1,113 @@ +// Copyright 2024 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{PoolId, TrancheId}; +use cfg_types::{ + permissions::{v0::PermissionRoles as PermissionRolesV0, PermissionRoles, PermissionScope}, + time::TimeProvider, + tokens::CurrencyId, +}; +#[cfg(feature = "try-runtime")] +use frame_support::pallet_prelude::{Decode, Encode}; +use frame_support::{ + storage_alias, + traits::{Get, OnRuntimeUpgrade}, + weights::Weight, + Blake2_128Concat, +}; +use sp_arithmetic::traits::SaturatedConversion; +#[cfg(feature = "try-runtime")] +use sp_std::vec::Vec; + +mod v0 { + use super::*; + + #[storage_alias] + pub type Permission = + StorageDoubleMap< + pallet_permissions::Pallet, + Blake2_128Concat, + ::AccountId, + Blake2_128Concat, + PermissionScope, + PermissionRolesV0>, MinD, TrancheId, MaxT>, + >; +} + +const LOG_PREFIX: &str = "PermssionsV1"; + +pub struct Migration( + sp_std::marker::PhantomData<(T, MinDelay, MaxTranches)>, +); + +impl OnRuntimeUpgrade for Migration +where + T: pallet_permissions::Config< + Storage = PermissionRoles< + TimeProvider>, + MinDelay, + TrancheId, + MaxTranches, + >, + > + frame_system::Config + + pallet_timestamp::Config, + MinDelay: 'static, + MaxTranches: Get + 'static, +{ + fn on_runtime_upgrade() -> Weight { + let writes = v0::Permission::::iter_keys() + .count() + .saturated_into(); + + pallet_permissions::Permission::::translate_values::< + PermissionRolesV0< + TimeProvider>, + MinDelay, + TrancheId, + MaxTranches, + >, + _, + >(|role| Some(role.migrate())); + + log::info!("{LOG_PREFIX}: Migrated {writes} permissions!"); + + T::DbWeight::get().reads_writes(1, writes) + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + let count: u64 = v0::Permission::::iter_keys() + .count() + .saturated_into(); + + log::info!("{LOG_PREFIX}: Pre checks done!"); + + Ok(count.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(pre_state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + let pre_count: u64 = Decode::decode(&mut pre_state.as_slice()) + .expect("pre_upgrade provides a valid state; qed"); + let post_count: u64 = pallet_permissions::Permission::::iter_keys() + .count() + .saturated_into(); + assert_eq!( + pre_count, post_count, + "{LOG_PREFIX}: Mismatching number of permission roles after migration!" + ); + + log::info!("{LOG_PREFIX}: Post checks done!"); + + Ok(()) + } +} diff --git a/runtime/common/src/permissions.rs b/runtime/common/src/permissions.rs new file mode 100644 index 0000000000..6dcd1d64c8 --- /dev/null +++ b/runtime/common/src/permissions.rs @@ -0,0 +1,108 @@ +// Copyright 2024 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +use cfg_primitives::{AccountId, Balance, InvestmentId, PoolId}; +use cfg_traits::{Permissions, PreConditions}; +use cfg_types::{ + permissions::{PermissionScope, PoolRole, Role}, + tokens::CurrencyId, +}; +use frame_support::{dispatch::DispatchResult, traits::UnixTime}; +use pallet_investments::OrderType; +use sp_runtime::DispatchError; +use sp_std::marker::PhantomData; + +/// Check if an account has a pool admin role +pub struct PoolAdminCheck

(PhantomData

); + +impl

PreConditions<(AccountId, PoolId)> for PoolAdminCheck

+where + P: Permissions, Role = Role>, +{ + type Result = bool; + + fn check((account_id, pool_id): (AccountId, PoolId)) -> bool { + P::has( + PermissionScope::Pool(pool_id), + account_id, + Role::PoolRole(PoolRole::PoolAdmin), + ) + } + + #[cfg(feature = "runtime-benchmarks")] + fn satisfy((account_id, pool_id): (AccountId, PoolId)) { + P::add( + PermissionScope::Pool(pool_id), + account_id, + Role::PoolRole(PoolRole::PoolAdmin), + ) + .unwrap(); + } +} + +/// Checks whether the given `who` has the role +/// of a `TrancheInvestor` without having `FrozenInvestor` for the given pool +/// and tranche. +pub struct IsUnfrozenTrancheInvestor(PhantomData<(P, T)>); +impl< + P: Permissions, Role = Role>, + T: UnixTime, + > PreConditions> for IsUnfrozenTrancheInvestor +{ + type Result = DispatchResult; + + fn check(order: OrderType) -> Self::Result { + let is_tranche_investor = match order { + OrderType::Investment { + who, + investment_id: (pool_id, tranche_id), + .. + } => { + P::has( + PermissionScope::Pool(pool_id), + who.clone(), + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::now().as_secs())), + ) && !P::has( + PermissionScope::Pool(pool_id), + who, + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)), + ) + } + OrderType::Redemption { + who, + investment_id: (pool_id, tranche_id), + .. + } => { + P::has( + PermissionScope::Pool(pool_id), + who.clone(), + Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::now().as_secs())), + ) && !P::has( + PermissionScope::Pool(pool_id), + who, + Role::PoolRole(PoolRole::FrozenTrancheInvestor(tranche_id)), + ) + } + }; + + if is_tranche_investor || cfg!(feature = "runtime-benchmarks") { + Ok(()) + } else { + // TODO: We should adapt the permissions pallets interface to return an error + // instead of a boolean. This makes the redundant "does not have role" error, + // which downstream pallets always need to generate, not needed anymore. + Err(DispatchError::Other( + "Account does not have the TrancheInvestor permission.", + )) + } + } +} diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index ede6acf529..67bf57c52c 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -56,7 +56,7 @@ use frame_support::{ construct_runtime, dispatch::DispatchClass, genesis_builder_helper::{build_config, create_default_config}, - pallet_prelude::{DispatchError, DispatchResult, RuntimeDebug}, + pallet_prelude::{DispatchError, RuntimeDebug}, parameter_types, traits::{ fungible::HoldConsideration, @@ -83,7 +83,6 @@ use pallet_evm::{ Account as EVMAccount, EnsureAddressNever, EnsureAddressRoot, FeeCalculator, GasWeightMapping, Runner, }; -use pallet_investments::OrderType; use pallet_liquidity_pools_gateway::message::GatewayMessage; pub use pallet_loans::entities::{input::PriceCollectionInput, loans::ActiveLoanInfo}; use pallet_loans::types::cashflow::CashflowPayment; @@ -122,7 +121,7 @@ use runtime_common::{ }, PoolAdmin, Treasurer, }, - permissions::PoolAdminCheck, + permissions::{IsUnfrozenTrancheInvestor, PoolAdminCheck}, remarks::Remark, rewards::SingleCurrencyMovement, transfer_filter::{PreLpTransfer, PreNativeTransfer}, @@ -1683,57 +1682,12 @@ impl pallet_investments::Config for Runtime { type CollectedRedemptionHook = pallet_foreign_investments::CollectedRedemptionHook; type InvestmentId = InvestmentId; type MaxOutstandingCollects = MaxOutstandingCollects; - type PreConditions = IsTrancheInvestor; + type PreConditions = IsUnfrozenTrancheInvestor; type RuntimeEvent = RuntimeEvent; type Tokens = Tokens; type WeightInfo = (); } -/// Checks whether the given `who` has the role -/// of a `TrancheInvestor` for the given pool. -pub struct IsTrancheInvestor(PhantomData<(P, T)>); -impl< - P: PermissionsT, Role = Role>, - T: UnixTime, - > PreConditions> for IsTrancheInvestor -{ - type Result = DispatchResult; - - fn check(order: OrderType) -> Self::Result { - let is_tranche_investor = match order { - OrderType::Investment { - who, - investment_id: (pool_id, tranche_id), - .. - } => P::has( - PermissionScope::Pool(pool_id), - who, - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::now().as_secs())), - ), - OrderType::Redemption { - who, - investment_id: (pool_id, tranche_id), - .. - } => P::has( - PermissionScope::Pool(pool_id), - who, - Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, T::now().as_secs())), - ), - }; - - if is_tranche_investor || cfg!(feature = "runtime-benchmarks") { - Ok(()) - } else { - // TODO: We should adapt the permissions pallets interface to return an error - // instead of a boolean. This makes the redundant "does not have role" error, - // which downstream pallets always need to generate, not needed anymore. - Err(DispatchError::Other( - "Account does not have the TrancheInvestor permission.", - )) - } - } -} - parameter_types! { pub const RewardsPalletId: PalletId = cfg_types::ids::BLOCK_REWARDS_PALLET_ID; pub const RewardCurrency: CurrencyId = CurrencyId::Native; diff --git a/runtime/development/src/migrations.rs b/runtime/development/src/migrations.rs index f25201cc3f..257c6e8915 100644 --- a/runtime/development/src/migrations.rs +++ b/runtime/development/src/migrations.rs @@ -29,4 +29,16 @@ pub type UpgradeDevelopment1401 = ( 1, 2, >, + // Migrate TrancheInvestor permission role and storage version from v0 to v1 + frame_support::migrations::VersionedMigration< + 0, + 1, + runtime_common::migrations::permissions_v1::Migration< + Runtime, + crate::MinDelay, + crate::MaxTranches, + >, + pallet_permissions::Pallet, + ::DbWeight, + >, ); diff --git a/runtime/integration-tests/src/cases/lp/pool_management.rs b/runtime/integration-tests/src/cases/lp/pool_management.rs index 45fb5f2190..40f005d45a 100644 --- a/runtime/integration-tests/src/cases/lp/pool_management.rs +++ b/runtime/integration-tests/src/cases/lp/pool_management.rs @@ -35,6 +35,7 @@ use crate::{ utils::{ accounts::Keyring, currency::{register_currency, CurrencyInfo}, + pool::{give_role, remove_role}, }, }; @@ -620,3 +621,146 @@ fn update_tranche_token_price() { assert_eq!(price.into_inner(), price_evm); }); } + +#[test_runtimes([centrifuge, development])] +fn freeze_member() { + let mut env = super::setup::(|evm| { + super::setup_currencies(evm); + super::setup_pools(evm); + super::setup_tranches(evm); + super::setup_investment_currencies(evm); + super::setup_deploy_lps(evm); + super::setup_investors(evm); + }); + + env.state(|evm| { + assert!(!Decoder::::decode( + &evm.view( + Keyring::Alice, + names::RESTRICTION_MANAGER, + "isFrozen", + Some(&[ + Token::Address(evm.deployed(names::POOL_A_T_1).address()), + Token::Address(Keyring::TrancheInvestor(2).into()) + ]), + ) + .unwrap() + .value + )); + }); + + env.state_mut(|_| { + give_role::( + AccountConverter::convert_evm_address( + EVM_DOMAIN_CHAIN_ID, + Keyring::TrancheInvestor(2).into(), + ), + POOL_A, + PoolRole::FrozenTrancheInvestor(pool_a_tranche_1_id::()), + ); + assert_ok!(pallet_liquidity_pools::Pallet::::freeze_investor( + Keyring::Admin.as_origin(), + POOL_A, + pool_a_tranche_1_id::(), + DomainAddress::evm(EVM_DOMAIN_CHAIN_ID, Keyring::TrancheInvestor(2).into()), + )); + + utils::process_gateway_message::(utils::verify_gateway_message_success::); + }); + + env.state(|evm| { + assert!(Decoder::::decode( + &evm.view( + Keyring::Alice, + names::RESTRICTION_MANAGER, + "isFrozen", + Some(&[ + Token::Address(evm.deployed(names::POOL_A_T_1).address()), + Token::Address(Keyring::TrancheInvestor(2).into()) + ]), + ) + .unwrap() + .value + )); + }); +} + +#[test_runtimes([centrifuge, development])] +fn unfreeze_member() { + let mut env = super::setup::(|evm| { + super::setup_currencies(evm); + super::setup_pools(evm); + super::setup_tranches(evm); + super::setup_investment_currencies(evm); + super::setup_deploy_lps(evm); + super::setup_investors(evm); + }); + + env.state_mut(|_| { + give_role::( + AccountConverter::convert_evm_address( + EVM_DOMAIN_CHAIN_ID, + Keyring::TrancheInvestor(2).into(), + ), + POOL_A, + PoolRole::FrozenTrancheInvestor(pool_a_tranche_1_id::()), + ); + assert_ok!(pallet_liquidity_pools::Pallet::::freeze_investor( + Keyring::Admin.as_origin(), + POOL_A, + pool_a_tranche_1_id::(), + DomainAddress::evm(EVM_DOMAIN_CHAIN_ID, Keyring::TrancheInvestor(2).into()), + )); + + utils::process_gateway_message::(utils::verify_gateway_message_success::); + }); + env.state(|evm| { + assert!(Decoder::::decode( + &evm.view( + Keyring::Alice, + names::RESTRICTION_MANAGER, + "isFrozen", + Some(&[ + Token::Address(evm.deployed(names::POOL_A_T_1).address()), + Token::Address(Keyring::TrancheInvestor(2).into()) + ]), + ) + .unwrap() + .value + )); + }); + + env.state_mut(|_| { + remove_role::( + AccountConverter::convert_evm_address( + EVM_DOMAIN_CHAIN_ID, + Keyring::TrancheInvestor(2).into(), + ), + POOL_A, + PoolRole::FrozenTrancheInvestor(pool_a_tranche_1_id::()), + ); + assert_ok!(pallet_liquidity_pools::Pallet::::unfreeze_investor( + Keyring::Admin.as_origin(), + POOL_A, + pool_a_tranche_1_id::(), + DomainAddress::evm(EVM_DOMAIN_CHAIN_ID, Keyring::TrancheInvestor(2).into()), + )); + + utils::process_gateway_message::(utils::verify_gateway_message_success::); + }); + env.state(|evm| { + assert!(!Decoder::::decode( + &evm.view( + Keyring::Alice, + names::RESTRICTION_MANAGER, + "isFrozen", + Some(&[ + Token::Address(evm.deployed(names::POOL_A_T_1).address()), + Token::Address(Keyring::TrancheInvestor(2).into()) + ]), + ) + .unwrap() + .value + )); + }); +} diff --git a/runtime/integration-tests/src/cases/lp/setup_lp.rs b/runtime/integration-tests/src/cases/lp/setup_lp.rs index f97463a81b..177fc25329 100644 --- a/runtime/integration-tests/src/cases/lp/setup_lp.rs +++ b/runtime/integration-tests/src/cases/lp/setup_lp.rs @@ -676,6 +676,7 @@ pub fn setup_currencies(evm: &mut impl EvmEnv) { /// Centrifuge Chain as well as EVM. Also mints default balance on both sides. pub fn setup_investors(evm: &mut impl EvmEnv) { default_investors().into_iter().for_each(|investor| { + // POOL A - Tranche 1/1 // Allow investor to locally invest crate::utils::pool::give_role::( investor.into(), @@ -696,6 +697,7 @@ pub fn setup_investors(evm: &mut impl EvmEnv) { SECONDS_PER_YEAR, )); + // POOL B - Tranche 1/2 // Allow investor to locally invest crate::utils::pool::give_role::( investor.into(), @@ -715,6 +717,7 @@ pub fn setup_investors(evm: &mut impl EvmEnv) { SECONDS_PER_YEAR, )); + // POOL B - Tranche 2/2 // Allow investor to locally invest crate::utils::pool::give_role::( investor.into(), @@ -734,6 +737,7 @@ pub fn setup_investors(evm: &mut impl EvmEnv) { SECONDS_PER_YEAR, )); + // POOL C - Tranche 1/1 // Allow investor to locally invest crate::utils::pool::give_role::( investor.into(), diff --git a/runtime/integration-tests/src/utils/pool.rs b/runtime/integration-tests/src/utils/pool.rs index ffa4fca521..ccdf417b1a 100644 --- a/runtime/integration-tests/src/utils/pool.rs +++ b/runtime/integration-tests/src/utils/pool.rs @@ -46,6 +46,17 @@ pub fn give_role(dest: AccountId, pool_id: PoolId, role: PoolRole) { .unwrap(); } +pub fn remove_role(dest: AccountId, pool_id: PoolId, role: PoolRole) { + pallet_permissions::Pallet::::remove( + RawOrigin::Root.into(), + Role::PoolRole(role), + dest, + PermissionScope::Pool(pool_id), + Role::PoolRole(role), + ) + .unwrap(); +} + pub fn create_empty(admin: AccountId, pool_id: PoolId, currency_id: CurrencyId) { create::( admin,