diff --git a/pallets/asset/src/benchmarking.rs b/pallets/asset/src/benchmarking.rs index bd9f5ff7d1..0f4236d346 100644 --- a/pallets/asset/src/benchmarking.rs +++ b/pallets/asset/src/benchmarking.rs @@ -752,4 +752,17 @@ benchmarks! { let asset_id = create_sample_asset::(&alice, true); let ticker = reg_unique_ticker::(alice.origin().into(), None); }: _(alice.origin, ticker, asset_id) + + unlink_ticker_from_asset_id { + set_ticker_registration_config::(); + let alice = UserBuilder::::default().generate_did().build("Alice"); + let asset_id = create_sample_asset::(&alice, true); + let ticker = reg_unique_ticker::(alice.origin().into(), None); + Module::::link_ticker_to_asset_id( + alice.clone().origin().into(), + ticker, + asset_id + ) + .unwrap(); + }: _(alice.origin, ticker, asset_id) } diff --git a/pallets/asset/src/error.rs b/pallets/asset/src/error.rs index 30419b3a6a..416876379e 100644 --- a/pallets/asset/src/error.rs +++ b/pallets/asset/src/error.rs @@ -99,6 +99,10 @@ decl_error! { /// An unexpected error when generating a new asset ID. AssetIDGenerationError, /// The ticker doesn't belong to the caller. - TickerNotRegisteredToCaller + TickerNotRegisteredToCaller, + /// The given asset is already linked to a ticker. + AssetIsAlreadyLinkedToATicker, + /// The given ticker is not linked to the given asset. + TickerIsNotLinkedToTheAsset } } diff --git a/pallets/asset/src/lib.rs b/pallets/asset/src/lib.rs index 4bb9c243bd..a8868dc8fb 100644 --- a/pallets/asset/src/lib.rs +++ b/pallets/asset/src/lib.rs @@ -837,6 +837,20 @@ decl_module! { pub fn link_ticker_to_asset_id(origin, ticker: Ticker, asset_id: AssetID) { Self::base_link_ticker_to_asset_id(origin, ticker, asset_id)?; } + + /// Removes the link between a ticker and an asset. + /// + /// # Arguments + /// * `origin`: the secondary key of the sender. + /// * `ticker`: the [`Ticker`] that will be unlinked from the given `asset_id`. + /// * `asset_id`: the [`AssetID`] that will be unlink from `ticker`. + /// + /// # Permissions + /// * Asset + #[weight = ::WeightInfo::unlink_ticker_from_asset_id()] + pub fn unlink_ticker_from_asset_id(origin, ticker: Ticker, asset_id: AssetID) { + Self::base_unlink_ticker_from_asset_id(origin, ticker, asset_id)?; + } } } @@ -959,7 +973,7 @@ impl Module { ) -> DispatchResult { let caller_did = >::ensure_perms(origin, asset_id)?; - Self::ensure_security_token_exists(&asset_id)?; + Self::ensure_asset_exists(&asset_id)?; if freeze { ensure!(Frozen::get(&asset_id) == false, Error::::AlreadyFrozen); @@ -981,7 +995,7 @@ impl Module { asset_name: AssetName, ) -> DispatchResult { Self::ensure_valid_asset_name(&asset_name)?; - Self::ensure_security_token_exists(&asset_id)?; + Self::ensure_asset_exists(&asset_id)?; let caller_did = >::ensure_perms(origin, asset_id)?; @@ -1608,14 +1622,16 @@ impl Module { ticker_registration.expiry = None; Ok(()) } - None => return Err(Error::::TickerRegistrationNotFound.into()), + None => Err(Error::::TickerRegistrationNotFound.into()), } }, )?; // The ticker can't be linked to any other asset + Self::ensure_ticker_not_linked(&ticker)?; + // The asset can't be linked to any other ticker ensure!( - !TickerAssetID::contains_key(ticker), - Error::::TickerIsAlreadyLinkedToAnAsset + !AssetIDTicker::contains_key(asset_id), + Error::::AssetIsAlreadyLinkedToATicker ); // Links the ticker to the asset TickerAssetID::insert(ticker, asset_id); @@ -1623,6 +1639,36 @@ impl Module { Self::deposit_event(RawEvent::TickerLinkedToAsset(caller_did, ticker, asset_id)); Ok(()) } + + pub fn base_unlink_ticker_from_asset_id( + origin: T::RuntimeOrigin, + ticker: Ticker, + asset_id: AssetID, + ) -> DispatchResult { + // Verifies if the caller has the correct permissions for this asset + let caller_did = ExternalAgents::::ensure_perms(origin, asset_id)?; + + // The caller must own the ticker + let ticker_registration = UniqueTickerRegistration::::take(ticker) + .ok_or(Error::::TickerRegistrationNotFound)?; + ensure!( + ticker_registration.owner == caller_did, + Error::::TickerNotRegisteredToCaller + ); + + // The ticker must be linked to the given asset + ensure!( + TickerAssetID::get(ticker) == Some(asset_id), + Error::::TickerIsNotLinkedToTheAsset + ); + + // Removes the storage links + TickersOwnedByUser::remove(caller_did, ticker); + TickerAssetID::remove(ticker); + AssetIDTicker::remove(asset_id); + + Ok(()) + } } //========================================================================== @@ -1859,12 +1905,6 @@ impl Module { }) } - /// Returns `Ok` if there is a [`AssetDetails`] associated to `asset_id`. Otherwise, returns [`Error::NoSuchAsset`]. - fn ensure_security_token_exists(asset_id: &AssetID) -> DispatchResult { - ensure!(Assets::contains_key(asset_id), Error::::NoSuchAsset); - Ok(()) - } - /// Ensure asset metadata `value` is within the global limit. fn ensure_asset_metadata_value_limited(value: &AssetMetadataValue) -> DispatchResult { ensure!( diff --git a/pallets/common/src/traits/asset.rs b/pallets/common/src/traits/asset.rs index 8e31457dd5..67d8c0adf7 100644 --- a/pallets/common/src/traits/asset.rs +++ b/pallets/common/src/traits/asset.rs @@ -218,6 +218,7 @@ pub trait WeightInfo { fn add_mandatory_mediators(n: u32) -> Weight; fn remove_mandatory_mediators(n: u32) -> Weight; fn link_ticker_to_asset_id() -> Weight; + fn unlink_ticker_from_asset_id() -> Weight; } pub trait AssetFnTrait { diff --git a/pallets/runtime/tests/src/asset_pallet/link_ticker_to_asset.rs b/pallets/runtime/tests/src/asset_pallet/link_ticker_to_asset.rs new file mode 100644 index 0000000000..4a68498e29 --- /dev/null +++ b/pallets/runtime/tests/src/asset_pallet/link_ticker_to_asset.rs @@ -0,0 +1,238 @@ +use frame_support::StorageMap; +use frame_support::{assert_noop, assert_ok}; +use sp_keyring::AccountKeyring; + +use pallet_asset::{AssetIDTicker, TickerAssetID}; +use polymesh_primitives::Ticker; + +use crate::asset_test::{now, set_timestamp}; +use crate::storage::User; +use crate::{ExtBuilder, TestStorage}; + +type Asset = pallet_asset::Module; +type AssetError = pallet_asset::Error; +type ExternalAgentsError = pallet_external_agents::Error; + +#[test] +fn link_ticker_to_asset_id_successfully() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_eq!(TickerAssetID::get(ticker), Some(asset_id)); + assert_eq!(AssetIDTicker::get(asset_id), Some(ticker)); + }); +} + +#[test] +fn link_ticker_to_asset_id_ticker_not_registered_to_caller() { + ExtBuilder::default().build().execute_with(|| { + let dave = User::new(AccountKeyring::Dave); + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(dave.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + assert_ok!(Asset::create_asset( + dave.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_noop!( + Asset::link_ticker_to_asset_id(dave.origin(), ticker, asset_id), + AssetError::TickerNotRegisteredToCaller + ); + }); +} + +#[test] +fn link_ticker_to_asset_id_ticker_unauthorized_agent() { + ExtBuilder::default().build().execute_with(|| { + let dave = User::new(AccountKeyring::Dave); + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(dave.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + assert_ok!(Asset::create_asset( + dave.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_noop!( + Asset::link_ticker_to_asset_id(alice.origin(), ticker, asset_id), + ExternalAgentsError::UnauthorizedAgent + ); + }); +} + +#[test] +fn link_ticker_to_asset_id_expired_ticker() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + set_timestamp(now() + 10001); + assert_noop!( + Asset::link_ticker_to_asset_id(alice.origin(), ticker, asset_id), + AssetError::TickerRegistrationExpired + ); + }); +} + +#[test] +fn link_ticker_to_asset_id_ticker_already_linked() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + let asset_id = Asset::generate_asset_id(alice.acc(), false); + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + let asset_id = Asset::generate_asset_id(alice.acc(), false); + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_noop!( + Asset::link_ticker_to_asset_id(alice.origin(), ticker, asset_id), + AssetError::TickerIsAlreadyLinkedToAnAsset + ); + }); +} + +#[test] +fn link_ticker_to_asset_asset_already_linked() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + let ticker_1: Ticker = Ticker::from_slice_truncated(b"TICKER1"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker_1,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_noop!( + Asset::link_ticker_to_asset_id(alice.origin(), ticker_1, asset_id), + AssetError::AssetIsAlreadyLinkedToATicker + ); + }); +} + +#[test] +fn link_ticker_to_asset_id_after_unlink() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + let ticker_1: Ticker = Ticker::from_slice_truncated(b"TICKER1"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker_1,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_ok!(Asset::unlink_ticker_from_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker_1, + asset_id + )); + + assert_eq!(TickerAssetID::get(ticker_1), Some(asset_id)); + assert_eq!(AssetIDTicker::get(asset_id), Some(ticker_1)); + }); +} diff --git a/pallets/runtime/tests/src/asset_pallet/mod.rs b/pallets/runtime/tests/src/asset_pallet/mod.rs index 13bd6567d8..7f1458254b 100644 --- a/pallets/runtime/tests/src/asset_pallet/mod.rs +++ b/pallets/runtime/tests/src/asset_pallet/mod.rs @@ -2,7 +2,9 @@ mod accept_ticker_transfer; mod base_transfer; mod controller_transfer; mod issue; +mod link_ticker_to_asset; mod register_metadata; mod register_ticker; +mod unlink_ticker_from_asset; pub(crate) mod setup; diff --git a/pallets/runtime/tests/src/asset_pallet/register_ticker.rs b/pallets/runtime/tests/src/asset_pallet/register_ticker.rs index 251765f86d..546d37d9cf 100644 --- a/pallets/runtime/tests/src/asset_pallet/register_ticker.rs +++ b/pallets/runtime/tests/src/asset_pallet/register_ticker.rs @@ -152,7 +152,7 @@ fn register_ticker_too_long() { } #[test] -fn register_ticker_already_expired() { +fn register_ticker_already_registered() { ExtBuilder::default().build().execute_with(|| { let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER00"); let bob = User::new(AccountKeyring::Bob); diff --git a/pallets/runtime/tests/src/asset_pallet/unlink_ticker_from_asset.rs b/pallets/runtime/tests/src/asset_pallet/unlink_ticker_from_asset.rs new file mode 100644 index 0000000000..e3816ca72f --- /dev/null +++ b/pallets/runtime/tests/src/asset_pallet/unlink_ticker_from_asset.rs @@ -0,0 +1,191 @@ +use frame_support::{assert_noop, assert_ok}; +use frame_support::{StorageDoubleMap, StorageMap}; +use sp_keyring::AccountKeyring; + +use pallet_asset::{AssetIDTicker, TickerAssetID, TickersOwnedByUser, UniqueTickerRegistration}; +use polymesh_primitives::agent::AgentGroup; +use polymesh_primitives::{AuthorizationData, Signatory, Ticker}; + +use crate::storage::User; +use crate::{ExtBuilder, TestStorage}; + +type Asset = pallet_asset::Module; +type AssetError = pallet_asset::Error; +type ExternalAgents = pallet_external_agents::Module; +type ExternalAgentsError = pallet_external_agents::Error; +type Identity = pallet_identity::Module; + +#[test] +fn unlink_ticker_from_asset_id_successfully() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_ok!(Asset::unlink_ticker_from_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_eq!(TickerAssetID::get(ticker), None); + assert_eq!(AssetIDTicker::get(asset_id), None); + assert_eq!(UniqueTickerRegistration::::get(ticker), None); + assert_eq!(TickersOwnedByUser::get(alice.did, ticker), false); + }); +} + +#[test] +fn unlink_ticker_from_asset_id_unauthorized_agent() { + ExtBuilder::default().build().execute_with(|| { + let dave = User::new(AccountKeyring::Dave); + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_noop!( + Asset::unlink_ticker_from_asset_id(dave.origin(), ticker, asset_id), + ExternalAgentsError::UnauthorizedAgent + ); + }); +} + +#[test] +fn unlink_ticker_from_asset_id_ticker_registration_not_found() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + let ticker_1: Ticker = Ticker::from_slice_truncated(b"TICKER1"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_noop!( + Asset::unlink_ticker_from_asset_id(alice.origin(), ticker_1, asset_id), + AssetError::TickerRegistrationNotFound + ); + }); +} + +#[test] +fn unlink_ticker_from_asset_id_ticker_not_registered_to_caller() { + ExtBuilder::default().build().execute_with(|| { + let bob = User::new(AccountKeyring::Bob); + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + let auth_id = Identity::add_auth( + alice.did, + Signatory::from(bob.did), + AuthorizationData::BecomeAgent(asset_id, AgentGroup::Full), + None, + ) + .unwrap(); + assert_ok!(ExternalAgents::accept_become_agent(bob.origin(), auth_id)); + + assert_noop!( + Asset::unlink_ticker_from_asset_id(bob.origin(), ticker, asset_id), + AssetError::TickerNotRegisteredToCaller + ); + }); +} + +#[test] +fn unlink_ticker_from_asset_id_ticker_not_linked() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let asset_id = Asset::generate_asset_id(alice.acc(), false); + let ticker: Ticker = Ticker::from_slice_truncated(b"TICKER"); + let ticker_1: Ticker = Ticker::from_slice_truncated(b"TICKER1"); + + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker,)); + assert_ok!(Asset::register_unique_ticker(alice.origin(), ticker_1,)); + + assert_ok!(Asset::create_asset( + alice.origin(), + b"MyAsset".into(), + true, + Default::default(), + Vec::new(), + None, + )); + + assert_ok!(Asset::link_ticker_to_asset_id( + alice.origin(), + ticker, + asset_id + )); + + assert_noop!( + Asset::unlink_ticker_from_asset_id(alice.origin(), ticker_1, asset_id), + AssetError::TickerIsNotLinkedToTheAsset + ); + }); +} diff --git a/pallets/weights/src/pallet_asset.rs b/pallets/weights/src/pallet_asset.rs index 7b56253e2e..1c52d6ab5b 100644 --- a/pallets/weights/src/pallet_asset.rs +++ b/pallets/weights/src/pallet_asset.rs @@ -740,4 +740,26 @@ impl pallet_asset::WeightInfo for SubstrateWeight { .saturating_add(DbWeight::get().reads(7)) .saturating_add(DbWeight::get().writes(3)) } + // Storage: Identity KeyRecords (r:1 w:0) + // Proof Skipped: Identity KeyRecords (max_values: None, max_size: None, mode: Measured) + // Storage: ExternalAgents GroupOfAgent (r:1 w:0) + // Proof Skipped: ExternalAgents GroupOfAgent (max_values: None, max_size: None, mode: Measured) + // Storage: Permissions CurrentPalletName (r:1 w:0) + // Proof Skipped: Permissions CurrentPalletName (max_values: Some(1), max_size: None, mode: Measured) + // Storage: Permissions CurrentDispatchableName (r:1 w:0) + // Proof Skipped: Permissions CurrentDispatchableName (max_values: Some(1), max_size: None, mode: Measured) + // Storage: Asset UniqueTickerRegistration (r:1 w:1) + // Proof Skipped: Asset UniqueTickerRegistration (max_values: None, max_size: None, mode: Measured) + // Storage: Asset TickerAssetID (r:1 w:1) + // Proof Skipped: Asset TickerAssetID (max_values: None, max_size: None, mode: Measured) + // Storage: Asset TickersOwnedByUser (r:0 w:1) + // Proof Skipped: Asset TickersOwnedByUser (max_values: None, max_size: None, mode: Measured) + // Storage: Asset AssetIDTicker (r:0 w:1) + // Proof Skipped: Asset AssetIDTicker (max_values: None, max_size: None, mode: Measured) + fn unlink_ticker_from_asset_id() -> Weight { + // Minimum execution time: 50_587 nanoseconds. + Weight::from_ref_time(53_381_000) + .saturating_add(DbWeight::get().reads(6)) + .saturating_add(DbWeight::get().writes(4)) + } }