From b3dd034cce96f7684fe0a89989ba47da04d6e1e5 Mon Sep 17 00:00:00 2001 From: Xiliang Chen Date: Fri, 12 Jul 2019 02:36:48 +1200 Subject: [PATCH] GenericAsset module (#2790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added generic asset module * Update srml/generic-asset/Cargo.toml * Update srml/generic-asset/Cargo.toml * revert Cargo.lock changes * cleanup line width * rename srml_support to support * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * Update srml/generic-asset/src/lib.rs Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com> * update comments * more docs * update Cargo.lock * fix tests * update headers and fix missing events * Update srml/generic-asset/Cargo.toml Co-Authored-By: Bastian Köcher * Update srml/generic-asset/src/lib.rs Co-Authored-By: Bastian Köcher * Update srml/generic-asset/src/lib.rs Co-Authored-By: Bastian Köcher * Update srml/generic-asset/src/lib.rs Co-Authored-By: Bastian Köcher * Update srml/generic-asset/src/lib.rs Co-Authored-By: Bastian Köcher * Apply suggestions from code review Co-Authored-By: Bastian Köcher * fix issues * avoid duplicated code * fix build issue * cleanup Cargo.toml * update based on reviews * remove not used method * update parity-codec version * fix test build issue * update Cargo.lock * update with latest substrate changes * refactor PermissionVersions encoding --- substrate/Cargo.lock | 14 + substrate/Cargo.toml | 1 + substrate/srml/generic-asset/Cargo.toml | 28 + substrate/srml/generic-asset/src/lib.rs | 1286 ++++++++++++++++++++ substrate/srml/generic-asset/src/mock.rs | 140 +++ substrate/srml/generic-asset/src/tests.rs | 1288 +++++++++++++++++++++ 6 files changed, 2757 insertions(+) create mode 100644 substrate/srml/generic-asset/Cargo.toml create mode 100644 substrate/srml/generic-asset/src/lib.rs create mode 100644 substrate/srml/generic-asset/src/mock.rs create mode 100644 substrate/srml/generic-asset/src/tests.rs diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index f389625eae..4e3f5e1630 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -3763,6 +3763,20 @@ dependencies = [ "substrate-primitives 2.0.0", ] +[[package]] +name = "srml-generic-asset" +version = "2.0.0" +dependencies = [ + "parity-codec 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-io 2.0.0", + "sr-primitives 2.0.0", + "sr-std 2.0.0", + "srml-support 2.0.0", + "srml-system 2.0.0", + "substrate-primitives 2.0.0", +] + [[package]] name = "srml-grandpa" version = "2.0.0" diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml index 7790c4127a..b8b6311a30 100644 --- a/substrate/Cargo.toml +++ b/substrate/Cargo.toml @@ -75,6 +75,7 @@ members = [ "srml/example", "srml/executive", "srml/finality-tracker", + "srml/generic-asset", "srml/grandpa", "srml/indices", "srml/metadata", diff --git a/substrate/srml/generic-asset/Cargo.toml b/substrate/srml/generic-asset/Cargo.toml new file mode 100644 index 0000000000..bfa49d8018 --- /dev/null +++ b/substrate/srml/generic-asset/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "srml-generic-asset" +version = "2.0.0" +authors = ["Centrality Developers "] +edition = "2018" + +[dependencies] +serde = { version = "1.0", optional = true } +parity-codec = { version = "4.1.1", default-features = false, features = ["derive"] } +rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } +primitives = { package = "sr-primitives", path = "../../core/sr-primitives", default-features = false } +support = { package = "srml-support", path = "../support", default-features = false } +system = { package = "srml-system", path = "../system", default-features = false } + +[dev-dependencies] +runtime_io = { package = "sr-io", path = "../../core/sr-io" } +substrate-primitives = { path = "../../core/primitives" } + +[features] +default = ["std"] +std =[ + "serde/std", + "parity-codec/std", + "rstd/std", + "primitives/std", + "support/std", + "system/std", +] diff --git a/substrate/srml/generic-asset/src/lib.rs b/substrate/srml/generic-asset/src/lib.rs new file mode 100644 index 0000000000..60370600a6 --- /dev/null +++ b/substrate/srml/generic-asset/src/lib.rs @@ -0,0 +1,1286 @@ +// Copyright 2019 +// by Centrality Investments Ltd. +// and Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! # Generic Asset Module +//! +//! The Generic Asset module provides functionality for handling accounts and asset balances. +//! +//! ## Overview +//! +//! The Generic Asset module provides functions for: +//! +//! - Creating a new kind of asset. +//! - Setting permissions of an asset. +//! - Getting and setting free balances. +//! - Retrieving total, reserved and unreserved balances. +//! - Repatriating a reserved balance to a beneficiary account. +//! - Transferring a balance between accounts (when not reserved). +//! - Slashing an account balance. +//! - Managing total issuance. +//! - Setting and managing locks. +//! +//! ### Terminology +//! +//! - **Staking Asset:** The asset for staking, to participate as Validators in the network. +//! - **Spending Asset:** The asset for payment, such as paying transfer fees, gas fees, etc. +//! - **Permissions:** A set of rules for a kind of asset, defining the allowed operations to the asset, and which +//! accounts are allowed to possess it. +//! - **Total Issuance:** The total number of units in existence in a system. +//! - **Free Balance:** The portion of a balance that is not reserved. The free balance is the only balance that matters +//! for most operations. When this balance falls below the existential deposit, most functionality of the account is +//! removed. When both it and the reserved balance are deleted, then the account is said to be dead. +//! - **Reserved Balance:** Reserved balance still belongs to the account holder, but is suspended. Reserved balance +//! can still be slashed, but only after all the free balance has been slashed. If the reserved balance falls below the +//! existential deposit then it and any related functionality will be deleted. When both it and the free balance are +//! deleted, then the account is said to be dead. +//! - **Imbalance:** A condition when some assets were credited or debited without equal and opposite accounting +//! (i.e. a difference between total issuance and account balances). Functions that result in an imbalance will +//! return an object of the `Imbalance` trait that can be managed within your runtime logic. (If an imbalance is +//! simply dropped, it should automatically maintain any book-keeping such as total issuance.) +//! - **Lock:** A freeze on a specified amount of an account's free balance until a specified block number. Multiple +//! locks always operate over the same funds, so they "overlay" rather than "stack". +//! +//! ### Implementations +//! +//! The Generic Asset module provides `AssetCurrency`, which implements the following traits. If these traits provide +//! the functionality that you need, you can avoid coupling with the Generic Asset module. +//! +//! - `Currency`: Functions for dealing with a fungible assets system. +//! - `ReservableCurrency`: Functions for dealing with assets that can be reserved from an account. +//! - `LockableCurrency`: Functions for dealing with accounts that allow liquidity restrictions. +//! - `Imbalance`: Functions for handling imbalances between total issuance in the system and account balances. +//! Must be used when a function creates new assets (e.g. a reward) or destroys some assets (e.g. a system fee). +//! +//! The Generic Asset module provides two types of `AssetCurrency` as follows. +//! +//! - `StakingAssetCurrency`: Currency for staking. +//! - `SpendingAssetCurrency`: Currency for payments such as transfer fee, gas fee. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! - `create`: Create a new kind of asset. +//! - `transfer`: Transfer some liquid free balance to another account. +//! - `update_permission`: Updates permission for a given `asset_id` and an account. The origin of this call +//! must have update permissions. +//! - `mint`: Mint an asset, increases its total issuance. The origin of this call must have mint permissions. +//! - `burn`: Burn an asset, decreases its total issuance. The origin of this call must have burn permissions. +//! - `create_reserved`: Create a new kind of reserved asset. The origin of this call must be root. +//! +//! ### Public Functions +//! +//! - `total_balance`: Get an account's total balance of an asset kind. +//! - `free_balance`: Get an account's free balance of an asset kind. +//! - `reserved_balance`: Get an account's reserved balance of an asset kind. +//! - `create_asset`: Creates an asset. +//! - `make_transfer`: Transfer some liquid free balance from one account to another. +//! This will not emit the `Transferred` event. +//! - `make_transfer_with_event`: Transfer some liquid free balance from one account to another. +//! This will emit the `Transferred` event. +//! - `reserve`: Moves an amount from free balance to reserved balance. +//! - `unreserve`: Move up to an amount from reserved balance to free balance. This function cannot fail. +//! - `slash`: Deduct up to an amount from the combined balance of `who`, preferring to deduct from the +//! free balance. This function cannot fail. +//! - `reward`: Add up to an amount to the free balance of an account. +//! - `slash_reserved`: Deduct up to an amount from reserved balance of an account. This function cannot fail. +//! - `repatriate_reserved`: Move up to an amount from reserved balance of an account to free balance of another +//! account. +//! - `check_permission`: Check permission to perform burn, mint or update. +//! - `ensure_can_withdraw`: Check if the account is able to make a withdrawal of the given amount +//! for the given reason. +//! +//! ### Usage +//! +//! The following examples show how to use the Generic Asset module in your custom module. +//! +//! ### Examples from the PRML +//! +//! The Fees module uses the `Currency` trait to handle fee charge/refund, and its types inherit from `Currency`: +//! +//! ``` +//! use support::{ +//! traits::{Currency, ExistenceRequirement, WithdrawReason}, +//! dispatch::Result, +//! }; +//! # pub trait Trait: system::Trait { +//! # type Currency: Currency; +//! # } +//! type AssetOf = <::Currency as Currency<::AccountId>>::Balance; +//! +//! fn charge_fee(transactor: &T::AccountId, amount: AssetOf) -> Result { +//! // ... +//! T::Currency::withdraw( +//! transactor, +//! amount, +//! WithdrawReason::TransactionPayment, +//! ExistenceRequirement::KeepAlive, +//! )?; +//! // ... +//! Ok(()) +//! } +//! +//! fn refund_fee(transactor: &T::AccountId, amount: AssetOf) -> Result { +//! // ... +//! T::Currency::deposit_into_existing(transactor, amount)?; +//! // ... +//! Ok(()) +//! } +//! +//! # fn main() {} +//! ``` +//! +//! ## Genesis config +//! +//! The Generic Asset module depends on the [`GenesisConfig`](./struct.GenesisConfig.html). + +#![cfg_attr(not(feature = "std"), no_std)] + +use parity_codec::{Decode, Encode, HasCompact, Input, Output}; + +use primitives::traits::{ + CheckedAdd, CheckedSub, MaybeSerializeDebug, Member, One, Saturating, SimpleArithmetic, Zero, Bounded +}; + +use rstd::prelude::*; +use rstd::{cmp, result}; +use support::dispatch::Result; +use support::{ + decl_event, decl_module, decl_storage, ensure, + traits::{ + Currency, ExistenceRequirement, Imbalance, LockIdentifier, LockableCurrency, ReservableCurrency, + SignedImbalance, UpdateBalanceOutcome, WithdrawReason, WithdrawReasons, + }, + Parameter, StorageDoubleMap, StorageMap, StorageValue, +}; +use system::{ensure_signed, ensure_root}; + +mod mock; +mod tests; + +pub use self::imbalances::{NegativeImbalance, PositiveImbalance}; + +pub trait Trait: system::Trait { + type Balance: Parameter + + Member + + SimpleArithmetic + + Default + + Copy + + MaybeSerializeDebug; + type AssetId: Parameter + Member + SimpleArithmetic + Default + Copy; + type Event: From> + Into<::Event>; +} + +pub trait Subtrait: system::Trait { + type Balance: Parameter + + Member + + SimpleArithmetic + + Default + + Copy + + MaybeSerializeDebug; + type AssetId: Parameter + Member + SimpleArithmetic + Default + Copy; +} + +impl Subtrait for T { + type Balance = T::Balance; + type AssetId = T::AssetId; +} + +/// Asset creation options. +#[cfg_attr(feature = "std", derive(Debug))] +#[derive(Clone, Encode, Decode, PartialEq, Eq)] +pub struct AssetOptions { + /// Initial issuance of this asset. All deposit to the creater of the asset. + #[codec(compact)] + pub initial_issuance: Balance, + /// Which accounts are allowed to possess this asset. + pub permissions: PermissionLatest, +} + +/// Owner of an asset. +#[cfg_attr(feature = "std", derive(Debug))] +#[derive(Clone, Encode, Decode, PartialEq, Eq)] +pub enum Owner { + /// No owner. + None, + /// Owned by an AccountId + Address(AccountId), +} + +impl Default for Owner { + fn default() -> Self { + Owner::None + } +} + +/// Asset permissions +#[cfg_attr(feature = "std", derive(Debug))] +#[derive(Clone, Encode, Decode, PartialEq, Eq)] +pub struct PermissionsV1 { + /// Who have permission to update asset permission + pub update: Owner, + /// Who have permission to mint new asset + pub mint: Owner, + /// Who have permission to burn asset + pub burn: Owner, +} + +#[cfg_attr(feature = "std", derive(Debug))] +#[derive(Clone, Encode, Decode, PartialEq, Eq)] +#[repr(u8)] +enum PermissionVersionNumber { + V1 = 0, +} + +/// Versioned asset permission +#[cfg_attr(feature = "std", derive(Debug))] +#[derive(Clone, PartialEq, Eq)] +pub enum PermissionVersions { + V1(PermissionsV1), +} + +/// Asset permission types +pub enum PermissionType { + /// Permission to update asset permission + Burn, + /// Permission to mint new asset + Mint, + /// Permission to burn asset + Update, +} + +/// Alias to latest asset permissions +pub type PermissionLatest = PermissionsV1; + +impl Default for PermissionVersions { + fn default() -> Self { + PermissionVersions::V1(Default::default()) + } +} + +impl Encode for PermissionVersions { + fn encode_to(&self, dest: &mut T) { + match self { + PermissionVersions::V1(payload) => { + dest.push(&PermissionVersionNumber::V1); + dest.push(payload); + }, + } + } +} + +impl Decode for PermissionVersions { + fn decode(input: &mut I) -> Option { + let version = PermissionVersionNumber::decode(input)?; + Some( + match version { + PermissionVersionNumber::V1 => PermissionVersions::V1(Decode::decode(input)?) + } + ) + } +} + +impl Default for PermissionsV1 { + fn default() -> Self { + PermissionsV1 { + update: Owner::None, + mint: Owner::None, + burn: Owner::None, + } + } +} + +impl Into> for PermissionVersions { + fn into(self) -> PermissionLatest { + match self { + PermissionVersions::V1(v1) => v1, + } + } +} + +/// Converts the latest permission to other version. +impl Into> for PermissionLatest { + fn into(self) -> PermissionVersions { + PermissionVersions::V1(self) + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + fn deposit_event() = default; + + /// Create a new kind of asset. + fn create(origin, options: AssetOptions) -> Result { + let origin = ensure_signed(origin)?; + let id = Self::next_asset_id(); + + let permissions: PermissionVersions = options.permissions.clone().into(); + + // The last available id serves as the overflow mark and won't be used. + let next_id = id.checked_add(&One::one()).ok_or_else(|| "No new assets id available.")?; + + >::put(next_id); + >::insert(id, &options.initial_issuance); + >::insert(&id, &origin, options.initial_issuance); + >::insert(&id, permissions); + + Self::deposit_event(RawEvent::Created(id, origin, options)); + + Ok(()) + } + + /// Transfer some liquid free balance to another account. + pub fn transfer(origin, #[compact] asset_id: T::AssetId, to: T::AccountId, #[compact] amount: T::Balance) { + let origin = ensure_signed(origin)?; + ensure!(!amount.is_zero(), "cannot transfer zero amount"); + Self::make_transfer_with_event(&asset_id, &origin, &to, amount)?; + } + + /// Updates permission for a given `asset_id` and an account. + /// + /// The `origin` must have `update` permission. + fn update_permission( + origin, + #[compact] asset_id: T::AssetId, + new_permission: PermissionLatest + ) -> Result { + let origin = ensure_signed(origin)?; + + let permissions: PermissionVersions = new_permission.into(); + + if Self::check_permission(&asset_id, &origin, &PermissionType::Update) { + >::insert(asset_id, &permissions); + + Self::deposit_event(RawEvent::PermissionUpdated(asset_id, permissions.into())); + + Ok(()) + } else { + Err("Origin does not have enough permission to update permissions.") + } + } + + /// Mints an asset, increases its total issuance. + /// The origin must have `mint` permissions. + fn mint(origin, #[compact] asset_id: T::AssetId, to: T::AccountId, amount: T::Balance) -> Result { + let origin = ensure_signed(origin)?; + if Self::check_permission(&asset_id, &origin, &PermissionType::Mint) { + let original_free_balance = Self::free_balance(&asset_id, &to); + let current_total_issuance = >::get(asset_id); + let new_total_issuance = current_total_issuance.checked_add(&amount) + .ok_or_else(|| "total_issuance got overflow after minting.")?; + let value = original_free_balance.checked_add(&amount) + .ok_or_else(|| "free balance got overflow after minting.")?; + + >::insert(asset_id, new_total_issuance); + Self::set_free_balance(&asset_id, &to, value); + + Self::deposit_event(RawEvent::Minted(asset_id, to, amount)); + + Ok(()) + } else { + Err("The origin does not have permission to mint an asset.") + } + } + + /// Burns an asset, decreases its total issuance. + /// + /// The `origin` must have `burn` permissions. + fn burn(origin, #[compact] asset_id: T::AssetId, to: T::AccountId, amount: T::Balance) -> Result { + let origin = ensure_signed(origin)?; + + if Self::check_permission(&asset_id, &origin, &PermissionType::Burn) { + let original_free_balance = Self::free_balance(&asset_id, &to); + + let current_total_issuance = >::get(asset_id); + let new_total_issuance = current_total_issuance.checked_sub(&amount) + .ok_or_else(|| "total_issuance got underflow after burning")?; + let value = original_free_balance.checked_sub(&amount) + .ok_or_else(|| "free_balance got underflow after burning")?; + + >::insert(asset_id, new_total_issuance); + + Self::set_free_balance(&asset_id, &to, value); + + Self::deposit_event(RawEvent::Burned(asset_id, to, amount)); + + Ok(()) + } else { + Err("The origin does not have permission to burn an asset.") + } + } + + /// Can be used to create reserved tokens. + /// Requires Root call. + fn create_reserved(origin, asset_id: T::AssetId, options: AssetOptions) -> Result { + ensure_root(origin)?; + Self::create_asset(Some(asset_id), None, options) + } + } +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct BalanceLock { + pub id: LockIdentifier, + pub amount: Balance, + pub until: BlockNumber, + pub reasons: WithdrawReasons, +} + +decl_storage! { + trait Store for Module as GenericAsset { + /// Total issuance of a given asset. + pub TotalIssuance get(total_issuance) build(|config: &GenesisConfig| { + let issuance = config.initial_balance * (config.endowed_accounts.len() as u32).into(); + config.assets.iter().map(|id| (id.clone(), issuance)).collect::>() + }): map T::AssetId => T::Balance; + + /// The free balance of a given asset under an account. + pub FreeBalance: double_map T::AssetId, twox_128(T::AccountId) => T::Balance; + + /// The reserved balance of a given asset under an account. + pub ReservedBalance: double_map T::AssetId, twox_128(T::AccountId) => T::Balance; + + /// Next available ID for user-created asset. + pub NextAssetId get(next_asset_id) config(): T::AssetId; + + /// Permission options for a given asset. + pub Permissions get(get_permission): map T::AssetId => PermissionVersions; + + /// Any liquidity locks on some account balances. + pub Locks get(locks): map T::AccountId => Vec>; + + /// The identity of the asset which is the one that is designated for the chain's staking system. + pub StakingAssetId get(staking_asset_id) config(): T::AssetId; + + /// The identity of the asset which is the one that is designated for paying the chain's transaction fee. + pub SpendingAssetId get(spending_asset_id) config(): T::AssetId; + } + add_extra_genesis { + config(assets): Vec; + config(initial_balance): T::Balance; + config(endowed_accounts): Vec; + + build(| + storage: &mut primitives::StorageOverlay, + _: &mut primitives::ChildrenStorageOverlay, + config: &GenesisConfig| { + config.assets.iter().for_each(|asset_id| { + config.endowed_accounts.iter().for_each(|account_id| { + storage.insert( + >::key_for(asset_id, account_id), + ::encode(&config.initial_balance) + ); + }); + }); + }); + } +} + +decl_event!( + pub enum Event where + ::AccountId, + ::Balance, + ::AssetId, + AssetOptions = AssetOptions<::Balance, ::AccountId> + { + /// Asset created (asset_id, creator, asset_options). + Created(AssetId, AccountId, AssetOptions), + /// Asset transfer succeeded (asset_id, from, to, amount). + Transferred(AssetId, AccountId, AccountId, Balance), + /// Asset permission updated (asset_id, new_permissions). + PermissionUpdated(AssetId, PermissionLatest), + /// New asset minted (asset_id, account, amount). + Minted(AssetId, AccountId, Balance), + /// Asset burned (asset_id, account, amount). + Burned(AssetId, AccountId, Balance), + } +); + +impl Module { + // PUBLIC IMMUTABLES + + /// Get an account's total balance of an asset kind. + pub fn total_balance(asset_id: &T::AssetId, who: &T::AccountId) -> T::Balance { + Self::free_balance(asset_id, who) + Self::reserved_balance(asset_id, who) + } + + /// Get an account's free balance of an asset kind. + pub fn free_balance(asset_id: &T::AssetId, who: &T::AccountId) -> T::Balance { + >::get(asset_id, who) + } + + /// Get an account's reserved balance of an asset kind. + pub fn reserved_balance(asset_id: &T::AssetId, who: &T::AccountId) -> T::Balance { + >::get(asset_id, who) + } + + /// Creates an asset. + /// + /// # Arguments + /// * `asset_id`: An ID of a reserved asset. + /// If not provided, a user-generated asset will be created with the next available ID. + /// * `from_account`: The initiator account of this call + /// * `asset_options`: Asset creation options. + /// + pub fn create_asset( + asset_id: Option, + from_account: Option, + options: AssetOptions, + ) -> Result { + let asset_id = if let Some(asset_id) = asset_id { + ensure!(!>::exists(&asset_id), "Asset id already taken."); + ensure!(asset_id < Self::next_asset_id(), "Asset id not available."); + asset_id + } else { + let asset_id = Self::next_asset_id(); + let next_id = asset_id + .checked_add(&One::one()) + .ok_or_else(|| "No new user asset id available.")?; + >::put(next_id); + asset_id + }; + + let account_id = from_account.unwrap_or_default(); + let permissions: PermissionVersions = options.permissions.clone().into(); + + >::insert(asset_id, &options.initial_issuance); + >::insert(&asset_id, &account_id, options.initial_issuance); + >::insert(&asset_id, permissions); + + Self::deposit_event(RawEvent::Created(asset_id, account_id, options)); + + Ok(()) + } + + /// Transfer some liquid free balance from one account to another. + /// This will not emit the `Transferred` event. + pub fn make_transfer(asset_id: &T::AssetId, from: &T::AccountId, to: &T::AccountId, amount: T::Balance) -> Result { + let new_balance = Self::free_balance(asset_id, from) + .checked_sub(&amount) + .ok_or_else(|| "balance too low to send amount")?; + Self::ensure_can_withdraw(asset_id, from, amount, WithdrawReason::Transfer, new_balance)?; + + if from != to { + >::mutate(asset_id, from, |balance| *balance -= amount); + >::mutate(asset_id, to, |balance| *balance += amount); + } + + Ok(()) + } + + /// Transfer some liquid free balance from one account to another. + /// This will emit the `Transferred` event. + pub fn make_transfer_with_event( + asset_id: &T::AssetId, + from: &T::AccountId, + to: &T::AccountId, + amount: T::Balance, + ) -> Result { + Self::make_transfer(asset_id, from, to, amount)?; + + if from != to { + Self::deposit_event(RawEvent::Transferred(*asset_id, from.clone(), to.clone(), amount)); + } + + Ok(()) + } + + /// Move `amount` from free balance to reserved balance. + /// + /// If the free balance is lower than `amount`, then no funds will be moved and an `Err` will + /// be returned. This is different behavior than `unreserve`. + pub fn reserve(asset_id: &T::AssetId, who: &T::AccountId, amount: T::Balance) -> Result { + // Do we need to consider that this is an atomic transaction? + let original_reserve_balance = Self::reserved_balance(asset_id, who); + let original_free_balance = Self::free_balance(asset_id, who); + if original_free_balance < amount { + return Err("not enough free funds"); + } + let new_reserve_balance = original_reserve_balance + amount; + Self::set_reserved_balance(asset_id, who, new_reserve_balance); + let new_free_balance = original_free_balance - amount; + Self::set_free_balance(asset_id, who, new_free_balance); + Ok(()) + } + + /// Moves up to `amount` from reserved balance to free balance. This function cannot fail. + /// + /// As many assets up to `amount` will be moved as possible. If the reserve balance of `who` + /// is less than `amount`, then the remaining amount will be returned. + /// NOTE: This is different behavior than `reserve`. + pub fn unreserve(asset_id: &T::AssetId, who: &T::AccountId, amount: T::Balance) -> T::Balance { + let b = Self::reserved_balance(asset_id, who); + let actual = rstd::cmp::min(b, amount); + let original_free_balance = Self::free_balance(asset_id, who); + let new_free_balance = original_free_balance + actual; + Self::set_free_balance(asset_id, who, new_free_balance); + Self::set_reserved_balance(asset_id, who, b - actual); + amount - actual + } + + /// Deduct up to `amount` from the combined balance of `who`, preferring to deduct from the + /// free balance. This function cannot fail. + /// + /// As much funds up to `amount` will be deducted as possible. If this is less than `amount` + /// then `Some(remaining)` will be returned. Full completion is given by `None`. + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + fn slash(asset_id: &T::AssetId, who: &T::AccountId, amount: T::Balance) -> Option { + let free_balance = Self::free_balance(asset_id, who); + let free_slash = rstd::cmp::min(free_balance, amount); + let new_free_balance = free_balance - free_slash; + Self::set_free_balance(asset_id, who, new_free_balance); + if free_slash < amount { + Self::slash_reserved(asset_id, who, amount - free_slash) + } else { + None + } + } + + /// Deducts up to `amount` from reserved balance of `who`. This function cannot fail. + /// + /// As much funds up to `amount` will be deducted as possible. If the reserve balance of `who` + /// is less than `amount`, then a non-zero second item will be returned. + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + fn slash_reserved(asset_id: &T::AssetId, who: &T::AccountId, amount: T::Balance) -> Option { + let original_reserve_balance = Self::reserved_balance(asset_id, who); + let slash = rstd::cmp::min(original_reserve_balance, amount); + let new_reserve_balance = original_reserve_balance - slash; + Self::set_reserved_balance(asset_id, who, new_reserve_balance); + if amount == slash { + None + } else { + Some(amount - slash) + } + } + + /// Move up to `amount` from reserved balance of account `who` to free balance of account + /// `beneficiary`. + /// + /// As much funds up to `amount` will be moved as possible. If this is less than `amount`, then + /// the `remaining` would be returned, else `Zero::zero()`. + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + fn repatriate_reserved( + asset_id: &T::AssetId, + who: &T::AccountId, + beneficiary: &T::AccountId, + amount: T::Balance, + ) -> T::Balance { + let b = Self::reserved_balance(asset_id, who); + let slash = rstd::cmp::min(b, amount); + + let original_free_balance = Self::free_balance(asset_id, beneficiary); + let new_free_balance = original_free_balance + slash; + Self::set_free_balance(asset_id, beneficiary, new_free_balance); + + let new_reserve_balance = b - slash; + Self::set_reserved_balance(asset_id, who, new_reserve_balance); + amount - slash + } + + /// Check permission to perform burn, mint or update. + /// + /// # Arguments + /// * `asset_id`: A `T::AssetId` type that contains the `asset_id`, which has the permission embedded. + /// * `who`: A `T::AccountId` type that contains the `account_id` for which to check permissions. + /// * `what`: The permission to check. + /// + pub fn check_permission(asset_id: &T::AssetId, who: &T::AccountId, what: &PermissionType) -> bool { + let permission_versions: PermissionVersions = Self::get_permission(asset_id); + let permission = permission_versions.into(); + + match (what, permission) { + ( + PermissionType::Burn, + PermissionLatest { + burn: Owner::Address(account), + .. + }, + ) => account == *who, + ( + PermissionType::Mint, + PermissionLatest { + mint: Owner::Address(account), + .. + }, + ) => account == *who, + ( + PermissionType::Update, + PermissionLatest { + update: Owner::Address(account), + .. + }, + ) => account == *who, + _ => false, + } + } + + /// Return `Ok` iff the account is able to make a withdrawal of the given amount + /// for the given reason. + /// + /// `Err(...)` with the reason why not otherwise. + pub fn ensure_can_withdraw( + asset_id: &T::AssetId, + who: &T::AccountId, + _amount: T::Balance, + reason: WithdrawReason, + new_balance: T::Balance, + ) -> Result { + if asset_id != &Self::staking_asset_id() { + return Ok(()); + } + + let locks = Self::locks(who); + if locks.is_empty() { + return Ok(()); + } + let now = >::block_number(); + if Self::locks(who) + .into_iter() + .all(|l| now >= l.until || new_balance >= l.amount || !l.reasons.contains(reason)) + { + Ok(()) + } else { + Err("account liquidity restrictions prevent withdrawal") + } + } + + // PRIVATE MUTABLES + + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + fn set_reserved_balance(asset_id: &T::AssetId, who: &T::AccountId, balance: T::Balance) { + >::insert(asset_id, who, balance); + } + + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + fn set_free_balance(asset_id: &T::AssetId, who: &T::AccountId, balance: T::Balance) { + >::insert(asset_id, who, balance); + } + + fn set_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: T::Balance, + until: T::BlockNumber, + reasons: WithdrawReasons, + ) { + let now = >::block_number(); + let mut new_lock = Some(BalanceLock { + id, + amount, + until, + reasons, + }); + let mut locks = >::locks(who) + .into_iter() + .filter_map(|l| { + if l.id == id { + new_lock.take() + } else if l.until > now { + Some(l) + } else { + None + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + >::insert(who, locks); + } + + fn extend_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: T::Balance, + until: T::BlockNumber, + reasons: WithdrawReasons, + ) { + let now = >::block_number(); + let mut new_lock = Some(BalanceLock { + id, + amount, + until, + reasons, + }); + let mut locks = >::locks(who) + .into_iter() + .filter_map(|l| { + if l.id == id { + new_lock.take().map(|nl| BalanceLock { + id: l.id, + amount: l.amount.max(nl.amount), + until: l.until.max(nl.until), + reasons: l.reasons | nl.reasons, + }) + } else if l.until > now { + Some(l) + } else { + None + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + >::insert(who, locks); + } + + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + let now = >::block_number(); + let locks = >::locks(who) + .into_iter() + .filter_map(|l| if l.until > now && l.id != id { Some(l) } else { None }) + .collect::>(); + >::insert(who, locks); + } +} + +pub trait AssetIdProvider { + type AssetId; + fn asset_id() -> Self::AssetId; +} + +// wrapping these imbalanes in a private module is necessary to ensure absolute privacy +// of the inner member. +mod imbalances { + use super::{result, AssetIdProvider, Imbalance, Saturating, StorageMap, Subtrait, Zero}; + use rstd::mem; + + /// Opaque, move-only struct with private fields that serves as a token denoting that + /// funds have been created without any equal and opposite accounting. + #[must_use] + pub struct PositiveImbalance>( + T::Balance, + rstd::marker::PhantomData, + ); + impl PositiveImbalance + where + T: Subtrait, + U: AssetIdProvider, + { + pub fn new(amount: T::Balance) -> Self { + PositiveImbalance(amount, Default::default()) + } + } + + /// Opaque, move-only struct with private fields that serves as a token denoting that + /// funds have been destroyed without any equal and opposite accounting. + #[must_use] + pub struct NegativeImbalance>( + T::Balance, + rstd::marker::PhantomData, + ); + impl NegativeImbalance + where + T: Subtrait, + U: AssetIdProvider, + { + pub fn new(amount: T::Balance) -> Self { + NegativeImbalance(amount, Default::default()) + } + } + + impl Imbalance for PositiveImbalance + where + T: Subtrait, + U: AssetIdProvider, + { + type Opposite = NegativeImbalance; + + fn zero() -> Self { + Self::new(Zero::zero()) + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0 - first; + + mem::forget(self); + (Self::new(first), Self::new(second)) + } + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + fn offset(self, other: Self::Opposite) -> result::Result { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + if a >= b { + Ok(Self::new(a - b)) + } else { + Err(NegativeImbalance::new(b - a)) + } + } + fn peek(&self) -> T::Balance { + self.0.clone() + } + } + + impl Imbalance for NegativeImbalance + where + T: Subtrait, + U: AssetIdProvider, + { + type Opposite = PositiveImbalance; + + fn zero() -> Self { + Self::new(Zero::zero()) + } + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0 - first; + + mem::forget(self); + (Self::new(first), Self::new(second)) + } + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + fn offset(self, other: Self::Opposite) -> result::Result { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + if a >= b { + Ok(Self::new(a - b)) + } else { + Err(PositiveImbalance::new(b - a)) + } + } + fn peek(&self) -> T::Balance { + self.0.clone() + } + } + + impl Drop for PositiveImbalance + where + T: Subtrait, + U: AssetIdProvider, + { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + >>::mutate(&U::asset_id(), |v| *v = v.saturating_add(self.0)); + } + } + + impl Drop for NegativeImbalance + where + T: Subtrait, + U: AssetIdProvider, + { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + >>::mutate(&U::asset_id(), |v| *v = v.saturating_sub(self.0)); + } + } +} + +// TODO: #2052 +// Somewhat ugly hack in order to gain access to module's `increase_total_issuance_by` +// using only the Subtrait (which defines only the types that are not dependent +// on Positive/NegativeImbalance). Subtrait must be used otherwise we end up with a +// circular dependency with Trait having some types be dependent on PositiveImbalance +// and PositiveImbalance itself depending back on Trait for its Drop impl (and thus +// its type declaration). +// This works as long as `increase_total_issuance_by` doesn't use the Imbalance +// types (basically for charging fees). +// This should eventually be refactored so that the three type items that do +// depend on the Imbalance type (TransactionPayment, TransferPayment, DustRemoval) +// are placed in their own SRML module. +struct ElevatedTrait(T); +impl Clone for ElevatedTrait { + fn clone(&self) -> Self { + unimplemented!() + } +} +impl PartialEq for ElevatedTrait { + fn eq(&self, _: &Self) -> bool { + unimplemented!() + } +} +impl Eq for ElevatedTrait {} +impl system::Trait for ElevatedTrait { + type Origin = T::Origin; + type Index = T::Index; + type BlockNumber = T::BlockNumber; + type Hash = T::Hash; + type Hashing = T::Hashing; + type AccountId = T::AccountId; + type Lookup = T::Lookup; + type Header = T::Header; + type Event = (); + type BlockHashCount = T::BlockHashCount; +} +impl Trait for ElevatedTrait { + type Balance = T::Balance; + type AssetId = T::AssetId; + type Event = (); +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct AssetCurrency(rstd::marker::PhantomData, rstd::marker::PhantomData); + +impl Currency for AssetCurrency +where + T: Trait, + U: AssetIdProvider, +{ + type Balance = T::Balance; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(who: &T::AccountId) -> Self::Balance { + Self::free_balance(&who) + Self::reserved_balance(&who) + } + + fn free_balance(who: &T::AccountId) -> Self::Balance { + >::free_balance(&U::asset_id(), &who) + } + + /// Returns the total staking asset issuance + fn total_issuance() -> Self::Balance { + >::total_issuance(U::asset_id()) + } + + fn minimum_balance() -> Self::Balance { + Zero::zero() + } + + fn transfer(transactor: &T::AccountId, dest: &T::AccountId, value: Self::Balance) -> Result { + >::make_transfer(&U::asset_id(), transactor, dest, value) + } + + fn ensure_can_withdraw( + who: &T::AccountId, + amount: Self::Balance, + reason: WithdrawReason, + new_balance: Self::Balance, + ) -> Result { + >::ensure_can_withdraw(&U::asset_id(), who, amount, reason, new_balance) + } + + fn withdraw( + who: &T::AccountId, + value: Self::Balance, + reason: WithdrawReason, + _: ExistenceRequirement, // no existential deposit policy for generic asset + ) -> result::Result { + let new_balance = Self::free_balance(who) + .checked_sub(&value) + .ok_or_else(|| "account has too few funds")?; + Self::ensure_can_withdraw(who, value, reason, new_balance)?; + >::set_free_balance(&U::asset_id(), who, new_balance); + Ok(NegativeImbalance::new(value)) + } + + fn deposit_into_existing( + who: &T::AccountId, + value: Self::Balance, + ) -> result::Result { + // No existential deposit rule and creation fee in GA. `deposit_into_existing` is same with `deposit_creating`. + Ok(Self::deposit_creating(who, value)) + } + + fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { + let (imbalance, _) = Self::make_free_balance_be(who, Self::free_balance(who) + value); + if let SignedImbalance::Positive(p) = imbalance { + p + } else { + // Impossible, but be defensive. + Self::PositiveImbalance::zero() + } + } + + fn make_free_balance_be( + who: &T::AccountId, + balance: Self::Balance, + ) -> ( + SignedImbalance, + UpdateBalanceOutcome, + ) { + let original = >::free_balance(&U::asset_id(), who); + let imbalance = if original <= balance { + SignedImbalance::Positive(PositiveImbalance::new(balance - original)) + } else { + SignedImbalance::Negative(NegativeImbalance::new(original - balance)) + }; + >::set_free_balance(&U::asset_id(), who, balance); + (imbalance, UpdateBalanceOutcome::Updated) + } + + fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { + >::free_balance(&U::asset_id(), &who) >= value + } + + fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + let remaining = >::slash(&U::asset_id(), who, value); + if let Some(r) = remaining { + (NegativeImbalance::new(value - r), r) + } else { + (NegativeImbalance::new(value), Zero::zero()) + } + } + + fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { + >::mutate(&U::asset_id(), |issued| + issued.checked_sub(&amount).unwrap_or_else(|| { + amount = *issued; + Zero::zero() + }) + ); + PositiveImbalance::new(amount) + } + + fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { + >::mutate(&U::asset_id(), |issued| + *issued = issued.checked_add(&amount).unwrap_or_else(|| { + amount = Self::Balance::max_value() - *issued; + Self::Balance::max_value() + }) + ); + NegativeImbalance::new(amount) + } +} + +impl ReservableCurrency for AssetCurrency +where + T: Trait, + U: AssetIdProvider, +{ + fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { + Self::free_balance(who) + .checked_sub(&value) + .map_or(false, |new_balance| + >::ensure_can_withdraw(&U::asset_id(), who, value, WithdrawReason::Reserve, new_balance).is_ok() + ) + } + + fn reserved_balance(who: &T::AccountId) -> Self::Balance { + >::reserved_balance(&U::asset_id(), &who) + } + + fn reserve(who: &T::AccountId, value: Self::Balance) -> result::Result<(), &'static str> { + >::reserve(&U::asset_id(), who, value) + } + + fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { + >::unreserve(&U::asset_id(), who, value) + } + + fn slash_reserved(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + let b = Self::reserved_balance(&who.clone()); + let slash = cmp::min(b, value); + + >::set_reserved_balance(&U::asset_id(), who, b - slash); + (NegativeImbalance::new(slash), value - slash) + } + + fn repatriate_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + ) -> result::Result { + Ok(>::repatriate_reserved(&U::asset_id(), slashed, beneficiary, value)) + } +} + +pub struct StakingAssetIdProvider(rstd::marker::PhantomData); + +impl AssetIdProvider for StakingAssetIdProvider { + type AssetId = T::AssetId; + fn asset_id() -> Self::AssetId { + >::staking_asset_id() + } +} + +pub struct SpendingAssetIdProvider(rstd::marker::PhantomData); + +impl AssetIdProvider for SpendingAssetIdProvider { + type AssetId = T::AssetId; + fn asset_id() -> Self::AssetId { + >::spending_asset_id() + } +} + +impl LockableCurrency for AssetCurrency> +where + T: Trait, + T::Balance: MaybeSerializeDebug, +{ + type Moment = T::BlockNumber; + + fn set_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: T::Balance, + until: T::BlockNumber, + reasons: WithdrawReasons, + ) { + >::set_lock(id, who, amount, until, reasons) + } + + fn extend_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: T::Balance, + until: T::BlockNumber, + reasons: WithdrawReasons, + ) { + >::extend_lock(id, who, amount, until, reasons) + } + + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + >::remove_lock(id, who) + } +} + +pub type StakingAssetCurrency = AssetCurrency>; +pub type SpendingAssetCurrency = AssetCurrency>; diff --git a/substrate/srml/generic-asset/src/mock.rs b/substrate/srml/generic-asset/src/mock.rs new file mode 100644 index 0000000000..02e18fc335 --- /dev/null +++ b/substrate/srml/generic-asset/src/mock.rs @@ -0,0 +1,140 @@ +// Copyright 2019 +// by Centrality Investments Ltd. +// and Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Mocks for the module. + +#![cfg(test)] + +use primitives::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; +use substrate_primitives::{Blake2Hasher, H256}; +use support::{parameter_types, impl_outer_event, impl_outer_origin}; + +use super::*; + +impl_outer_origin! { + pub enum Origin for Test {} +} + +// For testing the module, we construct most of a mock runtime. This means +// first constructing a configuration type (`Test`) which `impl`s each of the +// configuration traits of modules we want to use. +#[derive(Clone, Eq, PartialEq)] +pub struct Test; +parameter_types! { + pub const BlockHashCount: u64 = 250; +} +impl system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; +} + +impl Trait for Test { + type Balance = u64; + type AssetId = u32; + type Event = TestEvent; +} + +mod generic_asset { + pub use crate::Event; +} + +impl_outer_event! { + pub enum TestEvent for Test { + generic_asset, + } +} + +pub type GenericAsset = Module; + +pub type System = system::Module; + +pub struct ExtBuilder { + asset_id: u32, + next_asset_id: u32, + accounts: Vec, + initial_balance: u64, +} + +// Returns default values for genesis config +impl Default for ExtBuilder { + fn default() -> Self { + Self { + asset_id: 0, + next_asset_id: 1000, + accounts: vec![0], + initial_balance: 0, + } + } +} + +impl ExtBuilder { + // Sets free balance to genesis config + pub fn free_balance(mut self, free_balance: (u32, u64, u64)) -> Self { + self.asset_id = free_balance.0; + self.accounts = vec![free_balance.1]; + self.initial_balance = free_balance.2; + self + } + + pub fn next_asset_id(mut self, asset_id: u32) -> Self { + self.next_asset_id = asset_id; + self + } + + // builds genesis config + pub fn build(self) -> runtime_io::TestExternalities { + let mut t = system::GenesisConfig::default().build_storage::().unwrap().0; + + t.extend( + GenesisConfig:: { + assets: vec![self.asset_id], + endowed_accounts: self.accounts, + initial_balance: self.initial_balance, + next_asset_id: self.next_asset_id, + staking_asset_id: 16000, + spending_asset_id: 16001, + } + .build_storage() + .unwrap() + .0, + ); + + t.into() + } +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. +pub fn new_test_ext() -> runtime_io::TestExternalities { + system::GenesisConfig::default() + .build_storage::() + .unwrap() + .0 + .into() +} diff --git a/substrate/srml/generic-asset/src/tests.rs b/substrate/srml/generic-asset/src/tests.rs new file mode 100644 index 0000000000..685e553c1c --- /dev/null +++ b/substrate/srml/generic-asset/src/tests.rs @@ -0,0 +1,1288 @@ +// Copyright 2019 +// by Centrality Investments Ltd. +// and Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Tests for the module. + +#![cfg(test)] + +use super::*; +use crate::mock::{new_test_ext, ExtBuilder, GenericAsset, Origin, System, Test, TestEvent}; +use runtime_io::with_externalities; +use support::{assert_noop, assert_ok}; + +#[test] +fn issuing_asset_units_to_issuer_should_work() { + let balance = 100; + + with_externalities(&mut ExtBuilder::default().free_balance((16000, 1, 100)).build(), || { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + + let expected_balance = balance; + + assert_ok!(GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: balance, + permissions: default_permission + } + )); + assert_eq!(GenericAsset::free_balance(&16000, &1), expected_balance); + }); +} + +#[test] +fn issuing_with_next_asset_id_overflow_should_not_work() { + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + NextAssetId::::put(u32::max_value()); + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + assert_noop!( + GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: 1, + permissions: default_permission + } + ), + "No new assets id available." + ); + assert_eq!(GenericAsset::next_asset_id(), u32::max_value()); + }, + ); +} + +#[test] +fn querying_total_supply_should_work() { + let asset_id = 1000; + + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + assert_ok!(GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: 100, + permissions: default_permission + } + )); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), 100); + assert_ok!(GenericAsset::transfer(Origin::signed(1), asset_id, 2, 50)); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), 50); + assert_eq!(GenericAsset::free_balance(&asset_id, &2), 50); + assert_ok!(GenericAsset::transfer(Origin::signed(2), asset_id, 3, 31)); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), 50); + assert_eq!(GenericAsset::free_balance(&asset_id, &2), 19); + assert_eq!(GenericAsset::free_balance(&asset_id, &3), 31); + assert_ok!(GenericAsset::transfer(Origin::signed(1), asset_id, 1, 1)); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), 50); + }, + ); +} + +// Given +// - The next asset id as `asset_id` = 1000. +// - AssetOptions with all permissions. +// - GenesisStore has sufficient free balance. +// +// When +// - Create an asset from `origin` as 1. +// Then +// - free_balance of next asset id = 100. +// +// When +// - After transferring 40 from account 1 to account 2. +// Then +// - Origin account's `free_balance` = 60. +// - account 2's `free_balance` = 40. +#[test] +fn transferring_amount_should_work() { + let asset_id = 1000; + let free_balance = 100; + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + assert_ok!(GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: free_balance, + permissions: default_permission + } + )); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), free_balance); + assert_ok!(GenericAsset::transfer(Origin::signed(1), asset_id, 2, 40)); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), 60); + assert_eq!(GenericAsset::free_balance(&asset_id, &2), 40); + }, + ); +} + +// Given +// - The next asset id as `asset_id` = 1000. +// - AssetOptions with all permissions. +// - GenesisStore has sufficient free balance. +// +// When +// - Create an asset from `origin` as 1. +// Then +// - free_balance of next asset id = 100. +// +// When +// - After transferring 40 from account 1 to account 2. +// Then +// - Origin account's `free_balance` = 60. +// - account 2's `free_balance` = 40. +#[test] +fn transferring_amount_should_fail_when_transferring_more_than_free_balance() { + let asset_id = 1000; + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + assert_ok!(GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: 100, + permissions: default_permission + } + )); + assert_noop!( + GenericAsset::transfer(Origin::signed(1), asset_id, 2, 2000), + "balance too low to send amount" + ); + }, + ); +} + +#[test] +fn transferring_less_than_one_unit_should_not_work() { + let asset_id = 1000; + + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + assert_ok!(GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: 100, + permissions: default_permission + } + )); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), 100); + assert_noop!( + GenericAsset::transfer(Origin::signed(1), asset_id, 2, 0), + "cannot transfer zero amount" + ); + }, + ); +} + +// Given +// - Next asset id as `asset_id` = 1000. +// - Sufficient free balance. +// - initial balance = 100. +// When +// - After performing a self transfer from account 1 to 1. +// Then +// - Should not throw any errors. +// - Free balance after self transfer should equal to the free balance before self transfer. +#[test] +fn self_transfer_should_fail() { + let asset_id = 1000; + let balance = 100; + + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + assert_ok!(GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: balance, + permissions: default_permission + } + )); + + let initial_free_balance = GenericAsset::free_balance(&asset_id, &1); + assert_ok!(GenericAsset::transfer(Origin::signed(1), asset_id, 1, 10)); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), initial_free_balance); + }, + ); +} + +#[test] +fn transferring_more_units_than_total_supply_should_not_work() { + let asset_id = 1000; + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + assert_ok!(GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: 100, + permissions: default_permission + } + )); + assert_eq!(GenericAsset::free_balance(&asset_id, &1), 100); + assert_noop!( + GenericAsset::transfer(Origin::signed(1), asset_id, 2, 101), + "balance too low to send amount" + ); + }, + ); +} + +// Ensures it uses fake money for staking asset id. +#[test] +fn staking_asset_id_should_return_0() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(GenericAsset::staking_asset_id(), 16000); + }); +} + +// Ensures it uses fake money for spending asset id. +#[test] +fn spending_asset_id_should_return_10() { + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(GenericAsset::spending_asset_id(), 16001); + }); +} + +// Given +// -Â Free balance is 0 and the reserved balance is 0. +// Then +// -Â total_balance should return 0 +#[test] +fn total_balance_should_be_zero() { + with_externalities(&mut new_test_ext(), || { + assert_eq!(GenericAsset::total_balance(&0, &0), 0); + }); +} + +// Given +// -Â Free balance is 0 and the reserved balance > 0. +// When +// - After calling total_balance. +// Then +// -Â total_balance should equals to reserved balance. +#[test] +fn total_balance_should_be_equal_to_account_balance() { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + assert_ok!(GenericAsset::create( + Origin::signed(1), + AssetOptions { + initial_issuance: 100, + permissions: default_permission + } + )); + assert_eq!(GenericAsset::total_balance(&1000, &1), 100); + }, + ); +} + +// Given +// - An account presents with AccountId = 1 +// -Â free_balance > 0. +// - reserved_balance = 50. +// When +// - After calling free_balance. +// Then +// -Â free_balance should return 50. +#[test] +fn free_balance_should_only_return_account_free_balance() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 50)).build(), || { + GenericAsset::set_reserved_balance(&1, &0, 70); + assert_eq!(GenericAsset::free_balance(&1, &0), 50); + }); +} + +// Given +// - An account presents with AccountId = 1. +// -Â Free balance > 0 and the reserved balance > 0. +// When +// - After calling total_balance. +// Then +// -Â total_balance should equals to account balance + free balance. +#[test] +fn total_balance_should_be_equal_to_sum_of_account_balance_and_free_balance() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 50)).build(), || { + GenericAsset::set_reserved_balance(&1, &0, 70); + assert_eq!(GenericAsset::total_balance(&1, &0), 120); + }); +} + +// Given +// -Â free_balance > 0. +// - reserved_balance = 70. +// When +// - After calling reserved_balance. +// Then +// - reserved_balance should return 70. +#[test] +fn reserved_balance_should_only_return_account_reserved_balance() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 50)).build(), || { + GenericAsset::set_reserved_balance(&1, &0, 70); + assert_eq!(GenericAsset::reserved_balance(&1, &0), 70); + }); +} + +// Given +// - A valid account presents. +// - Initial reserved_balance = 0 +// When +// - After calls set_reserved_balance +// Then +// - Should persists the amount as reserved_balance. +// - reserved_balance = amount +#[test] +fn set_reserved_balance_should_add_balance_as_reserved() { + with_externalities(&mut ExtBuilder::default().build(), || { + GenericAsset::set_reserved_balance(&1, &0, 50); + assert_eq!(GenericAsset::reserved_balance(&1, &0), 50); + }); +} + +// Given +// - A valid account presents. +// - Initial free_balance = 100. +// When +// - After calling set_free_balance. +// Then +// - Should persists the amount as free_balance. +// - New free_balance should replace older free_balance. +#[test] +fn set_free_balance_should_add_amount_as_free_balance() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 100)).build(), || { + GenericAsset::set_free_balance(&1, &0, 50); + assert_eq!(GenericAsset::free_balance(&1, &0), 50); + }); +} + +// Given +// - free_balance is greater than the account balance. +// - free_balance = 100 +// - reserved_balance = 0 +// - reserve amount = 70 +// When +// - After calling reserve +// Then +// - Funds should be removed from the account. +// - new free_balance = original free_balance - reserved amount +// - new reserved_balance = original free balance + reserved amount +#[test] +fn reserve_should_moves_amount_from_balance_to_reserved_balance() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 100)).build(), || { + assert_ok!(GenericAsset::reserve(&1, &0, 70)); + assert_eq!(GenericAsset::free_balance(&1, &0), 30); + assert_eq!(GenericAsset::reserved_balance(&1, &0), 70); + }); +} + +// Given +// - Free balance is lower than the account balance. +// - free_balance = 100 +// - reserved_balance = 0 +// - reserve amount = 120 +// When +// - After calling reverse function. +// Then +// - Funds should not be removed from the account. +// - Should throw an error. +#[test] +fn reserve_should_not_moves_amount_from_balance_to_reserved_balance() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 100)).build(), || { + assert_noop!(GenericAsset::reserve(&1, &0, 120), "not enough free funds"); + assert_eq!(GenericAsset::free_balance(&1, &0), 100); + assert_eq!(GenericAsset::reserved_balance(&1, &0), 0); + }); +} + +// Given +// - unreserved_amount > reserved_balance. +// - reserved_balance = 100. +// - free_balance = 100. +// - unreserved_amount = 120. +// When +// - After calling unreserve function. +// Then +// - unreserved should return 20. +#[test] +fn unreserve_should_return_substratced_value_from_unreserved_amount_by_actual_acount_balance() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 100)).build(), || { + GenericAsset::set_reserved_balance(&1, &0, 100); + assert_eq!(GenericAsset::unreserve(&1, &0, 120), 20); + }); +} + +// Given +// - unreserved_amount < reserved_balance. +// - reserved_balance = 100. +// - free_balance = 100. +// - unreserved_amount = 50. +// When +// - After calling unreserve function. +// Then +// - unreserved should return None. +#[test] +fn unreserve_should_return_none() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 100)).build(), || { + GenericAsset::set_reserved_balance(&1, &0, 100); + assert_eq!(GenericAsset::unreserve(&1, &0, 50), 0); + }); +} + +// Given +// - unreserved_amount > reserved_balance. +// - reserved_balance = 100. +// - free_balance = 100. +// - unreserved_amount = 120. +// When +// - After calling unreserve function. +// Then +// - free_balance should be 200. +#[test] +fn unreserve_should_increase_free_balance_by_reserved_balance() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 100)).build(), || { + GenericAsset::set_reserved_balance(&1, &0, 100); + GenericAsset::unreserve(&1, &0, 120); + assert_eq!(GenericAsset::free_balance(&1, &0), 200); + }); +} + +// Given +// - unreserved_amount > reserved_balance. +// - reserved_balance = 100. +// - free_balance = 100. +// - unreserved_amount = 120. +// When +// - After calling unreserve function. +// Then +// - reserved_balance should be 0. +#[test] +fn unreserve_should_deduct_reserved_balance_by_reserved_amount() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 100)).build(), || { + GenericAsset::set_free_balance(&1, &0, 100); + GenericAsset::unreserve(&1, &0, 120); + assert_eq!(GenericAsset::reserved_balance(&1, &0), 0); + }); +} + +// Given +// - slash amount < free_balance. +// - reserved_balance = 100. +// - free_balance = 100. +// - slash amount = 70. +// When +// - After calling slash function. +// Then +// - slash should return None. +#[test] +fn slash_should_return_slash_reserved_amount() { + with_externalities(&mut ExtBuilder::default().free_balance((1, 0, 100)).build(), || { + GenericAsset::set_reserved_balance(&1, &0, 100); + assert_eq!(GenericAsset::slash(&1, &0, 70), None); + }); +} + +// Given +// - slashed_amount > reserved_balance. +// When +// - After calling slashed_reverse function. +// Then +// - Should return slashed_reserved - reserved_balance. +#[test] +fn slash_reserved_should_deducts_up_to_amount_from_reserved_balance() { + with_externalities(&mut ExtBuilder::default().build(), || { + GenericAsset::set_reserved_balance(&1, &0, 100); + assert_eq!(GenericAsset::slash_reserved(&1, &0, 150), Some(50)); + }); +} + +// Given +// - slashed_amount equals to reserved_amount. +// When +// - After calling slashed_reverse function. +// Then +// - Should return None. +#[test] +fn slash_reserved_should_return_none() { + with_externalities(&mut ExtBuilder::default().build(), || { + GenericAsset::set_reserved_balance(&1, &0, 100); + assert_eq!(GenericAsset::slash_reserved(&1, &0, 100), None); + }); +} + +// Given +// - reserved_balance = 100. +// - repatriate_reserved_amount > reserved_balance. +// When +// - After calling repatriate_reserved. +// Then +// - Should not return None. +#[test] +fn repatriate_reserved_return_amount_substracted_by_slash_amount() { + with_externalities(&mut ExtBuilder::default().build(), || { + GenericAsset::set_reserved_balance(&1, &0, 100); + assert_eq!(GenericAsset::repatriate_reserved(&1, &0, &1, 130), 30); + }); +} + +// Given +// - reserved_balance = 100. +// - repatriate_reserved_amount > reserved_balance. +// When +// - After calling repatriate_reserved. +// Then +// - Should return None. +#[test] +fn repatriate_reserved_return_none() { + with_externalities(&mut ExtBuilder::default().build(), || { + GenericAsset::set_reserved_balance(&1, &0, 100); + assert_eq!(GenericAsset::repatriate_reserved(&1, &0, &1, 90), 0); + }); +} + +// Given +// - An asset with all permissions +// When +// - After calling `create_reserved` function. +// Then +// - Should create a new reserved asset. +#[test] +fn create_reserved_should_create_a_default_account_with_the_balance_given() { + with_externalities(&mut ExtBuilder::default().next_asset_id(10).build(), || { + let default_permission = PermissionLatest { + update: Owner::Address(1), + mint: Owner::Address(1), + burn: Owner::Address(1), + }; + let options = AssetOptions { + initial_issuance: 500, + permissions: default_permission, + }; + + let expected_total_issuance = 500; + let created_asset_id = 9; + let created_account_id = 0; + + assert_ok!(GenericAsset::create_reserved(Origin::ROOT, created_asset_id, options)); + + // Tests for side effects. + assert_eq!(>::get(created_asset_id), expected_total_issuance); + assert_eq!( + >::get(&created_asset_id, &created_account_id), + expected_total_issuance + ); + }); +} + +// Given +// - Origin is signed +// - Origin does not have minting permission +// When +// - After calling mint function +// Then +// - Should throw a permission error +#[test] +fn mint_should_throw_permission_error() { + with_externalities(&mut ExtBuilder::default().build(), || { + let origin = 1; + let asset_id = 4; + let to_account = 2; + let amount = 100; + + assert_noop!( + GenericAsset::mint(Origin::signed(origin), asset_id, to_account, amount), + "The origin does not have permission to mint an asset." + ); + }); +} + +// Given +// - Origin is signed. +// - Origin has permissions. +// When +// - After calling mint function +// Then +// - Should increase `to` free_balance. +// - Should not change `origins` free_balance. +#[test] +fn mint_should_increase_asset() { + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let origin = 1; + let asset_id = 1000; + let to_account = 2; + let amount = 500; + let initial_issuance = 100; + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission + } + )); + + assert_ok!(GenericAsset::mint(Origin::signed(origin), asset_id, to_account, amount)); + assert_eq!(GenericAsset::free_balance(&asset_id, &to_account), amount); + + // Origin's free_balance should not change. + assert_eq!(GenericAsset::free_balance(&asset_id, &origin), initial_issuance); + }, + ); +} + +// Given +// - Origin is signed. +// - Origin does not have burning permission. +// When +// - After calling burn function. +// Then +// - Should throw a permission error. +#[test] +fn burn_should_throw_permission_error() { + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let origin = 1; + let asset_id = 4; + let to_account = 2; + let amount = 10; + + assert_noop!( + GenericAsset::burn(Origin::signed(origin), asset_id, to_account, amount), + "The origin does not have permission to burn an asset." + ); + }, + ); +} + +// Given +// - Origin is signed. +// - Origin has permissions. +// When +// - After calling burn function +// Then +// - Should decrease `to`'s free_balance. +// - Should not change `origin`'s free_balance. +#[test] +fn burn_should_burn_an_asset() { + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let origin = 1; + let asset_id = 1000; + let to_account = 2; + let amount = 1000; + let initial_issuance = 100; + let burn_amount = 400; + let expected_amount = 600; + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission + } + )); + assert_ok!(GenericAsset::mint(Origin::signed(origin), asset_id, to_account, amount)); + + assert_ok!(GenericAsset::burn( + Origin::signed(origin), + asset_id, + to_account, + burn_amount + )); + assert_eq!(GenericAsset::free_balance(&asset_id, &to_account), expected_amount); + }, + ); +} + +// Given +// - `default_permission` with all privileges. +// - All permissions for origin. +// When +// - After executing create function and check_permission function. +// Then +// - The account origin should have burn, mint and update permissions. +#[test] +fn check_permission_should_return_correct_permission() { + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let origin = 1; + let asset_id = 1000; + let initial_issuance = 100; + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission + } + )); + + assert_eq!( + GenericAsset::check_permission(&asset_id, &origin, &PermissionType::Burn), + true + ); + assert_eq!( + GenericAsset::check_permission(&asset_id, &origin, &PermissionType::Mint), + true + ); + assert_eq!( + GenericAsset::check_permission(&asset_id, &origin, &PermissionType::Update), + true + ); + }, + ); +} + +// Given +// - `default_permission` with no privileges. +// - No permissions for origin. +// When +// - After executing create function and check_permission function. +// Then +// - The account origin should not have burn, mint and update permissions. +#[test] +fn check_permission_should_return_false_for_no_permission() { + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let origin = 1; + let asset_id = 1000; + let initial_issuance = 100; + + let default_permission = PermissionLatest { + update: Owner::None, + mint: Owner::None, + burn: Owner::None, + }; + + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission + } + )); + + assert_eq!( + GenericAsset::check_permission(&asset_id, &origin, &PermissionType::Burn), + false + ); + assert_eq!( + GenericAsset::check_permission(&asset_id, &origin, &PermissionType::Mint), + false + ); + assert_eq!( + GenericAsset::check_permission(&asset_id, &origin, &PermissionType::Update), + false + ); + }, + ); +} + +// Given +// - `default_permission` only with update. +// When +// - After executing update_permission function. +// Then +// - The account origin should not have the burn permission. +// - The account origin should have update and mint permissions. +#[test] +fn update_permission_should_change_permission() { + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let origin = 1; + let asset_id = 1000; + let initial_issuance = 100; + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::None, + burn: Owner::None, + }; + + let new_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::None, + }; + + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission + } + )); + + assert_ok!(GenericAsset::update_permission( + Origin::signed(origin), + asset_id, + new_permission + )); + assert_eq!( + GenericAsset::check_permission(&asset_id, &origin, &PermissionType::Mint), + true + ); + assert_eq!( + GenericAsset::check_permission(&asset_id, &origin, &PermissionType::Burn), + false + ); + }, + ); +} + +// Given +// - `default_permission` without any permissions. +// When +// - After executing update_permission function. +// Then +// - Should throw an error stating "Origin does not have enough permission to update permissions." +#[test] +fn update_permission_should_throw_error_when_lack_of_permissions() { + with_externalities( + &mut ExtBuilder::default().free_balance((16000, 1, 100000)).build(), + || { + let origin = 1; + let asset_id = 1000; + let initial_issuance = 100; + + let default_permission = PermissionLatest { + update: Owner::None, + mint: Owner::None, + burn: Owner::None, + }; + + let new_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::None, + }; + + let expected_error_message = "Origin does not have enough permission to update permissions."; + + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission + } + )); + + assert_noop!( + GenericAsset::update_permission(Origin::signed(origin), asset_id, new_permission), + expected_error_message + ); + }, + ); +} + +// Given +// - `asset_id` provided. +// - `from_account` is present. +// - All permissions for origin. +// When +// - After calling create_asset. +// Then +// - Should create a reserved token with provided id. +// - NextAssetId doesn't change. +// - TotalIssuance must equal to initial issuance. +// - FreeBalance must equal to initial issuance for the given account. +// - Permissions must have burn, mint and updatePermission for the given asset_id. +#[test] +fn create_asset_works_with_given_asset_id_and_from_account() { + with_externalities(&mut ExtBuilder::default().next_asset_id(10).build(), || { + let origin = 1; + let from_account: Option<::AccountId> = Some(1); + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + let expected_permission = PermissionVersions::V1(default_permission.clone()); + let asset_id = 9; + let initial_issuance = 100; + + assert_ok!(GenericAsset::create_asset( + Some(asset_id), + from_account, + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission.clone() + } + )); + + // Test for side effects. + assert_eq!(>::get(), 10); + assert_eq!(>::get(asset_id), initial_issuance); + assert_eq!(>::get(&asset_id, &origin), initial_issuance); + assert_eq!(>::get(&asset_id), expected_permission); + }); +} + +// Given +// - `asset_id` is an id for user generated assets. +// - Whatever other params. +// Then +// - `create_asset` should not work. +#[test] +fn create_asset_with_non_reserved_asset_id_should_not_work() { + with_externalities(&mut ExtBuilder::default().next_asset_id(10).build(), || { + let origin = 1; + let from_account: Option<::AccountId> = Some(1); + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + + let asset_id = 11; + let initial_issuance = 100; + + assert_noop!( + GenericAsset::create_asset( + Some(asset_id), + from_account, + AssetOptions { + initial_issuance, + permissions: default_permission.clone() + } + ), + "Asset id not available." + ); + }); +} + +// Given +// - `asset_id` is for reserved assets, but already taken. +// - Whatever other params. +// Then +// - `create_asset` should not work. +#[test] +fn create_asset_with_a_taken_asset_id_should_not_work() { + with_externalities(&mut ExtBuilder::default().next_asset_id(10).build(), || { + let origin = 1; + let from_account: Option<::AccountId> = Some(1); + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + + let asset_id = 9; + let initial_issuance = 100; + + assert_ok!(GenericAsset::create_asset( + Some(asset_id), + from_account, + AssetOptions { + initial_issuance, + permissions: default_permission.clone() + } + )); + assert_noop!( + GenericAsset::create_asset( + Some(asset_id), + from_account, + AssetOptions { + initial_issuance, + permissions: default_permission.clone() + } + ), + "Asset id already taken." + ); + }); +} + +// Given +// - `asset_id` provided. +// - `from_account` is None. +// - All permissions for origin. +// When +// - After calling create_asset. +// Then +// - Should create a reserved token. +#[test] +fn create_asset_should_create_a_reserved_asset_when_from_account_is_none() { + with_externalities(&mut ExtBuilder::default().next_asset_id(10).build(), || { + let origin = 1; + let from_account: Option<::AccountId> = None; + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + + let created_account_id = 0; + let asset_id = 9; + let initial_issuance = 100; + + assert_ok!(GenericAsset::create_asset( + Some(asset_id), + from_account, + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission + } + )); + + // Test for a side effect. + assert_eq!( + >::get(&asset_id, &created_account_id), + initial_issuance + ); + }); +} + +// Given +// - `asset_id` not provided. +// - `from_account` is None. +// - All permissions for origin. +// When +// - After calling create_asset. +// Then +// - Should create a user token. +// - `NextAssetId`'s get should return a new value. +// - Should not create a `reserved_asset`. +#[test] +fn create_asset_should_create_a_user_asset() { + with_externalities(&mut ExtBuilder::default().next_asset_id(10).build(), || { + let origin = 1; + let from_account: Option<::AccountId> = None; + + let default_permission = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + + let created_account_id = 0; + let reserved_asset_id = 100000; + let initial_issuance = 100; + let created_user_asset_id = 10; + + assert_ok!(GenericAsset::create_asset( + None, + from_account, + AssetOptions { + initial_issuance: initial_issuance, + permissions: default_permission + } + )); + + // Test for side effects. + assert_eq!(>::get(&reserved_asset_id, &created_account_id), 0); + assert_eq!( + >::get(&created_user_asset_id, &created_account_id), + initial_issuance + ); + assert_eq!(>::get(created_user_asset_id), initial_issuance); + }); +} + +#[test] +fn update_permission_should_raise_event() { + // Arrange + let staking_asset_id = 16000; + let asset_id = 1000; + let origin = 1; + let initial_balance = 1000; + let permissions = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + + with_externalities( + &mut ExtBuilder::default() + .next_asset_id(asset_id) + .free_balance((staking_asset_id, origin, initial_balance)) + .build(), + || { + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: 0, + permissions: permissions.clone(), + } + )); + + // Act + assert_ok!(GenericAsset::update_permission( + Origin::signed(origin), + asset_id, + permissions.clone() + )); + + // Assert + assert!(System::events().iter().any(|record| record.event + == TestEvent::generic_asset(RawEvent::PermissionUpdated(asset_id, permissions.clone())))); + }, + ); +} + +#[test] +fn mint_should_raise_event() { + // Arrange + let staking_asset_id = 16000; + let asset_id = 1000; + let origin = 1; + let initial_balance = 1000; + let permissions = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + let to = 2; + let amount = 100; + + with_externalities( + &mut ExtBuilder::default() + .next_asset_id(asset_id) + .free_balance((staking_asset_id, origin, initial_balance)) + .build(), + || { + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: 0, + permissions: permissions.clone(), + } + )); + + // Act + assert_ok!(GenericAsset::mint(Origin::signed(origin), asset_id, to, amount)); + + // Assert + assert!(System::events() + .iter() + .any(|record| record.event == TestEvent::generic_asset(RawEvent::Minted(asset_id, to, amount)))); + }, + ); +} + +#[test] +fn burn_should_raise_event() { + // Arrange + let staking_asset_id = 16000; + let asset_id = 1000; + let origin = 1; + let initial_balance = 1000; + let permissions = PermissionLatest { + update: Owner::Address(origin), + mint: Owner::Address(origin), + burn: Owner::Address(origin), + }; + let amount = 100; + + with_externalities( + &mut ExtBuilder::default() + .next_asset_id(asset_id) + .free_balance((staking_asset_id, origin, initial_balance)) + .build(), + || { + assert_ok!(GenericAsset::create( + Origin::signed(origin), + AssetOptions { + initial_issuance: amount, + permissions: permissions.clone(), + } + )); + + // Act + assert_ok!(GenericAsset::burn(Origin::signed(origin), asset_id, origin, amount)); + + // Assert + assert!(System::events() + .iter() + .any(|record| record.event == TestEvent::generic_asset(RawEvent::Burned(asset_id, origin, amount)))); + }, + ); +}