use cfg_traits::{Properties, Seconds, TimeAsSecs};
use frame_support::{traits::Get, BoundedVec};
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};
use sp_std::{
cmp::{Ord, PartialEq, PartialOrd},
marker::PhantomData,
};
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, TypeInfo, Debug)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum PoolRole<TrancheId = [u8; 16]> {
PoolAdmin,
Borrower,
PricingAdmin,
LiquidityAdmin,
InvestorAdmin,
LoanAdmin,
TrancheInvestor(TrancheId, Seconds),
PODReadAccess,
FrozenTrancheInvestor(TrancheId),
}
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, TypeInfo, Debug)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum PermissionedCurrencyRole {
Holder(Seconds),
Manager,
Issuer,
}
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, TypeInfo, Debug)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum Role<TrancheId = [u8; 16]> {
PoolRole(PoolRole<TrancheId>),
PermissionedCurrencyRole(PermissionedCurrencyRole),
}
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, TypeInfo, Debug, MaxEncodedLen)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum PermissionScope<PoolId, CurrencyId> {
Pool(PoolId),
Currency(CurrencyId),
}
impl<PoolId, CurrencyId> Default for PermissionScope<PoolId, CurrencyId>
where
PoolId: Default,
{
fn default() -> Self {
Self::Pool(PoolId::default())
}
}
bitflags::bitflags! {
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct PoolAdminRoles: u32 {
const POOL_ADMIN = 0b00000001;
const BORROWER = 0b00000010;
const PRICING_ADMIN = 0b00000100;
const LIQUIDITY_ADMIN = 0b00001000;
const INVESTOR_ADMIN = 0b00010000;
const RISK_ADMIN = 0b00100000;
const POD_READ_ACCESS = 0b01000000;
}
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct CurrencyAdminRoles: u32 {
const PERMISSIONED_ASSET_MANAGER = 0b00000001;
const PERMISSIONED_ASSET_ISSUER = 0b00000010;
}
}
#[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)]
pub struct PermissionedCurrencyHolderInfo {
permissioned_till: Seconds,
}
#[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)]
pub struct TrancheInvestorInfo<TrancheId> {
tranche_id: TrancheId,
permissioned_till: Seconds,
is_frozen: bool,
}
#[derive(Encode, Decode, TypeInfo, Debug, Clone, Eq, PartialEq, MaxEncodedLen)]
pub struct PermissionedCurrencyHolders<Now, MinDelay> {
info: Option<PermissionedCurrencyHolderInfo>,
_phantom: PhantomData<(Now, MinDelay)>,
}
#[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, 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>,
}
impl<Now, MinDelay> Default for PermissionedCurrencyHolders<Now, MinDelay>
where
Now: TimeAsSecs,
MinDelay: Get<Seconds>,
{
fn default() -> Self {
Self {
info: None,
_phantom: Default::default(),
}
}
}
impl<Now, MinDelay, TrancheId, MaxTranches> Default
for TrancheInvestors<Now, MinDelay, TrancheId, MaxTranches>
where
Now: TimeAsSecs,
MinDelay: Get<Seconds>,
TrancheId: PartialEq + PartialOrd,
MaxTranches: Get<u32>,
{
fn default() -> Self {
Self {
info: BoundedVec::default(),
_phantom: Default::default(),
}
}
}
impl<Now, MinDelay, TrancheId, MaxTranches> Default
for PermissionRoles<Now, MinDelay, TrancheId, MaxTranches>
where
Now: TimeAsSecs,
MinDelay: Get<Seconds>,
TrancheId: PartialEq + PartialOrd,
MaxTranches: Get<u32>,
{
fn default() -> Self {
Self {
pool_admin: PoolAdminRoles::empty(),
currency_admin: CurrencyAdminRoles::empty(),
permissioned_asset_holder: PermissionedCurrencyHolders::<Now, MinDelay>::default(),
tranche_investor: TrancheInvestors::<Now, MinDelay, TrancheId, MaxTranches>::default(),
}
}
}
pub const UNION: Seconds = 0;
impl<Now, MinDelay, TrancheId, MaxTranches> Properties
for PermissionRoles<Now, MinDelay, TrancheId, MaxTranches>
where
Now: TimeAsSecs,
MinDelay: Get<Seconds>,
TrancheId: PartialEq + PartialOrd,
MaxTranches: Get<u32>,
{
type Error = ();
type Ok = ();
type Property = Role<TrancheId>;
fn exists(&self, property: Self::Property) -> bool {
match property {
Role::PoolRole(pool_role) => match pool_role {
PoolRole::Borrower => self.pool_admin.contains(PoolAdminRoles::BORROWER),
PoolRole::LiquidityAdmin => {
self.pool_admin.contains(PoolAdminRoles::LIQUIDITY_ADMIN)
}
PoolRole::PoolAdmin => self.pool_admin.contains(PoolAdminRoles::POOL_ADMIN),
PoolRole::PricingAdmin => self.pool_admin.contains(PoolAdminRoles::PRICING_ADMIN),
PoolRole::InvestorAdmin => self.pool_admin.contains(PoolAdminRoles::INVESTOR_ADMIN),
PoolRole::LoanAdmin => self.pool_admin.contains(PoolAdminRoles::RISK_ADMIN),
PoolRole::TrancheInvestor(id, _) => self.tranche_investor.contains(id),
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 {
PermissionedCurrencyRole::Holder(_) => {
self.permissioned_asset_holder.contains()
}
PermissionedCurrencyRole::Manager => self
.currency_admin
.contains(CurrencyAdminRoles::PERMISSIONED_ASSET_MANAGER),
PermissionedCurrencyRole::Issuer => self
.currency_admin
.contains(CurrencyAdminRoles::PERMISSIONED_ASSET_ISSUER),
}
}
}
}
fn empty(&self) -> bool {
self.pool_admin.is_empty()
&& self.currency_admin.is_empty()
&& self.tranche_investor.is_empty()
&& self.permissioned_asset_holder.is_empty()
}
fn rm(&mut self, property: Self::Property) -> Result<(), ()> {
match property {
Role::PoolRole(pool_role) => match pool_role {
PoolRole::Borrower => Ok(self.pool_admin.remove(PoolAdminRoles::BORROWER)),
PoolRole::LiquidityAdmin => {
Ok(self.pool_admin.remove(PoolAdminRoles::LIQUIDITY_ADMIN))
}
PoolRole::PoolAdmin => Ok(self.pool_admin.remove(PoolAdminRoles::POOL_ADMIN)),
PoolRole::PricingAdmin => Ok(self.pool_admin.remove(PoolAdminRoles::PRICING_ADMIN)),
PoolRole::InvestorAdmin => {
Ok(self.pool_admin.remove(PoolAdminRoles::INVESTOR_ADMIN))
}
PoolRole::LoanAdmin => Ok(self.pool_admin.remove(PoolAdminRoles::RISK_ADMIN)),
PoolRole::TrancheInvestor(id, delta) => self.tranche_investor.remove(id, delta),
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 {
PermissionedCurrencyRole::Holder(delta) => {
self.permissioned_asset_holder.remove(delta)
}
PermissionedCurrencyRole::Manager => Ok(self
.currency_admin
.remove(CurrencyAdminRoles::PERMISSIONED_ASSET_MANAGER)),
PermissionedCurrencyRole::Issuer => Ok(self
.currency_admin
.remove(CurrencyAdminRoles::PERMISSIONED_ASSET_ISSUER)),
}
}
}
}
fn add(&mut self, property: Self::Property) -> Result<(), ()> {
match property {
Role::PoolRole(pool_role) => match pool_role {
PoolRole::Borrower => Ok(self.pool_admin.insert(PoolAdminRoles::BORROWER)),
PoolRole::LiquidityAdmin => {
Ok(self.pool_admin.insert(PoolAdminRoles::LIQUIDITY_ADMIN))
}
PoolRole::PoolAdmin => Ok(self.pool_admin.insert(PoolAdminRoles::POOL_ADMIN)),
PoolRole::PricingAdmin => Ok(self.pool_admin.insert(PoolAdminRoles::PRICING_ADMIN)),
PoolRole::InvestorAdmin => {
Ok(self.pool_admin.insert(PoolAdminRoles::INVESTOR_ADMIN))
}
PoolRole::LoanAdmin => Ok(self.pool_admin.insert(PoolAdminRoles::RISK_ADMIN)),
PoolRole::TrancheInvestor(id, delta) => self.tranche_investor.insert(id, delta),
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 {
PermissionedCurrencyRole::Holder(delta) => {
self.permissioned_asset_holder.insert(delta)
}
PermissionedCurrencyRole::Manager => Ok(self
.currency_admin
.insert(CurrencyAdminRoles::PERMISSIONED_ASSET_MANAGER)),
PermissionedCurrencyRole::Issuer => Ok(self
.currency_admin
.insert(CurrencyAdminRoles::PERMISSIONED_ASSET_ISSUER)),
}
}
}
}
}
impl<Now, MinDelay> PermissionedCurrencyHolders<Now, MinDelay>
where
Now: TimeAsSecs,
MinDelay: Get<Seconds>,
{
pub fn empty() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.info.is_none()
}
fn validity(&self, delta: Seconds) -> Result<Seconds, ()> {
let now = <Now as TimeAsSecs>::now();
let min_validity = now.saturating_add(MinDelay::get());
let req_validity = now.saturating_add(delta);
if req_validity < min_validity {
return Err(());
}
Ok(req_validity)
}
pub fn contains(&self) -> bool {
if let Some(info) = &self.info {
info.permissioned_till >= <Now as TimeAsSecs>::now()
} else {
false
}
}
#[allow(clippy::result_unit_err)]
pub fn remove(&mut self, delta: Seconds) -> Result<(), ()> {
if let Some(info) = &self.info {
let valid_till = &info.permissioned_till;
let now = <Now as TimeAsSecs>::now();
if *valid_till <= now {
Err(())
} else {
let permissioned_till = self.validity(delta)?;
self.info = Some(PermissionedCurrencyHolderInfo { permissioned_till });
Ok(())
}
} else {
Err(())
}
}
#[allow(clippy::result_unit_err)]
pub fn insert(&mut self, delta: Seconds) -> Result<(), ()> {
let validity = self.validity(delta)?;
match &self.info {
Some(info) if info.permissioned_till > validity => Err(()),
_ => {
self.info = Some(PermissionedCurrencyHolderInfo {
permissioned_till: validity,
});
Ok(())
}
}
}
}
impl<Now, MinDelay, TrancheId, MaxTranches> TrancheInvestors<Now, MinDelay, TrancheId, MaxTranches>
where
Now: TimeAsSecs,
MinDelay: Get<Seconds>,
TrancheId: PartialEq + PartialOrd,
MaxTranches: Get<u32>,
{
pub fn empty() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.info.is_empty()
}
fn validity(&self, delta: Seconds) -> Result<Seconds, ()> {
let now = <Now as TimeAsSecs>::now();
let min_validity = now.saturating_add(MinDelay::get());
let req_validity = now.saturating_add(delta);
if req_validity < min_validity {
Err(())
} else {
Ok(req_validity)
}
}
pub fn contains(&self, tranche: TrancheId) -> bool {
self.info.iter().any(|info| {
info.tranche_id == tranche && info.permissioned_till >= <Now as TimeAsSecs>::now()
})
}
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) {
let valid_till = &self.info[index].permissioned_till;
let now = <Now as TimeAsSecs>::now();
if *valid_till <= now {
Err(())
} else {
Ok(self.info[index].permissioned_till = self.validity(delta)?)
}
} else {
Err(())
}
}
#[allow(clippy::result_unit_err)]
pub fn insert(&mut self, tranche: TrancheId, delta: Seconds) -> Result<(), ()> {
let validity = self.validity(delta)?;
if let Some(index) = self.info.iter().position(|info| info.tranche_id == tranche) {
if self.info[index].permissioned_till > validity {
Err(())
} else {
Ok(self.info[index].permissioned_till = validity)
}
} else {
self.info
.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)]
mod tests {
use core::time::Duration;
use frame_support::parameter_types;
use super::*;
parameter_types! {
pub const MinDelay: u64 = 4;
pub const MaxTranches: u32 = 5;
}
struct Now;
impl Now {
fn pass(delta: u64) {
unsafe {
let current = NOW_HOLDER;
NOW_HOLDER = current + delta;
};
}
fn set(now: u64) {
unsafe {
NOW_HOLDER = now;
};
}
}
static mut NOW_HOLDER: u64 = 0;
impl frame_support::traits::UnixTime for Now {
fn now() -> Duration {
unsafe { Duration::new(NOW_HOLDER, 0) }
}
}
const UNION: u64 = 0u64;
type TrancheId = [u8; 16];
fn into_tranche_id(val: u8) -> TrancheId {
[val; 16]
}
#[test]
fn permission_roles_work() {
assert!(PermissionRoles::<Now, MinDelay, TrancheId, MaxTranches>::default().empty());
let mut roles = PermissionRoles::<Now, MinDelay, TrancheId, MaxTranches>::default();
assert!(roles
.add(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(30),
10
)))
.is_ok());
assert!(roles
.add(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(30),
9
)))
.is_err());
assert!(roles
.add(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(30),
11
)))
.is_ok());
assert!(!roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
UNION
))));
assert!(roles
.add(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
MinDelay::get()
)))
.is_ok());
assert!(roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
UNION
))));
assert!(roles
.rm(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
0
)))
.is_err());
Now::pass(1);
assert!(roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
UNION
))));
assert!(roles
.rm(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
MinDelay::get() - 1
)))
.is_err());
assert!(roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
UNION
))));
Now::set(0);
assert!(roles
.rm(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
MinDelay::get()
)))
.is_ok());
Now::pass(6);
assert!(!roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(0),
UNION
))));
Now::set(0);
assert!(roles
.add(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(1),
MinDelay::get()
)))
.is_ok());
assert!(roles
.add(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(2),
MinDelay::get()
)))
.is_ok());
assert!(roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(1),
UNION
))));
assert!(roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(2),
UNION
))));
assert!(roles.add(Role::PoolRole(PoolRole::LiquidityAdmin)).is_ok());
assert!(roles.add(Role::PoolRole(PoolRole::InvestorAdmin)).is_ok());
assert!(roles.add(Role::PoolRole(PoolRole::PODReadAccess)).is_ok());
assert!(roles.exists(Role::PoolRole(PoolRole::LiquidityAdmin)));
assert!(roles.exists(Role::PoolRole(PoolRole::InvestorAdmin)));
assert!(roles.exists(Role::PoolRole(PoolRole::PODReadAccess)));
assert!(roles
.add(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(8),
MinDelay::get() + 2
)))
.is_ok());
assert!(roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(8),
UNION
))));
Now::pass(MinDelay::get() + 2);
assert!(roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(8),
UNION
))));
Now::pass(1);
assert!(!roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(8),
UNION
))));
Now::set(0);
assert!(roles
.add(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(5),
MinDelay::get() - 1
)))
.is_err());
assert!(!roles.exists(Role::PoolRole(PoolRole::TrancheInvestor(
into_tranche_id(5),
UNION
))));
assert!(roles.rm(Role::PoolRole(PoolRole::LiquidityAdmin)).is_ok());
assert!(roles.rm(Role::PoolRole(PoolRole::InvestorAdmin)).is_ok());
assert!(roles.rm(Role::PoolRole(PoolRole::PODReadAccess)).is_ok());
assert!(!roles.exists(Role::PoolRole(PoolRole::LiquidityAdmin)));
assert!(!roles.exists(Role::PoolRole(PoolRole::InvestorAdmin)));
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(),
}
}
}
}