Skip to content

Commit

Permalink
MESH-2110/allow-sc-to-create-custodial-portfolios (#1547)
Browse files Browse the repository at this point in the history
* Add extrisics for allowing an identity to create and take custody of portfolios owned by a different id

* Remove call to id pallet when creating a custodial portfolio

* Emits PortfolioCustodianChanged event

---------

Co-authored-by: Robert Gabriel Jakabosky <[email protected]>
  • Loading branch information
HenriqueNogara and Neopallium authored Oct 26, 2023
1 parent 4c61e4e commit 05f5f2f
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 35 deletions.
3 changes: 3 additions & 0 deletions pallets/common/src/traits/portfolio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pub trait WeightInfo {
Self::move_portfolio_funds(f, n)
}
fn move_portfolio_funds(f: u32, u: u32) -> Weight;
fn allow_identity_to_create_portfolios() -> Weight;
fn revoke_create_portfolios_permission() -> Weight;
fn create_custody_portfolio() -> Weight;
}

pub trait Config: CommonConfig + identity::Config + base::Config {
Expand Down
17 changes: 17 additions & 0 deletions pallets/portfolio/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,21 @@ benchmarks! {
Module::<T>::create_portfolio(alice.clone().origin().into(), PortfolioName(b"MyOwnPortfolio".to_vec())).unwrap();
Module::<T>::pre_approve_portfolio(alice.clone().origin().into(), ticker, alice_custom_portfolio).unwrap();
}: _(alice.origin, ticker, alice_custom_portfolio)

allow_identity_to_create_portfolios {
let bob = UserBuilder::<T>::default().generate_did().build("Bob");
let alice = UserBuilder::<T>::default().generate_did().build("Alice");
}: _(alice.origin, bob.did())

revoke_create_portfolios_permission {
let bob = UserBuilder::<T>::default().generate_did().build("Bob");
let alice = UserBuilder::<T>::default().generate_did().build("Alice");
}: _(alice.origin, bob.did())

create_custody_portfolio {
let bob = UserBuilder::<T>::default().generate_did().build("Bob");
let alice = UserBuilder::<T>::default().generate_did().build("Alice");
let portfolio_name = PortfolioName("AliceOwnsBobControls".as_bytes().to_vec());
Module::<T>::allow_identity_to_create_portfolios(alice.clone().origin().into(), bob.did()).unwrap();
}: _(bob.origin, alice.did(), portfolio_name)
}
161 changes: 127 additions & 34 deletions pallets/portfolio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub mod benchmarking;

use codec::{Decode, Encode};
use core::{iter, mem};
use frame_support::dispatch::{DispatchError, DispatchResult, Weight};
use frame_support::dispatch::{DispatchError, DispatchResult};
use frame_support::{decl_error, decl_module, decl_storage, ensure};
use sp_arithmetic::traits::Zero;
use sp_std::collections::btree_set::BTreeSet;
Expand Down Expand Up @@ -119,6 +119,10 @@ decl_storage! {
pub PreApprovedPortfolios get(fn pre_approved_portfolios):
double_map hasher(twox_64_concat) PortfolioId, hasher(blake2_128_concat) Ticker => bool;

/// Custodians allowed to create and take custody of portfolios on an id's behalf.
pub AllowedCustodians get(fn allowed_custodians):
double_map hasher(identity) IdentityId, hasher(identity) IdentityId => bool;

/// Storage version.
StorageVersion get(fn storage_version) build(|_| Version::new(2)): Version;
}
Expand Down Expand Up @@ -159,7 +163,9 @@ decl_error! {
/// Locked NFTs can not be moved between portfolios.
InvalidTransferNFTIsLocked,
/// Trying to move an amount of zero assets.
EmptyTransfer
EmptyTransfer,
/// The caller doesn't have permission to create portfolios on the owner's behalf.
MissingOwnersPermission
}
}

Expand All @@ -172,14 +178,9 @@ decl_module! {

/// Creates a portfolio with the given `name`.
#[weight = <T as Config>::WeightInfo::create_portfolio()]
pub fn create_portfolio(origin, name: PortfolioName) {
let did = Identity::<T>::ensure_perms(origin)?;
Self::ensure_name_unique(&did, &name)?;

let num = Self::get_next_portfolio_number(&did);
NameToNumber::insert(&did, &name, num);
Portfolios::insert(&did, &num, name.clone());
Self::deposit_event(Event::PortfolioCreated(did, num, name));
pub fn create_portfolio(origin, name: PortfolioName) -> DispatchResult {
let callers_did = Identity::<T>::ensure_perms(origin)?;
Self::base_create_portfolio(callers_did, name)
}

/// Deletes a user portfolio. A portfolio can be deleted only if it has no funds.
Expand Down Expand Up @@ -360,34 +361,72 @@ decl_module! {
Self::base_remove_portfolio_pre_approval(origin, &ticker, portfolio_id)
}

fn on_runtime_upgrade() -> Weight {
use polymesh_primitives::storage_migrate_on;

// Remove old name to number mappings.
// In version 4.0.0 (first mainnet deployment) when a portfolio was removed
// the NameToNumber mapping was left out of date, this upgrade removes dangling
// NameToNumber mappings.
// https://github.com/PolymeshAssociation/Polymesh/pull/1200
storage_migrate_on!(StorageVersion, 1, {
NameToNumber::iter()
.filter(|(identity, _, number)| !Portfolios::contains_key(identity, number))
.for_each(|(identity, name, _)| NameToNumber::remove(identity, name));
});
storage_migrate_on!(StorageVersion, 2, {
Portfolios::iter()
.filter(|(identity, number, name)| Some(number) == Self::name_to_number(identity, name).as_ref())
.for_each(|(identity, number, name)| {
NameToNumber::insert(identity, name, number);
}
);
});
/// Adds an identity that will be allowed to create and take custody of a portfolio under the caller's identity.
///
/// # Arguments
/// * `trusted_identity` - the [`IdentityId`] that will be allowed to call `create_custody_portfolio`.
///
#[weight = <T as Config>::WeightInfo::allow_identity_to_create_portfolios()]
pub fn allow_identity_to_create_portfolios(
origin,
trusted_identity: IdentityId
) -> DispatchResult {
Self::base_allow_identity_to_create_portfolios(origin, trusted_identity)
}

/// Removes permission of an identity to create and take custody of a portfolio under the caller's identity.
///
/// # Arguments
/// * `identity` - the [`IdentityId`] that will have the permissions to call `create_custody_portfolio` revoked.
///
#[weight = <T as Config>::WeightInfo::revoke_create_portfolios_permission()]
pub fn revoke_create_portfolios_permission(
origin,
identity: IdentityId
) -> DispatchResult {
Self::base_revoke_create_portfolios_permission(origin, identity)
}

Weight::zero()
/// Creates a portfolio under the `portfolio_owner_id` identity and transfers its custody to the caller's identity.
///
/// # Arguments
/// * `portfolio_owner_id` - the [`IdentityId`] that will own the new portfolio.
/// * `portfolio_name` - the [`PortfolioName`] of the new portfolio.
///
#[weight = <T as Config>::WeightInfo::create_custody_portfolio()]
pub fn create_custody_portfolio(
origin,
portfolio_owner_id: IdentityId,
portfolio_name: PortfolioName
) -> DispatchResult {
Self::base_create_custody_portfolio(origin, portfolio_owner_id, portfolio_name)
}
}
}

impl<T: Config> Module<T> {
/// Creates a portfolio named `portfolio_name` owned by `portfolio_owner_id`.
fn base_create_portfolio(
portfolio_owner_id: IdentityId,
portfolio_name: PortfolioName,
) -> DispatchResult {
Self::ensure_name_unique(&portfolio_owner_id, &portfolio_name)?;
let portfolio_number = Self::get_next_portfolio_number(&portfolio_owner_id);

NameToNumber::insert(&portfolio_owner_id, &portfolio_name, portfolio_number);
Portfolios::insert(
&portfolio_owner_id,
&portfolio_number,
portfolio_name.clone(),
);
Self::deposit_event(Event::PortfolioCreated(
portfolio_owner_id,
portfolio_number,
portfolio_name,
));
Ok(())
}

/// Returns the custodian of `pid`.
fn custodian(pid: &PortfolioId) -> IdentityId {
PortfolioCustodian::get(&pid).unwrap_or(pid.did)
Expand Down Expand Up @@ -628,15 +667,23 @@ impl<T: Config> Module<T> {
// Set the custodian to the default value `None` meaning that the owner is the custodian.
PortfolioCustodian::remove(&pid);
} else {
PortfolioCustodian::insert(&pid, to);
PortfoliosInCustody::insert(&to, &pid, true);
Self::unverified_take_portfolio_custody(&pid, &to);
}

Self::deposit_event(Event::PortfolioCustodianChanged(to, pid, to));
Ok(())
})
}

/// Updates `portfolio_id` custody to `new_custodian_id`.
fn unverified_take_portfolio_custody(
portfolio_id: &PortfolioId,
new_custodian_id: &IdentityId,
) {
PortfolioCustodian::insert(&portfolio_id, new_custodian_id);
PortfoliosInCustody::insert(&new_custodian_id, &portfolio_id, true);
}

/// Verifies if the portfolios are different, if the move is between the same identity, if the receiving portfolio exists,
/// and if the user has access to both portfolios.
fn ensure_portfolios_validity_and_permissions(
Expand Down Expand Up @@ -781,6 +828,52 @@ impl<T: Config> Module<T> {
PreApprovedPortfolios::remove(&portfolio_id, ticker);
Ok(())
}

fn base_allow_identity_to_create_portfolios(
origin: T::RuntimeOrigin,
trusted_identity: IdentityId,
) -> DispatchResult {
let callers_did = Identity::<T>::ensure_perms(origin)?;
AllowedCustodians::insert(callers_did, trusted_identity, true);
Ok(())
}

fn base_revoke_create_portfolios_permission(
origin: T::RuntimeOrigin,
identity: IdentityId,
) -> DispatchResult {
let callers_did = Identity::<T>::ensure_perms(origin)?;
AllowedCustodians::remove(callers_did, identity);
Ok(())
}

fn base_create_custody_portfolio(
origin: T::RuntimeOrigin,
portfolio_owner_id: IdentityId,
portfolio_name: PortfolioName,
) -> DispatchResult {
let callers_did = Identity::<T>::ensure_perms(origin.clone())?;
// Ensures that the caller is allowed to create portfolios on `portfolio_owner_id` behalf
ensure!(
AllowedCustodians::get(&portfolio_owner_id, &callers_did),
Error::<T>::MissingOwnersPermission
);
// Creates a new portfolio
let portfolio_number = NextPortfolioNumber::get(&portfolio_owner_id);
let portfolio_id = PortfolioId {
did: portfolio_owner_id,
kind: PortfolioKind::User(portfolio_number),
};
Self::base_create_portfolio(portfolio_owner_id, portfolio_name)?;
// Updates storage for taking ownership of a portfolio
Self::unverified_take_portfolio_custody(&portfolio_id, &callers_did);
Self::deposit_event(Event::PortfolioCustodianChanged(
callers_did,
portfolio_id,
callers_did,
));
Ok(())
}
}

impl<T: Config> PortfolioSubTrait<T::AccountId> for Module<T> {
Expand Down
81 changes: 80 additions & 1 deletion pallets/runtime/tests/src/portfolio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use frame_support::storage::StorageDoubleMap;
use frame_support::{assert_noop, assert_ok, StorageMap};

use pallet_portfolio::{
Event, NameToNumber, PortfolioAssetBalances, PortfolioNFT, PreApprovedPortfolios,
AllowedCustodians, Event, NameToNumber, PortfolioAssetBalances, PortfolioCustodian,
PortfolioNFT, Portfolios, PreApprovedPortfolios,
};
use polymesh_common_utilities::portfolio::PortfolioSubTrait;
use polymesh_primitives::asset::{AssetType, NonFungibleType};
Expand Down Expand Up @@ -1015,3 +1016,81 @@ fn unauthorized_custodian_pre_approval() {
),);
});
}

#[test]
fn create_custody_portfolio_missing_owners_permission() {
ExtBuilder::default().build().execute_with(|| {
let bob = User::new(AccountKeyring::Bob);
let alice = User::new(AccountKeyring::Alice);
let portfolio_name = PortfolioName("AliceOwnsBobControls".as_bytes().to_vec());

assert_noop!(
Portfolio::create_custody_portfolio(bob.origin(), alice.did, portfolio_name),
Error::MissingOwnersPermission
);
assert_eq!(AllowedCustodians::get(alice.did, bob.did), false);
});
}

#[test]
fn create_custody_portfolio() {
ExtBuilder::default().build().execute_with(|| {
let bob = User::new(AccountKeyring::Bob);
let alice = User::new(AccountKeyring::Alice);
let portfolio_number = PortfolioNumber(1);
let portfolio_name = PortfolioName("AliceOwnsBobControls".as_bytes().to_vec());
let portfolio_id = PortfolioId {
did: alice.did,
kind: PortfolioKind::User(portfolio_number),
};

assert_ok!(Portfolio::allow_identity_to_create_portfolios(
alice.origin(),
bob.did
));
// Asserts storage has been updated
assert_eq!(AllowedCustodians::get(alice.did, bob.did), true);

assert_ok!(Portfolio::create_custody_portfolio(
bob.origin(),
alice.did,
portfolio_name.clone()
));
// Asserts storage has been updated
assert_eq!(
Portfolios::get(alice.did, portfolio_number),
Some(portfolio_name)
);
assert_eq!(PortfolioCustodian::get(portfolio_id), Some(bob.did));
});
}

#[test]
fn create_custody_portfolio_revoke_permission() {
ExtBuilder::default().build().execute_with(|| {
let bob = User::new(AccountKeyring::Bob);
let alice = User::new(AccountKeyring::Alice);
let portfolio_name = PortfolioName("AliceOwnsBobControls".as_bytes().to_vec());

assert_ok!(Portfolio::allow_identity_to_create_portfolios(
alice.origin(),
bob.did
));
assert_ok!(Portfolio::create_custody_portfolio(
bob.origin(),
alice.did,
portfolio_name.clone()
));
assert_ok!(Portfolio::revoke_create_portfolios_permission(
alice.origin(),
bob.did
));
// Asserts storage has been updated
assert_eq!(AllowedCustodians::get(alice.did, bob.did), false);

assert_noop!(
Portfolio::create_custody_portfolio(bob.origin(), alice.did, portfolio_name),
Error::MissingOwnersPermission
);
});
}
40 changes: 40 additions & 0 deletions pallets/weights/src/pallet_portfolio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,44 @@ impl pallet_portfolio::WeightInfo for SubstrateWeight {
.saturating_add(DbWeight::get().reads(3))
.saturating_add(DbWeight::get().writes(1))
}
// Storage: Identity KeyRecords (r:1 w:0)
// Proof Skipped: Identity KeyRecords (max_values: None, max_size: None, mode: Measured)
// Storage: Portfolio AllowedCustodians (r:0 w:1)
// Proof Skipped: Portfolio AllowedCustodians (max_values: None, max_size: None, mode: Measured)
fn allow_identity_to_create_portfolios() -> Weight {
// Minimum execution time: 23_275 nanoseconds.
Weight::from_ref_time(23_598_000)
.saturating_add(DbWeight::get().reads(1))
.saturating_add(DbWeight::get().writes(1))
}
// Storage: Identity KeyRecords (r:1 w:0)
// Proof Skipped: Identity KeyRecords (max_values: None, max_size: None, mode: Measured)
// Storage: Portfolio AllowedCustodians (r:0 w:1)
// Proof Skipped: Portfolio AllowedCustodians (max_values: None, max_size: None, mode: Measured)
fn revoke_create_portfolios_permission() -> Weight {
// Minimum execution time: 23_458 nanoseconds.
Weight::from_ref_time(23_605_000)
.saturating_add(DbWeight::get().reads(1))
.saturating_add(DbWeight::get().writes(1))
}
// Storage: Identity KeyRecords (r:1 w:0)
// Proof Skipped: Identity KeyRecords (max_values: None, max_size: None, mode: Measured)
// Storage: Portfolio AllowedCustodians (r:1 w:0)
// Proof Skipped: Portfolio AllowedCustodians (max_values: None, max_size: None, mode: Measured)
// Storage: Portfolio NextPortfolioNumber (r:1 w:1)
// Proof Skipped: Portfolio NextPortfolioNumber (max_values: None, max_size: None, mode: Measured)
// Storage: Portfolio NameToNumber (r:1 w:1)
// Proof Skipped: Portfolio NameToNumber (max_values: None, max_size: None, mode: Measured)
// Storage: Portfolio PortfolioCustodian (r:0 w:1)
// Proof Skipped: Portfolio PortfolioCustodian (max_values: None, max_size: None, mode: Measured)
// Storage: Portfolio PortfoliosInCustody (r:0 w:1)
// Proof Skipped: Portfolio PortfoliosInCustody (max_values: None, max_size: None, mode: Measured)
// Storage: Portfolio Portfolios (r:0 w:1)
// Proof Skipped: Portfolio Portfolios (max_values: None, max_size: None, mode: Measured)
fn create_custody_portfolio() -> Weight {
// Minimum execution time: 59_601 nanoseconds.
Weight::from_ref_time(60_400_000)
.saturating_add(DbWeight::get().reads(4))
.saturating_add(DbWeight::get().writes(5))
}
}

0 comments on commit 05f5f2f

Please sign in to comment.