From 54bfb3421a89847d58d416058a34289f7178bdb1 Mon Sep 17 00:00:00 2001 From: Gianfranco Tasteri Date: Wed, 29 Nov 2023 17:08:57 -0300 Subject: [PATCH] very basic implementation --- Cargo.lock | 1 + pallets/orml-tokens-extension/Cargo.toml | 6 +- .../src/default_weights.rs | 17 ++ pallets/orml-tokens-extension/src/ext.rs | 19 +- pallets/orml-tokens-extension/src/lib.rs | 220 ++++++++++++++++-- pallets/orml-tokens-extension/src/mock.rs | 173 ++++++++++++++ pallets/orml-tokens-extension/src/tests.rs | 132 +++++++++++ pallets/orml-tokens-extension/src/types.rs | 14 +- 8 files changed, 546 insertions(+), 36 deletions(-) create mode 100644 pallets/orml-tokens-extension/src/default_weights.rs create mode 100644 pallets/orml-tokens-extension/src/mock.rs create mode 100644 pallets/orml-tokens-extension/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 4a5da7b24..b1256d92a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6197,6 +6197,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", + "spacewalk-primitives 1.0.0", ] [[package]] diff --git a/pallets/orml-tokens-extension/Cargo.toml b/pallets/orml-tokens-extension/Cargo.toml index 54a0ca019..586b3a253 100644 --- a/pallets/orml-tokens-extension/Cargo.toml +++ b/pallets/orml-tokens-extension/Cargo.toml @@ -23,6 +23,9 @@ orml-currencies = { git = "https://github.com/open-web3-stack/open-runtime-modul orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.40", default-features = false } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.40", default-features = false } +# Spacewalk libraries +spacewalk-primitives = { git = "https://github.com/pendulum-chain/spacewalk", default-features = false, rev = "d05b0015d15ca39cc780889bcc095335e9862a36"} + [dev-dependencies] mocktopus = "0.8.0" frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40" } @@ -45,7 +48,8 @@ std = [ "orml-currencies/std", "orml-tokens/std", "orml-traits/std", - "frame-benchmarking/std" + "frame-benchmarking/std", + "spacewalk-primitives/std" ] runtime-benchmarks = [ diff --git a/pallets/orml-tokens-extension/src/default_weights.rs b/pallets/orml-tokens-extension/src/default_weights.rs new file mode 100644 index 000000000..98cbf3382 --- /dev/null +++ b/pallets/orml-tokens-extension/src/default_weights.rs @@ -0,0 +1,17 @@ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for `issue`. +pub trait WeightInfo { + +} + +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + +} diff --git a/pallets/orml-tokens-extension/src/ext.rs b/pallets/orml-tokens-extension/src/ext.rs index 1b5cf6c17..4bd057bbd 100644 --- a/pallets/orml-tokens-extension/src/ext.rs +++ b/pallets/orml-tokens-extension/src/ext.rs @@ -7,14 +7,12 @@ pub(crate) mod orml_tokens { use crate::BalanceOf; use crate::CurrencyOf; use crate::AccountIdOf; - use frame_system::Origin as RuntimeOrigin; - use sp_runtime::traits::Zero; use orml_traits::MultiCurrency; pub fn mint( - amount: BalanceOf, + currency_id: CurrencyOf, who: &AccountIdOf, - currency_id: CurrencyOf + amount: BalanceOf, ) -> Result<(), DispatchError> { as MultiCurrency>>::deposit( currency_id, @@ -24,5 +22,18 @@ pub(crate) mod orml_tokens { Ok(()) } + pub fn burn( + currency_id: CurrencyOf, + who: &AccountIdOf, + amount: BalanceOf, + ) -> Result<(), DispatchError> { + as MultiCurrency>>::withdraw( + currency_id, + who, + amount, + )?; + Ok(()) + } + } diff --git a/pallets/orml-tokens-extension/src/lib.rs b/pallets/orml-tokens-extension/src/lib.rs index 387d46934..7e868c067 100644 --- a/pallets/orml-tokens-extension/src/lib.rs +++ b/pallets/orml-tokens-extension/src/lib.rs @@ -1,29 +1,25 @@ -//#![deny(warnings)] +#![deny(warnings)] #![cfg_attr(test, feature(proc_macro_hygiene))] #![cfg_attr(not(feature = "std"), no_std)] #[cfg(test)] extern crate mocktopus; -use frame_support::{dispatch::DispatchResult, ensure}; - #[cfg(test)] use mocktopus::macros::mockable; use orml_traits::MultiCurrency; -use sp_runtime::traits::*; use sp_std::{convert::TryInto, prelude::*, vec}; - +pub use default_weights::{SubstrateWeight, WeightInfo}; // #[cfg(feature = "runtime-benchmarks")] // mod benchmarking; -// #[cfg(test)] -// mod mock; +#[cfg(test)] +mod mock; -// TODO add after definition -//pub mod default_weights; +pub mod default_weights; -// #[cfg(test)] -// mod tests; +#[cfg(test)] +mod tests; mod ext; @@ -40,10 +36,9 @@ pub(crate) type AccountIdOf = ::AccountId; #[frame_support::pallet] pub mod pallet { - // use crate::default_weights::WeightInfo; use frame_support::{pallet_prelude::*, transactional}; - use frame_system::{ensure_root, ensure_signed, pallet_prelude::OriginFor, WeightInfo}; - use crate::types::AssetDetails; + use frame_system::{ ensure_signed, pallet_prelude::OriginFor}; + use crate::types::CurrencyDetails; use super::*; /// ## Configuration @@ -55,29 +50,49 @@ pub mod pallet { /// Weight information for the extrinsics in this module. type WeightInfo: WeightInfo; + + /// Type that allows for checking if currency type is ownable by users + type CurrencyIdChecker: CurrencyIdCheck>; } #[pallet::storage] - #[pallet::getter(fn asset_details)] - pub type PremiumRedeemFee = StorageMap< + #[pallet::getter(fn currency_details)] + pub type CurrencyData = StorageMap< _, Blake2_128Concat, CurrencyOf, - AssetDetails, AccountIdOf>, + CurrencyDetails>, >; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - Mint, - Burn, + /// Some currency was issued. + Mint { currency_id: CurrencyOf, to: AccountIdOf, amount: BalanceOf }, + /// Some currency was burned. + Burned { currency_id: CurrencyOf, from: AccountIdOf, amount: BalanceOf }, + /// Some currency class was created. + Created { currency_id: CurrencyOf, creator: AccountIdOf, owner: AccountIdOf }, + /// Some currency was destroyed (it's data) + Destroyed { currency_id: CurrencyOf }, + /// Change of ownership + OwnershipChanged {currency_id: CurrencyOf, new_owner: AccountIdOf}, + /// Issuer and admin changed + ManagersChanged {currency_id: CurrencyOf, new_admin: AccountIdOf, new_issuer: AccountIdOf} } #[pallet::error] pub enum Error { - NotOwner, + /// Trying to register a new currency when id is in use + AlreadyCreated, + /// Trying to register a currency variant that is not ownable + NotOwnableCurrency, + /// Currency has not been created + NotCreated, + /// No permission to call the operation + NoPermission } #[pallet::pallet] @@ -87,16 +102,177 @@ pub mod pallet { #[pallet::call] impl Pallet { + /// Create and take ownership of one CurrencyId + /// + /// The creator will have full control of this pallelt's functions + /// regarding this currency + /// + /// Parameters: + /// - `currency_id`: Currency id of the Token(u64) variant. + /// + /// Emits `Created` event when successful. + /// + /// Weight: `O(1)` #[pallet::call_index(0)] #[pallet::weight(1)] #[transactional] - pub fn mint(origin: OriginFor, currencies: Vec>) -> DispatchResult { - ensure_root(origin)?; + pub fn create(origin: OriginFor, currency_id: CurrencyOf) -> DispatchResult { + let creator = ensure_signed(origin)?; + + ensure!(T::CurrencyIdChecker::is_valid_currency_id(¤cy_id), Error::::NotOwnableCurrency); + ensure!(!CurrencyData::::contains_key(¤cy_id), Error::::AlreadyCreated); + + CurrencyData::::insert( + currency_id.clone(), + CurrencyDetails { + owner: creator.clone(), + issuer: creator.clone(), + admin: creator.clone(), + }, + ); + + Self::deposit_event(Event::Created { + currency_id, + creator: creator.clone(), + owner: creator, + }); + + Ok(()) + } + + /// Mint currency of a particular class. + /// + /// The origin must be Signed and the sender must be the Issuer of the currency `id`. + /// + /// - `id`: The identifier of the currency to have some amount minted. + /// - `beneficiary`: The account to be credited with the minted currency. + /// - `amount`: The amount of the currency to be minted. + /// + /// Emits `Issued` event when successful. + /// + /// Weight: `O(1)` + #[pallet::call_index(1)] + #[pallet::weight(1)] + #[transactional] + pub fn mint(origin: OriginFor, currency_id: CurrencyOf, to: AccountIdOf, amount: BalanceOf) -> DispatchResult { + let origin = ensure_signed(origin)?; + + // get currency details and check issuer + let currency_data = CurrencyData::::get(currency_id).ok_or(Error::::NotCreated)?; + ensure!(origin == currency_data.issuer, Error::::NoPermission); + + // do mint via orml-currencies + let _ = ext::orml_tokens::mint::(currency_id, &to,amount)?; + + Self::deposit_event(Event::Mint { + currency_id, + to, + amount, + }); + Ok(()) + } + + /// Burn currency of a particular class. + /// + /// The origin must be Signed and the sender must be the Admin of the currency `id`. + /// + /// - `id`: The identifier of the currency to have some amount burned. + /// - `from`: The account to be debited. + /// - `amount`: The amount of the currency to be burned. + /// + /// Emits `Burned` event when successful. + /// + /// Weight: `O(1)` + #[pallet::call_index(2)] + #[pallet::weight(1)] + #[transactional] + pub fn burn(origin: OriginFor, currency_id: CurrencyOf, from: AccountIdOf, amount: BalanceOf) -> DispatchResult { + let origin = ensure_signed(origin)?; + + // get currency details and check admin + let currency_data = CurrencyData::::get(currency_id).ok_or(Error::::NotCreated)?; + ensure!(origin == currency_data.admin, Error::::NoPermission); + + // do burn via orml-currencies + let _ = ext::orml_tokens::burn::(currency_id, &from,amount)?; + Self::deposit_event(Event::Burned { + currency_id, + from, + amount, + }); Ok(()) } + + /// Change the Owner of a currency. + /// + /// Origin must be Signed and the sender should be the Owner of the currency. + /// + /// - `currency_id`: Currency id. + /// - `new_owner`: The new Owner of this currency. + /// + /// Emits `OwnershipChanged`. + /// + /// Weight: `O(1)` + #[pallet::call_index(3)] + #[pallet::weight(1)] + #[transactional] + pub fn transfer_ownership(origin: OriginFor, currency_id: CurrencyOf, new_owner: AccountIdOf) -> DispatchResult { + let origin = ensure_signed(origin)?; + + CurrencyData::::try_mutate(currency_id.clone(), |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::NotCreated)?; + + ensure!(origin == details.owner, Error::::NoPermission); + + if details.owner == new_owner { + return Ok(()) + } + + details.owner = new_owner.clone(); + + Self::deposit_event(Event::OwnershipChanged { currency_id, new_owner }); + Ok(()) + }) + } + + /// Change the Issuer and Admin. + /// + /// Origin must be Signed and the sender should be the Owner of the currency. + /// + /// - `currency_id`: Identifier of the currency. + /// - `issuer`: The new Issuer of this currency. + /// - `admin`: The new Admin of this currency. + /// + /// Emits `ManagersChanged`. + /// + /// Weight: `O(1)` + #[pallet::call_index(4)] + #[pallet::weight(1)] + #[transactional] + pub fn set_managers(origin: OriginFor, currency_id: CurrencyOf, new_admin: AccountIdOf, new_issuer: AccountIdOf) -> DispatchResult { + let origin = ensure_signed(origin)?; + + CurrencyData::::try_mutate(currency_id.clone(), |maybe_details| { + let details = maybe_details.as_mut().ok_or(Error::::NotCreated)?; + + ensure!(origin == details.owner, Error::::NoPermission); + + details.issuer = new_issuer.clone(); + details.admin = new_admin.clone(); + + Self::deposit_event(Event::ManagersChanged { currency_id, new_admin, new_issuer }); + Ok(()) + }) + } + } } +pub trait CurrencyIdCheck { + type CurrencyId; + fn is_valid_currency_id(currency_id: &Self::CurrencyId) -> bool; +} + #[cfg_attr(test, mockable)] impl Pallet {} diff --git a/pallets/orml-tokens-extension/src/mock.rs b/pallets/orml-tokens-extension/src/mock.rs new file mode 100644 index 000000000..b0a55611f --- /dev/null +++ b/pallets/orml-tokens-extension/src/mock.rs @@ -0,0 +1,173 @@ +use crate::{self as orml_tokens_extension, Config}; +use frame_support::{ + parameter_types, + traits::{ConstU32, Everything}, +}; +use orml_currencies::BasicCurrencyAdapter; +use orml_traits::parameter_type_with_key; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; +use crate::CurrencyIdCheck; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Config, Event}, + Tokens: orml_tokens::{Pallet, Storage, Config, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Event}, + Currencies: orml_currencies::{Pallet, Call}, + TokensExtension: orml_tokens_extension::{Pallet, Storage, Call, Event}, + } +); + +pub type AccountId = u64; +pub type Balance = u128; +pub type BlockNumber = u64; +pub type Index = u64; +pub type Amount = i64; +pub use spacewalk_primitives::CurrencyId; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = Index; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = TestEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +pub type TestEvent = RuntimeEvent; + +parameter_types! { + pub const GetCollateralCurrencyId: CurrencyId = CurrencyId::XCM(1); + pub const MaxLocks: u32 = 50; + pub const GetNativeCurrencyId: CurrencyId = CurrencyId::Native; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + 0 + }; +} + +pub struct CurrencyHooks(sp_std::marker::PhantomData); +impl + orml_traits::currency::MutationHooks for CurrencyHooks +{ + type OnDust = orml_tokens::BurnDust; + type OnSlash = (); + type PreDeposit = (); + type PostDeposit = (); + type PreTransfer = (); + type PostTransfer = (); + type OnNewTokenAccount = (); + type OnKilledTokenAccount = (); +} + +impl orml_tokens::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = CurrencyHooks; + type MaxLocks = MaxLocks; + type MaxReserves = ConstU32<0>; + type ReserveIdentifier = (); + type DustRemovalWhitelist = Everything; +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 1000; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Test { + type MaxLocks = MaxLocks; + /// The type for recording an account's balance. + type Balance = Balance; + /// The ubiquitous event type. + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight; + type MaxReserves = MaxReserves; + type ReserveIdentifier = (); +} + +impl orml_currencies::Config for Test { + type MultiCurrency = Tokens; + type NativeCurrency = BasicCurrencyAdapter; + type GetNativeCurrencyId = GetNativeCurrencyId; + type WeightInfo = (); +} + +pub struct CurrencyIdCheckerImpl; + +impl CurrencyIdCheck for CurrencyIdCheckerImpl { + type CurrencyId = CurrencyId; + + fn is_valid_currency_id(currency_id: &Self::CurrencyId) -> bool { + matches!(currency_id, CurrencyId::Token(_)) + } +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = crate::SubstrateWeight; + type CurrencyIdChecker = CurrencyIdCheckerImpl; +} + +pub struct ExtBuilder; + +impl ExtBuilder { + pub fn build() -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + sp_io::TestExternalities::from(storage) + } +} + +pub fn run_test(test: T) +where + T: FnOnce(), +{ + ExtBuilder::build().execute_with(|| { + System::set_block_number(1); + test(); + }); +} diff --git a/pallets/orml-tokens-extension/src/tests.rs b/pallets/orml-tokens-extension/src/tests.rs new file mode 100644 index 000000000..cae9456d3 --- /dev/null +++ b/pallets/orml-tokens-extension/src/tests.rs @@ -0,0 +1,132 @@ +use frame_support::{assert_ok, assert_err}; +use orml_traits::MultiCurrency; +use crate::{mock::*, Error, AccountIdOf}; +use crate::types::CurrencyDetails; +use spacewalk_primitives::CurrencyId; + +fn get_balance(currency_id: CurrencyId, account: &AccountId) -> Balance { + as MultiCurrency>::free_balance( + currency_id, + account, + ) +} + +fn get_total_issuance(currency_id: CurrencyId) -> Balance { + as MultiCurrency>::total_issuance( + currency_id + ) +} + +#[test] +fn can_create_currency_and_mint() { + run_test(|| { + let amount_minted= 10; + let beneficiary_id = 1; + let owner_id = 0; + assert_ok!(crate::Pallet::::create( + RuntimeOrigin::signed(owner_id), + CurrencyId::Token(1), + )); + + assert_ok!(crate::Pallet::::mint( + RuntimeOrigin::signed(owner_id), + CurrencyId::Token(1), + beneficiary_id, + amount_minted + )); + + assert_eq!(get_balance(CurrencyId::Token(1), &beneficiary_id), amount_minted); + assert_eq!(get_total_issuance(CurrencyId::Token(1)),amount_minted); + + }) +} + +#[test] +fn cannot_mint_if_not_owner() { + run_test(|| { + let amount_minted= 10; + + let owner_id = 0; + let beneficiary_id = 1; + let not_owner_id = 2; + assert_ok!(crate::Pallet::::create( + RuntimeOrigin::signed(owner_id), + CurrencyId::Token(1), + )); + + assert_err!(crate::Pallet::::mint( + RuntimeOrigin::signed(not_owner_id), + CurrencyId::Token(1), + beneficiary_id, + amount_minted + ),Error::::NoPermission); + + }) +} + +#[test] +fn cannot_create_invalid_currency() { + run_test(|| { + let owner_id = 0; + assert_err!(crate::Pallet::::create( + RuntimeOrigin::signed(owner_id), + CurrencyId::XCM(1), + ), Error::::NotOwnableCurrency); + + }) +} + +#[test] +fn can_mint_and_burn() { + run_test(|| { + let amount_minted= 10; + let amount_burned= 5; + let beneficiary_id = 1; + let owner_id = 0; + assert_ok!(crate::Pallet::::create( + RuntimeOrigin::signed(owner_id), + CurrencyId::Token(1), + )); + + assert_ok!(crate::Pallet::::mint( + RuntimeOrigin::signed(owner_id), + CurrencyId::Token(1), + beneficiary_id, + amount_minted + )); + + assert_ok!(crate::Pallet::::burn( + RuntimeOrigin::signed(owner_id), + CurrencyId::Token(1), + beneficiary_id, + amount_burned + )); + + assert_eq!(get_balance(CurrencyId::Token(1), &beneficiary_id), (amount_minted-amount_burned)); + assert_eq!(get_total_issuance(CurrencyId::Token(1)),(amount_minted-amount_burned)); + + }) +} + +#[test] +fn can_change_ownership() { + run_test(|| { + let creator_id = 0; + let new_owner_id = 2; + assert_ok!(crate::Pallet::::create( + RuntimeOrigin::signed(creator_id), + CurrencyId::Token(1), + )); + + assert_ok!(crate::Pallet::::transfer_ownership( + RuntimeOrigin::signed(creator_id), + CurrencyId::Token(1), + new_owner_id + )); + + assert_eq!(crate::Pallet::::currency_details( + CurrencyId::Token(1) + ), Some(CurrencyDetails::> {owner:new_owner_id, issuer: creator_id, admin: creator_id })); + + }) +} \ No newline at end of file diff --git a/pallets/orml-tokens-extension/src/types.rs b/pallets/orml-tokens-extension/src/types.rs index 75c170df6..bd69c31c2 100644 --- a/pallets/orml-tokens-extension/src/types.rs +++ b/pallets/orml-tokens-extension/src/types.rs @@ -1,17 +1,13 @@ -use codec::{Decode, Encode, HasCompact, MaxEncodedLen}; +use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -#[derive(Encode, Decode, MaxEncodedLen, TypeInfo)] -pub struct AssetDetails { - /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq)] +pub struct CurrencyDetails { + /// Can change `owner`, `issuer` and `admin` accounts. pub(super) owner: AccountId, /// Can mint tokens. pub(super) issuer: AccountId, - /// Can thaw tokens, force transfers and burn tokens from any account. + /// Can burn tokens from any account. pub(super) admin: AccountId, - /// Can freeze tokens. - pub(super) freezer: AccountId, - /// The total supply across all accounts. - pub(super) supply: Balance, } \ No newline at end of file