Skip to content

Commit

Permalink
feat: add freeze & unfreeze extrinsics (centrifuge#1947)
Browse files Browse the repository at this point in the history
* feat: add FrozenInvestor permission

* feat: add freeze + unfreeze extrinsics

* chore: update investments logic

* tests: fix UTs + add ITs

* docs: improve LP

* weights: simplify for LP

* fix: clippy

* refactor: req perm to be set beforehand

* chore: apply suggestions from code review

* refactor: move some tests

* refactor: common runtime permissions

* feat: add perm migration

* feat-creep: rm nonce > 0 pre-upgrade condition in gateway migration

* trigger CI after re-disabling DRAFT

* fix: unwanted nesting + fmt
  • Loading branch information
wischli authored Aug 9, 2024
1 parent 1cb9fe3 commit 1e318e2
Show file tree
Hide file tree
Showing 25 changed files with 1,360 additions and 331 deletions.
85 changes: 85 additions & 0 deletions libs/types/src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub enum PoolRole<TrancheId = [u8; 16]> {
LoanAdmin,
TrancheInvestor(TrancheId, Seconds),
PODReadAccess,
FrozenTrancheInvestor(TrancheId),
}

#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, TypeInfo, Debug)]
Expand Down Expand Up @@ -110,6 +111,7 @@ pub struct PermissionedCurrencyHolderInfo {
pub struct TrancheInvestorInfo<TrancheId> {
tranche_id: TrancheId,
permissioned_till: Seconds,
is_frozen: bool,
}

#[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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<Now, MinDelay, TrancheId, MaxTranches: Get<u32>> {
pool_admin: PoolAdminRoles,
currency_admin: CurrencyAdminRoles,
permissioned_asset_holder: PermissionedCurrencyHolders<Now, MinDelay>,
tranche_investor: TrancheInvestors<Now, MinDelay, TrancheId, MaxTranches>,
}

#[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)]
pub struct TrancheInvestors<Now, MinDelay, TrancheId, MaxTranches: Get<u32>> {
info: BoundedVec<TrancheInvestorInfo<TrancheId>, MaxTranches>,
_phantom: PhantomData<(Now, MinDelay)>,
}

#[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)]
pub struct TrancheInvestorInfo<TrancheId> {
tranche_id: TrancheId,
permissioned_till: Seconds,
}

impl<Now, MinDelay, TrancheId, MaxTranches: Get<u32>>
TrancheInvestors<Now, MinDelay, TrancheId, MaxTranches>
{
fn migrate(self) -> super::TrancheInvestors<Now, MinDelay, TrancheId, MaxTranches> {
super::TrancheInvestors::<Now, MinDelay, TrancheId, MaxTranches> {
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<Now, MinDelay, TrancheId: Clone, MaxTranches: Get<u32>>
PermissionRoles<Now, MinDelay, TrancheId, MaxTranches>
{
pub fn migrate(self) -> super::PermissionRoles<Now, MinDelay, TrancheId, MaxTranches> {
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(),
}
}
}
}
7 changes: 7 additions & 0 deletions pallets/investments/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,13 @@ impl<T: Config> Pallet<T> {
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::<T>::UnknownInvestment)?;
let (collected_investment, post_dispatch_info) = InvestOrders::<T>::try_mutate(
&who,
Expand Down
27 changes: 22 additions & 5 deletions pallets/investments/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use sp_std::{
};

pub use crate as pallet_investments;
use crate::OrderType;

pub type AccountId = u64;

Expand Down Expand Up @@ -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<T> PreConditions<T> for Always {
pub(crate) const ERR_PRE_CONDITION: DispatchError =
DispatchError::Other("PreCondition mock fails on u64::MAX account on purpose");

pub struct AlwaysWithOneException;
impl<T> PreConditions<T> for AlwaysWithOneException
where
T: Into<OrderType<AccountId, InvestmentId, Balance>> + 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(()),
}
}
}

Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions pallets/investments/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
),);
})
}
Loading

0 comments on commit 1e318e2

Please sign in to comment.