diff --git a/pallets/tokens/Cargo.toml b/pallets/tokens/Cargo.toml new file mode 100644 index 0000000000000..1837edf8c4b16 --- /dev/null +++ b/pallets/tokens/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "orml-tokens" +description = "Fungible tokens module that implements `MultiCurrency` trait." +repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/tokens" +license = "Apache-2.0" +version = "0.3.1" +authors = ["Laminar Developers "] +edition = "2018" + +[dependencies] +serde = { version = "1.0.111", optional = true } +codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false } +sp-runtime = { version = "2.0.0", default-features = false } +sp-io = { version = "2.0.0", default-features = false } +sp-std = { version = "2.0.0", default-features = false } + +frame-support = { version = "2.0.0", default-features = false } +frame-system = { version = "2.0.0", default-features = false } + +orml-traits = { version = "0.3.1", default-features = false } + + +[dev-dependencies] +sp-core = { version = "2.0.0", default-features = false } +pallet-treasury = { version = "2.0.0" } +pallet-elections-phragmen = { version = "2.0.0" } + +clear_on_drop = { version = "0.2.4", features = ["no_cc"] } # https://github.com/paritytech/substrate/issues/4179 + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sp-runtime/std", + "sp-std/std", + "sp-io/std", + "frame-support/std", + "frame-system/std", + "orml-traits/std", +] diff --git a/pallets/tokens/README.md b/pallets/tokens/README.md new file mode 100644 index 0000000000000..1ca0ab511ba4e --- /dev/null +++ b/pallets/tokens/README.md @@ -0,0 +1,2 @@ +This is Fork of the `orml_tokens` crate because Origin crate does not support minting + diff --git a/pallets/tokens/src/default_weight.rs b/pallets/tokens/src/default_weight.rs new file mode 100644 index 0000000000000..a54d3c259a6c7 --- /dev/null +++ b/pallets/tokens/src/default_weight.rs @@ -0,0 +1,20 @@ +//! Weights for the Tokens Module + +use frame_support::weights::{ + constants::{RocksDbWeight as DbWeight, WEIGHT_PER_MICROS}, + Weight, +}; + +impl crate::WeightInfo for () { + fn transfer() -> Weight { + WEIGHT_PER_MICROS + .saturating_mul(84) + .saturating_add(DbWeight::get().reads_writes(4, 2)) + } + + fn transfer_all() -> Weight { + WEIGHT_PER_MICROS + .saturating_mul(88) + .saturating_add(DbWeight::get().reads_writes(4, 2)) + } +} diff --git a/pallets/tokens/src/imbalances.rs b/pallets/tokens/src/imbalances.rs new file mode 100644 index 0000000000000..5c6a857aa35c1 --- /dev/null +++ b/pallets/tokens/src/imbalances.rs @@ -0,0 +1,155 @@ +// wrapping these imbalances in a private module is necessary to ensure absolute +// privacy of the inner member. +use crate::{TotalIssuance, Trait}; +use frame_support::storage::StorageMap; +use frame_support::traits::{Get, Imbalance, TryDrop}; +use sp_runtime::traits::{Saturating, Zero}; +use sp_std::{marker, mem, result}; + +/// 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, + marker::PhantomData, +); + +impl> PositiveImbalance { + /// Create a new positive imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + PositiveImbalance(amount, marker::PhantomData::) + } +} + +/// 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, + marker::PhantomData, +); + +impl> NegativeImbalance { + /// Create a new negative imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + NegativeImbalance(amount, marker::PhantomData::) + } +} + +impl> TryDrop for PositiveImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } +} + +impl> Imbalance for PositiveImbalance { + 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 + } +} + +impl> TryDrop for NegativeImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } +} + +impl> Imbalance for NegativeImbalance { + 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 + } +} + +impl> Drop for PositiveImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + >::mutate(GetCurrencyId::get(), |v| *v = v.saturating_add(self.0)); + } +} + +impl> Drop for NegativeImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + >::mutate(GetCurrencyId::get(), |v| *v = v.saturating_sub(self.0)); + } +} diff --git a/pallets/tokens/src/lib.rs b/pallets/tokens/src/lib.rs new file mode 100644 index 0000000000000..861b4c3cdb6ed --- /dev/null +++ b/pallets/tokens/src/lib.rs @@ -0,0 +1,877 @@ +//! # Tokens Module +//! +//! ## Overview +//! +//! The tokens module provides fungible multi-currency functionality that +//! implements `MultiCurrency` trait. +//! +//! The tokens module provides functions for: +//! +//! - Querying and setting the balance of a given account. +//! - Getting and managing total issuance. +//! - Balance transfer between accounts. +//! - Depositing and withdrawing balance. +//! - Slashing an account balance. +//! +//! ### Implementations +//! +//! The tokens module provides implementations for following traits. +//! +//! - `MultiCurrency` - Abstraction over a fungible multi-currency system. +//! - `MultiCurrencyExtended` - Extended `MultiCurrency` with additional helper +//! types and methods, like updating balance +//! by a given signed integer amount. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! - `transfer` - Transfer some balance to another account. +//! - `transfer_all` - Transfer all balance to another account. +//! +//! ### Genesis Config +//! +//! The tokens module depends on the `GenesisConfig`. Endowed accounts could be +//! configured in genesis configs. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use frame_support::{ + decl_error, decl_event, decl_module, decl_storage, ensure, + traits::Get, + traits::{ + BalanceStatus as Status, Currency as PalletCurrency, ExistenceRequirement, Imbalance, + LockableCurrency as PalletLockableCurrency, ReservableCurrency as PalletReservableCurrency, SignedImbalance, + WithdrawReasons, + }, + weights::Weight, + Parameter, StorageMap, +}; +use frame_system::ensure_signed; +use sp_runtime::{ + traits::{ + AtLeast32BitUnsigned, Bounded, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Member, Saturating, + StaticLookup, Zero, + }, + DispatchError, DispatchResult, RuntimeDebug, +}; +use sp_std::{ + convert::{TryFrom, TryInto}, + marker, + prelude::*, + result, +}; + +#[cfg(feature = "std")] +use sp_std::collections::btree_map::BTreeMap; + +pub use crate::imbalances::{NegativeImbalance, PositiveImbalance}; +use orml_traits::{ + arithmetic::{self, Signed}, + BalanceStatus, LockIdentifier, MultiCurrency, MultiCurrencyExtended, MultiLockableCurrency, + MultiReservableCurrency, OnReceived, +}; + +mod default_weight; +mod imbalances; +mod mock; +mod tests; + +pub trait WeightInfo { + fn transfer() -> Weight; + fn transfer_all() -> Weight; +} + +pub trait Trait: frame_system::Trait { + type Event: From> + Into<::Event>; + + /// The balance type + type Balance: Parameter + Member + AtLeast32BitUnsigned + Default + Copy + MaybeSerializeDeserialize; + + /// The amount type, should be signed version of `Balance` + type Amount: Signed + + TryInto + + TryFrom + + Parameter + + Member + + arithmetic::SimpleArithmetic + + Default + + Copy + + MaybeSerializeDeserialize; + + /// The currency ID type + type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord; + + /// Hook when some fund is deposited into an account + type OnReceived: OnReceived; + + /// Weight information for extrinsics in this module. + type WeightInfo: WeightInfo; +} + +/// A single lock on a balance. There can be many of these on an account and +/// they "overlap", so the same balance is frozen by multiple locks. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)] +pub struct BalanceLock { + /// An identifier for this lock. Only one lock may be in existence for each + /// identifier. + pub id: LockIdentifier, + /// The amount which the free balance may not drop below when this lock is + /// in effect. + pub amount: Balance, +} + +/// balance information for an account. +#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug)] +pub struct AccountData { + /// Non-reserved part of the balance. There may still be restrictions on + /// this, but it is the total pool what may in principle be transferred, + /// reserved. + /// + /// This is the only balance that matters in terms of most operations on + /// tokens. + pub free: Balance, + /// Balance which is reserved and may not be used at all. + /// + /// This can still get slashed, but gets slashed last of all. + /// + /// This balance is a 'reserve' balance that other subsystems use in order + /// to set aside tokens that are still 'owned' by the account holder, but + /// which are suspendable. + pub reserved: Balance, + /// The amount that `free` may not drop below when withdrawing. + pub frozen: Balance, +} + +impl AccountData { + /// The amount that this account's free balance may not be reduced beyond. + fn frozen(&self) -> Balance { + self.frozen + } + /// The total balance in this account including any that is reserved and + /// ignoring any frozen. + fn total(&self) -> Balance { + self.free.saturating_add(self.reserved) + } +} + +decl_storage! { + trait Store for Module as Tokens { + /// The total issuance of a token type. + pub TotalIssuance get(fn total_issuance) build(|config: &GenesisConfig| { + config + .endowed_accounts + .iter() + .map(|(_, currency_id, initial_balance)| (currency_id, initial_balance)) + .fold(BTreeMap::::new(), |mut acc, (currency_id, initial_balance)| { + if let Some(issuance) = acc.get_mut(currency_id) { + *issuance = issuance.checked_add(initial_balance).expect("total issuance cannot overflow when building genesis"); + } else { + acc.insert(*currency_id, *initial_balance); + } + acc + }) + .into_iter() + .collect::>() + }): map hasher(twox_64_concat) T::CurrencyId => T::Balance; + + /// Any liquidity locks of a token type under an account. + /// NOTE: Should only be accessed when setting, changing and freeing a lock. + pub Locks get(fn locks): double_map hasher(blake2_128_concat) T::AccountId, hasher(twox_64_concat) T::CurrencyId => Vec>; + + /// The balance of a token type under an account. + /// + /// NOTE: If the total is ever zero, decrease account ref account. + /// + /// NOTE: This is only used in the case that this module is used to store balances. + pub Accounts get(fn accounts): double_map hasher(blake2_128_concat) T::AccountId, hasher(twox_64_concat) T::CurrencyId => AccountData; + } + add_extra_genesis { + config(endowed_accounts): Vec<(T::AccountId, T::CurrencyId, T::Balance)>; + + build(|config: &GenesisConfig| { + config.endowed_accounts.iter().for_each(|(account_id, currency_id, initial_balance)| { + >::mutate(account_id, currency_id, |account_data| account_data.free = *initial_balance) + }) + }) + } +} + +decl_event!( + pub enum Event where + ::AccountId, + ::CurrencyId, + ::Balance + { + /// Token transfer success. [currency_id, from, to, amount] + Transferred(CurrencyId, AccountId, AccountId, Balance), + } +); + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + type Error = Error; + + fn deposit_event() = default; + + /// Transfer some balance to another account. + /// + /// The dispatch origin for this call must be `Signed` by the transactor. + /// + /// # + /// - Complexity: `O(1)` + /// - Db reads: 4 + /// - Db writes: 2 + /// ------------------- + /// Base Weight: 84.08 µs + /// # + #[weight = T::WeightInfo::transfer()] + pub fn transfer( + origin, + dest: ::Source, + currency_id: T::CurrencyId, + #[compact] amount: T::Balance, + ) { + let from = ensure_signed(origin)?; + let to = T::Lookup::lookup(dest)?; + >::transfer(currency_id, &from, &to, amount)?; + + Self::deposit_event(RawEvent::Transferred(currency_id, from, to, amount)); + } + + /// Transfer all remaining balance to the given account. + /// + /// The dispatch origin for this call must be `Signed` by the transactor. + /// + /// # + /// - Complexity: `O(1)` + /// - Db reads: 4 + /// - Db writes: 2 + /// ------------------- + /// Base Weight: 87.71 µs + /// # + #[weight = T::WeightInfo::transfer_all()] + pub fn transfer_all( + origin, + dest: ::Source, + currency_id: T::CurrencyId, + ) { + let from = ensure_signed(origin)?; + let to = T::Lookup::lookup(dest)?; + let balance = >::free_balance(currency_id, &from); + >::transfer(currency_id, &from, &to, balance)?; + + Self::deposit_event(RawEvent::Transferred(currency_id, from, to, balance)); + } + } +} + +decl_error! { + /// Error for token module. + pub enum Error for Module { + /// The balance is too low + BalanceTooLow, + /// This operation will cause balance to overflow + BalanceOverflow, + /// This operation will cause total issuance to overflow + TotalIssuanceOverflow, + /// Cannot convert Amount into Balance type + AmountIntoBalanceFailed, + /// Failed because liquidity restrictions due to locking + LiquidityRestrictions, + } +} + +impl Module { + /// Set free balance of `who` to a new value. + /// + /// Note this will not maintain total issuance. + fn set_free_balance(currency_id: T::CurrencyId, who: &T::AccountId, balance: T::Balance) { + >::mutate(who, currency_id, |account_data| account_data.free = balance); + } + + /// Set reserved balance of `who` to a new value, meanwhile enforce + /// existential rule. + /// + /// Note this will not maintain total issuance, and the caller is expected + /// to do it. + fn set_reserved_balance(currency_id: T::CurrencyId, who: &T::AccountId, balance: T::Balance) { + >::mutate(who, currency_id, |account_data| account_data.reserved = balance); + } + + /// Update the account entry for `who` under `currency_id`, given the locks. + fn update_locks(currency_id: T::CurrencyId, who: &T::AccountId, locks: &[BalanceLock]) { + // update account data + >::mutate(who, currency_id, |account_data| { + account_data.frozen = Zero::zero(); + for lock in locks.iter() { + account_data.frozen = account_data.frozen.max(lock.amount); + } + }); + + // update locks + let existed = >::contains_key(who, currency_id); + if locks.is_empty() { + >::remove(who, currency_id); + if existed { + // decrease account ref count when destruct lock + frame_system::Module::::dec_ref(who); + } + } else { + >::insert(who, currency_id, locks); + if !existed { + // increase account ref count when initialize lock + frame_system::Module::::inc_ref(who); + } + } + } +} + +impl MultiCurrency for Module { + type CurrencyId = T::CurrencyId; + type Balance = T::Balance; + + fn total_issuance(currency_id: Self::CurrencyId) -> Self::Balance { + >::get(currency_id) + } + + fn total_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).total() + } + + fn free_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).free + } + + // Ensure that an account can withdraw from their free balance given any + // existing withdrawal restrictions like locks and vesting balance. + // Is a no-op if amount to be withdrawn is zero. + fn ensure_can_withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + + let new_balance = Self::free_balance(currency_id, who) + .checked_sub(&amount) + .ok_or(Error::::BalanceTooLow)?; + ensure!( + new_balance >= Self::accounts(who, currency_id).frozen(), + Error::::LiquidityRestrictions + ); + Ok(()) + } + + /// Transfer some free balance from `from` to `to`. + /// Is a no-op if value to be transferred is zero or the `from` is the same + /// as `to`. + fn transfer( + currency_id: Self::CurrencyId, + from: &T::AccountId, + to: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + if amount.is_zero() || from == to { + return Ok(()); + } + Self::ensure_can_withdraw(currency_id, from, amount)?; + + let from_balance = Self::free_balance(currency_id, from); + let to_balance = Self::free_balance(currency_id, to) + .checked_add(&amount) + .ok_or(Error::::BalanceOverflow)?; + // Cannot underflow because ensure_can_withdraw check + Self::set_free_balance(currency_id, from, from_balance - amount); + Self::set_free_balance(currency_id, to, to_balance); + T::OnReceived::on_received(to, currency_id, amount); + + Ok(()) + } + + /// Deposit some `amount` into the free balance of account `who`. + /// + /// Is a no-op if the `amount` to be deposited is zero. + fn deposit(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + + let new_total = Self::total_issuance(currency_id) + .checked_add(&amount) + .ok_or(Error::::TotalIssuanceOverflow)?; + >::insert(currency_id, new_total); + Self::set_free_balance(currency_id, who, Self::free_balance(currency_id, who) + amount); + T::OnReceived::on_received(who, currency_id, amount); + + Ok(()) + } + + fn withdraw(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + Self::ensure_can_withdraw(currency_id, who, amount)?; + + // Cannot underflow because ensure_can_withdraw check + >::mutate(currency_id, |v| *v -= amount); + Self::set_free_balance(currency_id, who, Self::free_balance(currency_id, who) - amount); + + Ok(()) + } + + // Check if `value` amount of free balance can be slashed from `who`. + fn can_slash(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::free_balance(currency_id, who) >= value + } + + /// Is a no-op if `value` to be slashed is zero. + /// + /// NOTE: `slash()` prefers free balance, but assumes that reserve balance + /// can be drawn from in extreme circumstances. `can_slash()` should be used + /// prior to `slash()` to avoid having to draw from reserved funds, however + /// we err on the side of punishment if things are inconsistent + /// or `can_slash` wasn't used appropriately. + fn slash(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> Self::Balance { + if amount.is_zero() { + return amount; + } + + let account = Self::accounts(who, currency_id); + let free_slashed_amount = account.free.min(amount); + // Cannot underflow becuase free_slashed_amount can never be greater than amount + let mut remaining_slash = amount - free_slashed_amount; + + // slash free balance + if !free_slashed_amount.is_zero() { + // Cannot underflow becuase free_slashed_amount can never be greater than + // account.free + Self::set_free_balance(currency_id, who, account.free - free_slashed_amount); + } + + // slash reserved balance + if !remaining_slash.is_zero() { + let reserved_slashed_amount = account.reserved.min(remaining_slash); + // Cannot underflow due to above line + remaining_slash -= reserved_slashed_amount; + Self::set_reserved_balance(currency_id, who, account.reserved - reserved_slashed_amount); + } + + // Cannot underflow because the slashed value cannot be greater than total + // issuance + >::mutate(currency_id, |v| *v -= amount - remaining_slash); + remaining_slash + } +} + +impl MultiCurrencyExtended for Module { + type Amount = T::Amount; + + fn update_balance(currency_id: Self::CurrencyId, who: &T::AccountId, by_amount: Self::Amount) -> DispatchResult { + if by_amount.is_zero() { + return Ok(()); + } + + // Ensure this doesn't overflow. There isn't any traits that exposes + // `saturating_abs` so we need to do it manually. + let by_amount_abs = if by_amount == Self::Amount::min_value() { + Self::Amount::max_value() + } else { + by_amount.abs() + }; + + let by_balance = + TryInto::::try_into(by_amount_abs).map_err(|_| Error::::AmountIntoBalanceFailed)?; + if by_amount.is_positive() { + Self::deposit(currency_id, who, by_balance) + } else { + Self::withdraw(currency_id, who, by_balance).map(|_| ()) + } + } +} + +impl MultiLockableCurrency for Module { + type Moment = T::BlockNumber; + + // Set a lock on the balance of `who` under `currency_id`. + // Is a no-op if lock amount is zero. + fn set_lock(lock_id: LockIdentifier, currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) { + if amount.is_zero() { + return; + } + let mut new_lock = Some(BalanceLock { id: lock_id, amount }); + let mut locks = Self::locks(who, currency_id) + .into_iter() + .filter_map(|lock| { + if lock.id == lock_id { + new_lock.take() + } else { + Some(lock) + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(currency_id, who, &locks[..]); + } + + // Extend a lock on the balance of `who` under `currency_id`. + // Is a no-op if lock amount is zero + fn extend_lock(lock_id: LockIdentifier, currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) { + if amount.is_zero() { + return; + } + let mut new_lock = Some(BalanceLock { id: lock_id, amount }); + let mut locks = Self::locks(who, currency_id) + .into_iter() + .filter_map(|lock| { + if lock.id == lock_id { + new_lock.take().map(|nl| BalanceLock { + id: lock.id, + amount: lock.amount.max(nl.amount), + }) + } else { + Some(lock) + } + }) + .collect::>(); + if let Some(lock) = new_lock { + locks.push(lock) + } + Self::update_locks(currency_id, who, &locks[..]); + } + + fn remove_lock(lock_id: LockIdentifier, currency_id: Self::CurrencyId, who: &T::AccountId) { + let mut locks = Self::locks(who, currency_id); + locks.retain(|lock| lock.id != lock_id); + Self::update_locks(currency_id, who, &locks[..]); + } +} + +impl MultiReservableCurrency for Module { + /// Check if `who` can reserve `value` from their free balance. + /// + /// Always `true` if value to be reserved is zero. + fn can_reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::ensure_can_withdraw(currency_id, who, value).is_ok() + } + + /// Slash from reserved balance, returning any amount that was unable to be + /// slashed. + /// + /// Is a no-op if the value to be slashed is zero. + fn slash_reserved(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + if value.is_zero() { + return value; + } + + let reserved_balance = Self::reserved_balance(currency_id, who); + let actual = reserved_balance.min(value); + Self::set_reserved_balance(currency_id, who, reserved_balance - actual); + >::mutate(currency_id, |v| *v -= actual); + value - actual + } + + fn reserved_balance(currency_id: Self::CurrencyId, who: &T::AccountId) -> Self::Balance { + Self::accounts(who, currency_id).reserved + } + + /// Move `value` from the free balance from `who` to their reserved balance. + /// + /// Is a no-op if value to be reserved is zero. + fn reserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> DispatchResult { + if value.is_zero() { + return Ok(()); + } + Self::ensure_can_withdraw(currency_id, who, value)?; + + let account = Self::accounts(who, currency_id); + Self::set_free_balance(currency_id, who, account.free - value); + // Cannot overflow becuase total issuance is using the same balance type and + // this doesn't increase total issuance + Self::set_reserved_balance(currency_id, who, account.reserved + value); + Ok(()) + } + + /// Unreserve some funds, returning any amount that was unable to be + /// unreserved. + /// + /// Is a no-op if the value to be unreserved is zero. + fn unreserve(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance { + if value.is_zero() { + return value; + } + + let account = Self::accounts(who, currency_id); + let actual = account.reserved.min(value); + Self::set_reserved_balance(currency_id, who, account.reserved - actual); + Self::set_free_balance(currency_id, who, account.free + actual); + T::OnReceived::on_received(who, currency_id, actual); + value - actual + } + + /// Move the reserved balance of one account into the balance of another, + /// according to `status`. + /// + /// Is a no-op if: + /// - the value to be moved is zero; or + /// - the `slashed` id equal to `beneficiary` and the `status` is + /// `Reserved`. + fn repatriate_reserved( + currency_id: Self::CurrencyId, + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: BalanceStatus, + ) -> result::Result { + if value.is_zero() { + return Ok(value); + } + + if slashed == beneficiary { + return match status { + BalanceStatus::Free => Ok(Self::unreserve(currency_id, slashed, value)), + BalanceStatus::Reserved => Ok(value.saturating_sub(Self::reserved_balance(currency_id, slashed))), + }; + } + + let from_account = Self::accounts(slashed, currency_id); + let to_account = Self::accounts(beneficiary, currency_id); + let actual = from_account.reserved.min(value); + match status { + BalanceStatus::Free => { + Self::set_free_balance(currency_id, beneficiary, to_account.free + actual); + T::OnReceived::on_received(beneficiary, currency_id, actual); + } + BalanceStatus::Reserved => { + Self::set_reserved_balance(currency_id, beneficiary, to_account.reserved + actual); + } + } + Self::set_reserved_balance(currency_id, slashed, from_account.reserved - actual); + Ok(value - actual) + } +} + +pub struct CurrencyAdapter(marker::PhantomData<(T, GetCurrencyId)>); + +impl PalletCurrency for CurrencyAdapter +where + T: Trait, + GetCurrencyId: Get, +{ + type Balance = T::Balance; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(who: &T::AccountId) -> Self::Balance { + Module::::total_balance(GetCurrencyId::get(), who) + } + + fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { + Module::::can_slash(GetCurrencyId::get(), who, value) + } + + fn total_issuance() -> Self::Balance { + Module::::total_issuance(GetCurrencyId::get()) + } + + fn minimum_balance() -> Self::Balance { + Zero::zero() + } + + fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { + if amount.is_zero() { + return PositiveImbalance::zero(); + } + >::mutate(GetCurrencyId::get(), |issued| { + *issued = issued.checked_sub(&amount).unwrap_or_else(|| { + amount = *issued; + Zero::zero() + }); + }); + PositiveImbalance::new(amount) + } + + fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { + if amount.is_zero() { + return NegativeImbalance::zero(); + } + >::mutate(GetCurrencyId::get(), |issued| { + *issued = issued.checked_add(&amount).unwrap_or_else(|| { + amount = Self::Balance::max_value() - *issued; + Self::Balance::max_value() + }) + }); + NegativeImbalance::new(amount) + } + + fn free_balance(who: &T::AccountId) -> Self::Balance { + Module::::free_balance(GetCurrencyId::get(), who) + } + + fn ensure_can_withdraw( + who: &T::AccountId, + amount: Self::Balance, + _reasons: WithdrawReasons, + _new_balance: Self::Balance, + ) -> DispatchResult { + Module::::ensure_can_withdraw(GetCurrencyId::get(), who, amount) + } + + fn transfer( + source: &T::AccountId, + dest: &T::AccountId, + value: Self::Balance, + _existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + as MultiCurrency>::transfer(GetCurrencyId::get(), &source, &dest, value) + } + + fn slash(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (Self::NegativeImbalance::zero(), value); + } + + let currency_id = GetCurrencyId::get(); + let account = Module::::accounts(who, currency_id); + let free_slashed_amount = account.free.min(value); + let mut remaining_slash = value - free_slashed_amount; + + // slash free balance + if !free_slashed_amount.is_zero() { + Module::::set_free_balance(currency_id, who, account.free - free_slashed_amount); + } + + // slash reserved balance + if !remaining_slash.is_zero() { + let reserved_slashed_amount = account.reserved.min(remaining_slash); + remaining_slash -= reserved_slashed_amount; + Module::::set_reserved_balance(currency_id, who, account.reserved - reserved_slashed_amount); + ( + Self::NegativeImbalance::new(free_slashed_amount + reserved_slashed_amount), + remaining_slash, + ) + } else { + (Self::NegativeImbalance::new(value), remaining_slash) + } + } + + fn deposit_into_existing( + who: &T::AccountId, + value: Self::Balance, + ) -> result::Result { + if value.is_zero() { + return Ok(Self::PositiveImbalance::zero()); + } + let currency_id = GetCurrencyId::get(); + let new_total = Module::::free_balance(currency_id, who) + .checked_add(&value) + .ok_or(Error::::TotalIssuanceOverflow)?; + Module::::set_free_balance(currency_id, who, new_total); + + Ok(Self::PositiveImbalance::new(value)) + } + + fn deposit_creating(who: &T::AccountId, value: Self::Balance) -> Self::PositiveImbalance { + Self::deposit_into_existing(who, value).unwrap_or_else(|_| Self::PositiveImbalance::zero()) + } + + fn withdraw( + who: &T::AccountId, + value: Self::Balance, + _reasons: WithdrawReasons, + _liveness: ExistenceRequirement, + ) -> result::Result { + if value.is_zero() { + return Ok(Self::NegativeImbalance::zero()); + } + let currency_id = GetCurrencyId::get(); + Module::::ensure_can_withdraw(currency_id, who, value)?; + Module::::set_free_balance(currency_id, who, Module::::free_balance(currency_id, who) - value); + + Ok(Self::NegativeImbalance::new(value)) + } + + fn make_free_balance_be( + who: &T::AccountId, + value: Self::Balance, + ) -> SignedImbalance { + >::mutate( + who, + GetCurrencyId::get(), + |account| -> Result, ()> { + let imbalance = if account.free <= value { + SignedImbalance::Positive(PositiveImbalance::new(value - account.free)) + } else { + SignedImbalance::Negative(NegativeImbalance::new(account.free - value)) + }; + account.free = value; + Ok(imbalance) + }, + ) + .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) + } +} + +impl PalletReservableCurrency for CurrencyAdapter +where + T: Trait, + GetCurrencyId: Get, +{ + fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { + Module::::can_reserve(GetCurrencyId::get(), who, value) + } + + fn slash_reserved(who: &T::AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) { + let actual = Module::::slash_reserved(GetCurrencyId::get(), who, value); + (Self::NegativeImbalance::zero(), actual) + } + + fn reserved_balance(who: &T::AccountId) -> Self::Balance { + Module::::reserved_balance(GetCurrencyId::get(), who) + } + + fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { + Module::::reserve(GetCurrencyId::get(), who, value) + } + + fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { + Module::::unreserve(GetCurrencyId::get(), who, value) + } + + fn repatriate_reserved( + slashed: &T::AccountId, + beneficiary: &T::AccountId, + value: Self::Balance, + status: Status, + ) -> result::Result { + Module::::repatriate_reserved(GetCurrencyId::get(), slashed, beneficiary, value, status) + } +} + +impl PalletLockableCurrency for CurrencyAdapter +where + T: Trait, + GetCurrencyId: Get, +{ + type Moment = T::BlockNumber; + type MaxLocks = (); + + fn set_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { + Module::::set_lock(id, GetCurrencyId::get(), who, amount) + } + + fn extend_lock(id: LockIdentifier, who: &T::AccountId, amount: Self::Balance, _reasons: WithdrawReasons) { + Module::::extend_lock(id, GetCurrencyId::get(), who, amount) + } + + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + Module::::remove_lock(id, GetCurrencyId::get(), who) + } +} diff --git a/pallets/tokens/src/mock.rs b/pallets/tokens/src/mock.rs new file mode 100644 index 0000000000000..a215005d068a7 --- /dev/null +++ b/pallets/tokens/src/mock.rs @@ -0,0 +1,362 @@ +//! Mocks for the tokens module. + +#![cfg(test)] + +use frame_support::{ + impl_outer_event, impl_outer_origin, parameter_types, + traits::{ChangeMembers, Contains, ContainsLengthBound}, +}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{Convert, IdentityLookup}, + ModuleId, Perbill, Percent, Permill, +}; +use sp_std::cell::RefCell; +use std::collections::HashMap; + +use super::*; + +impl_outer_origin! { + pub enum Origin for Runtime {} +} + +mod tokens { + pub use crate::Event; +} + +impl_outer_event! { + pub enum TestEvent for Runtime { + frame_system, + tokens, + pallet_treasury, + pallet_elections_phragmen, + } +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} + +type AccountId = u64; +impl frame_system::Trait for Runtime { + type Origin = Origin; + type Call = (); + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type PalletInfo = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BlockExecutionWeight = (); + type ExtrinsicBaseWeight = (); + type MaximumExtrinsicWeight = (); + type BaseCallFilter = (); + type SystemWeightInfo = (); +} +pub type System = system::Module; + +type CurrencyId = u32; +pub type Balance = u64; + +thread_local! { + pub static ACCUMULATED_RECEIVED: RefCell> = RefCell::new(HashMap::new()); +} + +pub struct MockOnReceived; +impl OnReceived for MockOnReceived { + fn on_received(who: &AccountId, currency_id: CurrencyId, amount: Balance) { + ACCUMULATED_RECEIVED.with(|v| { + let mut old_map = v.borrow().clone(); + if let Some(before) = old_map.get_mut(&(*who, currency_id)) { + *before += amount; + } else { + old_map.insert((*who, currency_id), amount); + }; + + *v.borrow_mut() = old_map; + }); + } +} + +thread_local! { + static TEN_TO_FOURTEEN: RefCell> = RefCell::new(vec![10,11,12,13,14]); +} +pub struct TenToFourteen; +impl Contains for TenToFourteen { + fn sorted_members() -> Vec { + TEN_TO_FOURTEEN.with(|v| v.borrow().clone()) + } + #[cfg(feature = "runtime-benchmarks")] + fn add(new: &u64) { + TEN_TO_FOURTEEN.with(|v| { + let mut members = v.borrow_mut(); + members.push(*new); + members.sort(); + }) + } +} +impl ContainsLengthBound for TenToFourteen { + fn max_len() -> usize { + TEN_TO_FOURTEEN.with(|v| v.borrow().len()) + } + fn min_len() -> usize { + 0 + } +} + +parameter_types! { + pub const ProposalBond: Permill = Permill::from_percent(5); + pub const ProposalBondMinimum: u64 = 1; + pub const TipCountdown: u64 = 1; + pub const TipFindersFee: Percent = Percent::from_percent(20); + pub const TipReportDepositBase: u64 = 1; + pub const DataDepositPerByte: u64 = 1; + pub const SpendPeriod: u64 = 2; + pub const Burn: Permill = Permill::from_percent(50); + pub const TreasuryModuleId: ModuleId = ModuleId(*b"py/trsry"); + pub const GetTokenId: CurrencyId = TEST_TOKEN_ID; +} + +impl pallet_treasury::Trait for Runtime { + type ModuleId = TreasuryModuleId; + type Currency = CurrencyAdapter; + type ApproveOrigin = frame_system::EnsureRoot; + type RejectOrigin = frame_system::EnsureRoot; + type Tippers = TenToFourteen; + type TipCountdown = TipCountdown; + type TipFindersFee = TipFindersFee; + type TipReportDepositBase = TipReportDepositBase; + type DataDepositPerByte = DataDepositPerByte; + type Event = TestEvent; + type OnSlash = (); + type ProposalBond = ProposalBond; + type ProposalBondMinimum = ProposalBondMinimum; + type SpendPeriod = SpendPeriod; + type Burn = Burn; + type BurnDestination = (); // Just gets burned. + type BountyDepositBase = (); + type BountyDepositPayoutDelay = (); + type BountyUpdatePeriod = (); + type BountyCuratorDeposit = (); + type BountyValueMinimum = (); + type MaximumReasonLength = (); + type WeightInfo = (); +} + +pub struct CurrencyToVoteHandler; +impl Convert for CurrencyToVoteHandler { + fn convert(x: u64) -> u64 { + x + } +} +impl Convert for CurrencyToVoteHandler { + fn convert(x: u128) -> u64 { + x as u64 + } +} + +parameter_types! { + pub const CandidacyBond: u64 = 3; +} + +thread_local! { + static VOTING_BOND: RefCell = RefCell::new(2); + static DESIRED_MEMBERS: RefCell = RefCell::new(2); + static DESIRED_RUNNERS_UP: RefCell = RefCell::new(2); + static TERM_DURATION: RefCell = RefCell::new(5); +} + +pub struct VotingBond; +impl Get for VotingBond { + fn get() -> u64 { + VOTING_BOND.with(|v| *v.borrow()) + } +} + +pub struct DesiredMembers; +impl Get for DesiredMembers { + fn get() -> u32 { + DESIRED_MEMBERS.with(|v| *v.borrow()) + } +} + +pub struct DesiredRunnersUp; +impl Get for DesiredRunnersUp { + fn get() -> u32 { + DESIRED_RUNNERS_UP.with(|v| *v.borrow()) + } +} + +pub struct TermDuration; +impl Get for TermDuration { + fn get() -> u64 { + TERM_DURATION.with(|v| *v.borrow()) + } +} + +thread_local! { + pub static MEMBERS: RefCell> = RefCell::new(vec![]); + pub static PRIME: RefCell> = RefCell::new(None); +} + +pub struct TestChangeMembers; +impl ChangeMembers for TestChangeMembers { + fn change_members_sorted(incoming: &[u64], outgoing: &[u64], new: &[u64]) { + // new, incoming, outgoing must be sorted. + let mut new_sorted = new.to_vec(); + new_sorted.sort(); + assert_eq!(new, &new_sorted[..]); + + let mut incoming_sorted = incoming.to_vec(); + incoming_sorted.sort(); + assert_eq!(incoming, &incoming_sorted[..]); + + let mut outgoing_sorted = outgoing.to_vec(); + outgoing_sorted.sort(); + assert_eq!(outgoing, &outgoing_sorted[..]); + + // incoming and outgoing must be disjoint + for x in incoming.iter() { + assert!(outgoing.binary_search(x).is_err()); + } + + let mut old_plus_incoming = MEMBERS.with(|m| m.borrow().to_vec()); + old_plus_incoming.extend_from_slice(incoming); + old_plus_incoming.sort(); + + let mut new_plus_outgoing = new.to_vec(); + new_plus_outgoing.extend_from_slice(outgoing); + new_plus_outgoing.sort(); + + assert_eq!( + old_plus_incoming, new_plus_outgoing, + "change members call is incorrect!" + ); + + MEMBERS.with(|m| *m.borrow_mut() = new.to_vec()); + PRIME.with(|p| *p.borrow_mut() = None); + } + + fn set_prime(who: Option) { + PRIME.with(|p| *p.borrow_mut() = who); + } +} + +parameter_types! { + pub const ElectionsPhragmenModuleId: LockIdentifier = *b"phrelect"; +} + +impl pallet_elections_phragmen::Trait for Runtime { + type ModuleId = ElectionsPhragmenModuleId; + type Event = TestEvent; + type Currency = CurrencyAdapter; + type CurrencyToVote = CurrencyToVoteHandler; + type ChangeMembers = TestChangeMembers; + type InitializeMembers = (); + type CandidacyBond = CandidacyBond; + type VotingBond = VotingBond; + type TermDuration = TermDuration; + type DesiredMembers = DesiredMembers; + type DesiredRunnersUp = DesiredRunnersUp; + type LoserCandidate = (); + type KickedMember = (); + type BadReport = (); + type WeightInfo = (); +} + +impl Trait for Runtime { + type Event = TestEvent; + type Balance = Balance; + type Amount = i64; + type CurrencyId = CurrencyId; + type OnReceived = MockOnReceived; + type WeightInfo = (); +} + +pub type Tokens = Module; +pub type TreasuryCurrencyAdapter = ::Currency; + +pub const TEST_TOKEN_ID: CurrencyId = 1; +pub const ALICE: AccountId = 1; +pub const BOB: AccountId = 2; +pub const TREASURY_ACCOUNT: AccountId = 3; +pub const ID_1: LockIdentifier = *b"1 "; +pub const ID_2: LockIdentifier = *b"2 "; + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>, + treasury_genesis: bool, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + endowed_accounts: vec![], + treasury_genesis: false, + } + } +} + +impl ExtBuilder { + pub fn balances(mut self, endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>) -> Self { + self.endowed_accounts = endowed_accounts; + self + } + + pub fn one_hundred_for_alice_n_bob(self) -> Self { + self.balances(vec![(ALICE, TEST_TOKEN_ID, 100), (BOB, TEST_TOKEN_ID, 100)]) + } + + pub fn one_hundred_for_treasury_account(mut self) -> Self { + self.treasury_genesis = true; + self.balances(vec![(TREASURY_ACCOUNT, TEST_TOKEN_ID, 100)]) + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + GenesisConfig:: { + endowed_accounts: self.endowed_accounts, + } + .assimilate_storage(&mut t) + .unwrap(); + + if self.treasury_genesis { + pallet_treasury::GenesisConfig::default() + .assimilate_storage::(&mut t) + .unwrap(); + + pallet_elections_phragmen::GenesisConfig:: { + members: vec![(TREASURY_ACCOUNT, 10)], + } + .assimilate_storage(&mut t) + .unwrap(); + } + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/pallets/tokens/src/tests.rs b/pallets/tokens/src/tests.rs new file mode 100644 index 0000000000000..18d2088afd415 --- /dev/null +++ b/pallets/tokens/src/tests.rs @@ -0,0 +1,903 @@ +//! Unit tests for the tokens module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok, traits::WithdrawReason}; +use mock::{ + Balance, ExtBuilder, Runtime, System, TestEvent, Tokens, TreasuryCurrencyAdapter, ACCUMULATED_RECEIVED, ALICE, BOB, + ID_1, ID_2, TEST_TOKEN_ID, TREASURY_ACCOUNT, +}; + +#[test] +fn set_lock_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + Tokens::set_lock(ID_1, TEST_TOKEN_ID, &ALICE, 10); + assert_eq!(Tokens::accounts(&ALICE, TEST_TOKEN_ID).frozen, 10); + assert_eq!(Tokens::accounts(&ALICE, TEST_TOKEN_ID).frozen(), 10); + assert_eq!(Tokens::locks(ALICE, TEST_TOKEN_ID).len(), 1); + Tokens::set_lock(ID_1, TEST_TOKEN_ID, &ALICE, 50); + assert_eq!(Tokens::accounts(&ALICE, TEST_TOKEN_ID).frozen, 50); + assert_eq!(Tokens::locks(ALICE, TEST_TOKEN_ID).len(), 1); + Tokens::set_lock(ID_2, TEST_TOKEN_ID, &ALICE, 60); + assert_eq!(Tokens::accounts(&ALICE, TEST_TOKEN_ID).frozen, 60); + assert_eq!(Tokens::locks(ALICE, TEST_TOKEN_ID).len(), 2); + }); +} + +#[test] +fn extend_lock_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + Tokens::set_lock(ID_1, TEST_TOKEN_ID, &ALICE, 10); + assert_eq!(Tokens::locks(ALICE, TEST_TOKEN_ID).len(), 1); + assert_eq!(Tokens::accounts(&ALICE, TEST_TOKEN_ID).frozen, 10); + Tokens::extend_lock(ID_1, TEST_TOKEN_ID, &ALICE, 20); + assert_eq!(Tokens::locks(ALICE, TEST_TOKEN_ID).len(), 1); + assert_eq!(Tokens::accounts(&ALICE, TEST_TOKEN_ID).frozen, 20); + Tokens::extend_lock(ID_2, TEST_TOKEN_ID, &ALICE, 10); + Tokens::extend_lock(ID_1, TEST_TOKEN_ID, &ALICE, 20); + assert_eq!(Tokens::locks(ALICE, TEST_TOKEN_ID).len(), 2); + }); +} + +#[test] +fn remove_lock_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + Tokens::set_lock(ID_1, TEST_TOKEN_ID, &ALICE, 10); + Tokens::set_lock(ID_2, TEST_TOKEN_ID, &ALICE, 20); + assert_eq!(Tokens::locks(ALICE, TEST_TOKEN_ID).len(), 2); + Tokens::remove_lock(ID_2, TEST_TOKEN_ID, &ALICE); + assert_eq!(Tokens::locks(ALICE, TEST_TOKEN_ID).len(), 1); + }); +} + +#[test] +fn frozen_can_limit_liquidity() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + Tokens::set_lock(ID_1, TEST_TOKEN_ID, &ALICE, 90); + assert_noop!( + >::transfer(TEST_TOKEN_ID, &ALICE, &BOB, 11), + Error::::LiquidityRestrictions, + ); + Tokens::set_lock(ID_1, TEST_TOKEN_ID, &ALICE, 10); + assert_ok!(>::transfer(TEST_TOKEN_ID, &ALICE, &BOB, 11),); + }); +} + +#[test] +fn can_reserve_is_correct() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_eq!(Tokens::can_reserve(TEST_TOKEN_ID, &ALICE, 0), true); + assert_eq!(Tokens::can_reserve(TEST_TOKEN_ID, &ALICE, 101), false); + assert_eq!(Tokens::can_reserve(TEST_TOKEN_ID, &ALICE, 100), true); + }); +} + +#[test] +fn reserve_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_noop!( + Tokens::reserve(TEST_TOKEN_ID, &ALICE, 101), + Error::::BalanceTooLow, + ); + assert_ok!(Tokens::reserve(TEST_TOKEN_ID, &ALICE, 0)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!(Tokens::total_balance(TEST_TOKEN_ID, &ALICE), 100); + assert_ok!(Tokens::reserve(TEST_TOKEN_ID, &ALICE, 50)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::total_balance(TEST_TOKEN_ID, &ALICE), 100); + }); +} + +#[test] +fn unreserve_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!(Tokens::unreserve(TEST_TOKEN_ID, &ALICE, 0), 0); + assert_eq!(Tokens::unreserve(TEST_TOKEN_ID, &ALICE, 50), 50); + assert_ok!(Tokens::reserve(TEST_TOKEN_ID, &ALICE, 30)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 70); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 30); + assert_eq!(Tokens::unreserve(TEST_TOKEN_ID, &ALICE, 15), 0); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 85); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 15); + assert_eq!(Tokens::unreserve(TEST_TOKEN_ID, &ALICE, 30), 15); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 0); + }); +} + +#[test] +fn slash_reserved_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_ok!(Tokens::reserve(TEST_TOKEN_ID, &ALICE, 50)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 200); + assert_eq!(Tokens::slash_reserved(TEST_TOKEN_ID, &ALICE, 0), 0); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 200); + assert_eq!(Tokens::slash_reserved(TEST_TOKEN_ID, &ALICE, 100), 50); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 150); + }); +} + +#[test] +fn repatriate_reserved_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!( + Tokens::repatriate_reserved(TEST_TOKEN_ID, &ALICE, &ALICE, 0, BalanceStatus::Free), + Ok(0) + ); + assert_eq!( + Tokens::repatriate_reserved(TEST_TOKEN_ID, &ALICE, &ALICE, 50, BalanceStatus::Free), + Ok(50) + ); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 0); + + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 100); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &BOB), 0); + assert_ok!(Tokens::reserve(TEST_TOKEN_ID, &BOB, 50)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &BOB), 50); + assert_eq!( + Tokens::repatriate_reserved(TEST_TOKEN_ID, &BOB, &BOB, 60, BalanceStatus::Reserved), + Ok(10) + ); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &BOB), 50); + + assert_eq!( + Tokens::repatriate_reserved(TEST_TOKEN_ID, &BOB, &ALICE, 30, BalanceStatus::Reserved), + Ok(0) + ); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 100); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 30); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &BOB), 20); + + assert_eq!( + Tokens::repatriate_reserved(TEST_TOKEN_ID, &BOB, &ALICE, 30, BalanceStatus::Free), + Ok(10) + ); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 120); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 30); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &BOB), 0); + }); +} + +#[test] +fn slash_draw_reserved_correct() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_ok!(Tokens::reserve(TEST_TOKEN_ID, &ALICE, 50)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 200); + + assert_eq!(Tokens::slash(TEST_TOKEN_ID, &ALICE, 80), 0); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 20); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 120); + + assert_eq!(Tokens::slash(TEST_TOKEN_ID, &ALICE, 50), 30); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 100); + }); +} + +#[test] +fn genesis_issuance_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 100); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 100); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 200); + }); +} + +#[test] +fn transfer_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + System::set_block_number(1); + + assert_ok!(Tokens::transfer(Some(ALICE).into(), BOB, TEST_TOKEN_ID, 50)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 150); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 200); + assert_eq!( + ACCUMULATED_RECEIVED.with(|v| *v.borrow().get(&(BOB, TEST_TOKEN_ID)).unwrap()), + 50 + ); + + let transferred_event = TestEvent::tokens(RawEvent::Transferred(TEST_TOKEN_ID, ALICE, BOB, 50)); + assert!(System::events().iter().any(|record| record.event == transferred_event)); + + assert_noop!( + Tokens::transfer(Some(ALICE).into(), BOB, TEST_TOKEN_ID, 60), + Error::::BalanceTooLow, + ); + }); +} + +#[test] +fn transfer_all_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + System::set_block_number(1); + + assert_ok!(Tokens::transfer_all(Some(ALICE).into(), BOB, TEST_TOKEN_ID)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 200); + + let transferred_event = TestEvent::tokens(RawEvent::Transferred(TEST_TOKEN_ID, ALICE, BOB, 100)); + assert!(System::events().iter().any(|record| record.event == transferred_event)); + }); +} + +#[test] +fn deposit_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_ok!(Tokens::deposit(TEST_TOKEN_ID, &ALICE, 100)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 200); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 300); + + assert_noop!( + Tokens::deposit(TEST_TOKEN_ID, &ALICE, Balance::max_value()), + Error::::TotalIssuanceOverflow, + ); + }); +} + +#[test] +fn withdraw_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_ok!(Tokens::withdraw(TEST_TOKEN_ID, &ALICE, 50)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 150); + + assert_noop!( + Tokens::withdraw(TEST_TOKEN_ID, &ALICE, 60), + Error::::BalanceTooLow + ); + }); +} + +#[test] +fn slash_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + // slashed_amount < amount + assert_eq!(Tokens::slash(TEST_TOKEN_ID, &ALICE, 50), 0); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 50); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 150); + + // slashed_amount == amount + assert_eq!(Tokens::slash(TEST_TOKEN_ID, &ALICE, 51), 1); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 0); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 100); + }); +} + +#[test] +fn update_balance_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_ok!(Tokens::update_balance(TEST_TOKEN_ID, &ALICE, 50)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 150); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 250); + + assert_ok!(Tokens::update_balance(TEST_TOKEN_ID, &BOB, -50)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &BOB), 50); + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 200); + + assert_noop!( + Tokens::update_balance(TEST_TOKEN_ID, &BOB, -60), + Error::::BalanceTooLow + ); + }); +} + +#[test] +fn ensure_can_withdraw_should_work() { + ExtBuilder::default() + .one_hundred_for_alice_n_bob() + .build() + .execute_with(|| { + assert_noop!( + Tokens::ensure_can_withdraw(TEST_TOKEN_ID, &ALICE, 101), + Error::::BalanceTooLow + ); + + assert_ok!(Tokens::ensure_can_withdraw(TEST_TOKEN_ID, &ALICE, 1)); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &ALICE), 100); + }); +} + +#[test] +fn no_op_if_amount_is_zero() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(Tokens::ensure_can_withdraw(TEST_TOKEN_ID, &ALICE, 0)); + assert_ok!(Tokens::transfer(Some(ALICE).into(), BOB, TEST_TOKEN_ID, 0)); + assert_ok!(Tokens::transfer(Some(ALICE).into(), ALICE, TEST_TOKEN_ID, 0)); + assert_ok!(Tokens::deposit(TEST_TOKEN_ID, &ALICE, 0)); + assert_ok!(Tokens::withdraw(TEST_TOKEN_ID, &ALICE, 0)); + assert_eq!(Tokens::slash(TEST_TOKEN_ID, &ALICE, 0), 0); + assert_eq!(Tokens::slash(TEST_TOKEN_ID, &ALICE, 1), 1); + assert_ok!(Tokens::update_balance(TEST_TOKEN_ID, &ALICE, 0)); + }); +} + +#[test] +fn currency_adapter_ensure_currency_adapter_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + assert_eq!(Tokens::total_issuance(TEST_TOKEN_ID), 100); + assert_eq!(Tokens::total_balance(TEST_TOKEN_ID, &TREASURY_ACCOUNT), 100); + // CandidacyBond = 3 VotingBond = 2 + assert_eq!(Tokens::reserved_balance(TEST_TOKEN_ID, &TREASURY_ACCOUNT), 5); + assert_eq!(Tokens::free_balance(TEST_TOKEN_ID, &TREASURY_ACCOUNT), 95); + assert_eq!( + ::Currency::total_balance(&TREASURY_ACCOUNT), + 100 + ); + assert_eq!( + ::Currency::can_slash(&TREASURY_ACCOUNT, 10), + true + ); + assert_eq!( + ::Currency::total_issuance(), + 100 + ); + assert_eq!( + ::Currency::minimum_balance(), + 0 + ); + assert_eq!( + ::Currency::can_reserve(&TREASURY_ACCOUNT, 5), + true + ); + + // burn + let imbalance = ::Currency::burn(10); + assert_eq!( + ::Currency::total_issuance(), + 90 + ); + drop(imbalance); + assert_eq!( + ::Currency::total_issuance(), + 100 + ); + + // issue + let imbalance = ::Currency::issue(20); + assert_eq!( + ::Currency::total_issuance(), + 120 + ); + drop(imbalance); + assert_eq!( + ::Currency::total_issuance(), + 100 + ); + + // transfer + assert_eq!( + ::Currency::free_balance(&TREASURY_ACCOUNT), + 95 + ); + assert_ok!( + ::Currency::ensure_can_withdraw( + &TREASURY_ACCOUNT, + 10, + WithdrawReason::Transfer.into(), + 0 + ) + ); + assert_ok!(::Currency::transfer( + &TREASURY_ACCOUNT, + &ALICE, + 11, + ExistenceRequirement::KeepAlive + )); + assert_eq!( + ::Currency::free_balance(&TREASURY_ACCOUNT), + 84 + ); + + // deposit + assert_eq!( + ::Currency::total_issuance(), + 100 + ); + let imbalance = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 11); + assert_eq!( + ::Currency::free_balance(&TREASURY_ACCOUNT), + 95 + ); + assert_eq!( + ::Currency::total_issuance(), + 100 + ); + drop(imbalance); + assert_eq!( + ::Currency::free_balance(&TREASURY_ACCOUNT), + 95 + ); + assert_eq!( + ::Currency::total_issuance(), + 111 + ); + + // withdraw + let imbalance = ::Currency::withdraw( + &TREASURY_ACCOUNT, + 10, + WithdrawReason::Transfer.into(), + ExistenceRequirement::KeepAlive, + ); + assert_eq!( + ::Currency::free_balance(&TREASURY_ACCOUNT), + 85 + ); + assert_eq!( + ::Currency::total_issuance(), + 111 + ); + drop(imbalance); + assert_eq!( + ::Currency::free_balance(&TREASURY_ACCOUNT), + 85 + ); + assert_eq!( + ::Currency::total_issuance(), + 101 + ); + }); +} + +#[test] +fn currency_adapter_burn_must_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + let init_total_issuance = TreasuryCurrencyAdapter::total_issuance(); + let imbalance = TreasuryCurrencyAdapter::burn(10); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), init_total_issuance - 10); + drop(imbalance); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), init_total_issuance); + }); +} + +#[test] +fn currency_adapter_reserving_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + + assert_eq!(TreasuryCurrencyAdapter::total_balance(&TREASURY_ACCOUNT), 111); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 111); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); + + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 69)); + + assert_eq!(TreasuryCurrencyAdapter::total_balance(&TREASURY_ACCOUNT), 111); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 42); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 69); + }); +} + +#[test] +fn currency_adapter_balance_transfer_when_reserved_should_not_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 69)); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 69, ExistenceRequirement::AllowDeath), + Error::::BalanceTooLow, + ); + }); +} + +#[test] +fn currency_adapter_deducting_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 69)); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 42); + }); +} + +#[test] +fn currency_adapter_refunding_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 42); + Tokens::set_reserved_balance(TEST_TOKEN_ID, &TREASURY_ACCOUNT, 69); + TreasuryCurrencyAdapter::unreserve(&TREASURY_ACCOUNT, 69); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 111); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); + }); +} + +#[test] +fn currency_adapter_slashing_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 69)); + assert!(TreasuryCurrencyAdapter::slash(&TREASURY_ACCOUNT, 69).1.is_zero()); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 42); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 42); + }); +} + +#[test] +fn currency_adapter_slashing_incomplete_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 42); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 21)); + assert_eq!(TreasuryCurrencyAdapter::slash(&TREASURY_ACCOUNT, 69).1, 27); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 0); + }); +} + +#[test] +fn currency_adapter_basic_locking_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + // CandidacyBond = 3 VotingBond = 2 + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 95); + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, 91, WithdrawReasons::all()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 5, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + }); +} + +#[test] +fn currency_adapter_partial_locking_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, 5, WithdrawReasons::all()); + assert_ok!(TreasuryCurrencyAdapter::transfer( + &TREASURY_ACCOUNT, + &ALICE, + 1, + ExistenceRequirement::AllowDeath + )); + }); +} + +#[test] +fn currency_adapter_lock_removal_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, u64::max_value(), WithdrawReasons::all()); + TreasuryCurrencyAdapter::remove_lock(ID_1, &TREASURY_ACCOUNT); + assert_ok!(TreasuryCurrencyAdapter::transfer( + &TREASURY_ACCOUNT, + &ALICE, + 1, + ExistenceRequirement::AllowDeath + )); + }); +} + +#[test] +fn currency_adapter_lock_replacement_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, u64::max_value(), WithdrawReasons::all()); + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, 5, WithdrawReasons::all()); + assert_ok!(TreasuryCurrencyAdapter::transfer( + &TREASURY_ACCOUNT, + &ALICE, + 1, + ExistenceRequirement::AllowDeath + )); + }); +} + +#[test] +fn currency_adapter_double_locking_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, 5, WithdrawReasons::none()); + TreasuryCurrencyAdapter::set_lock(ID_2, &TREASURY_ACCOUNT, 5, WithdrawReasons::all()); + assert_ok!(TreasuryCurrencyAdapter::transfer( + &TREASURY_ACCOUNT, + &ALICE, + 1, + ExistenceRequirement::AllowDeath + )); + }); +} + +#[test] +fn currency_adapter_combination_locking_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + // withdrawReasons not work + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, u64::max_value(), WithdrawReasons::none()); + TreasuryCurrencyAdapter::set_lock(ID_2, &TREASURY_ACCOUNT, 0, WithdrawReasons::all()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 1, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + }); +} + +#[test] +fn currency_adapter_lock_value_extension_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, 100, WithdrawReasons::all()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 6, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + TreasuryCurrencyAdapter::extend_lock(ID_1, &TREASURY_ACCOUNT, 2, WithdrawReasons::all()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 6, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + TreasuryCurrencyAdapter::extend_lock(ID_1, &TREASURY_ACCOUNT, 8, WithdrawReasons::all()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 3, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + }); +} + +#[test] +fn currency_adapter_lock_block_number_extension_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, 200, WithdrawReasons::all()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 6, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + TreasuryCurrencyAdapter::extend_lock(ID_1, &TREASURY_ACCOUNT, 90, WithdrawReasons::all()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 6, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + System::set_block_number(2); + TreasuryCurrencyAdapter::extend_lock(ID_1, &TREASURY_ACCOUNT, 90, WithdrawReasons::all()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 3, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + }); +} + +#[test] +fn currency_adapter_lock_reasons_extension_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + TreasuryCurrencyAdapter::set_lock(ID_1, &TREASURY_ACCOUNT, 90, WithdrawReason::Transfer.into()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 6, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + TreasuryCurrencyAdapter::extend_lock(ID_1, &TREASURY_ACCOUNT, 90, WithdrawReasons::none()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 6, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + TreasuryCurrencyAdapter::extend_lock(ID_1, &TREASURY_ACCOUNT, 90, WithdrawReason::Reserve.into()); + assert_noop!( + TreasuryCurrencyAdapter::transfer(&TREASURY_ACCOUNT, &ALICE, 6, ExistenceRequirement::AllowDeath), + Error::::LiquidityRestrictions + ); + }); +} + +#[test] +fn currency_adapter_reward_should_work() { + ExtBuilder::default() + .one_hundred_for_treasury_account() + .build() + .execute_with(|| { + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 100); + assert_eq!(TreasuryCurrencyAdapter::total_balance(&TREASURY_ACCOUNT), 100); + assert_ok!(TreasuryCurrencyAdapter::deposit_into_existing(&TREASURY_ACCOUNT, 10).map(drop)); + assert_eq!(TreasuryCurrencyAdapter::total_balance(&TREASURY_ACCOUNT), 110); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 110); + }); +} + +#[test] +fn currency_adapter_slashing_reserved_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 111)); + assert_eq!(TreasuryCurrencyAdapter::slash_reserved(&TREASURY_ACCOUNT, 42).1, 0); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 69); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 69); + }); +} + +#[test] +fn currency_adapter_slashing_incomplete_reserved_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 42)); + assert_eq!(TreasuryCurrencyAdapter::slash_reserved(&TREASURY_ACCOUNT, 69).1, 27); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 69); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::total_issuance(), 69); + }); +} + +#[test] +fn currency_adapter_repatriating_reserved_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 110); + let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 1); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 110)); + assert_ok!( + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, Status::Free), + 0 + ); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 69); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&ALICE), 0); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&ALICE), 42); + }); +} + +#[test] +fn currency_adapter_transferring_reserved_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 110); + let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 1); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 110)); + assert_ok!( + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 41, Status::Reserved), + 0 + ); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 69); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&ALICE), 41); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&ALICE), 1); + }); +} + +#[test] +fn currency_adapter_transferring_reserved_balance_to_nonexistent_should_fail() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 111); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 111)); + assert_ok!(TreasuryCurrencyAdapter::repatriate_reserved( + &TREASURY_ACCOUNT, + &ALICE, + 42, + Status::Free + )); + }); +} + +#[test] +fn currency_adapter_transferring_incomplete_reserved_balance_should_work() { + ExtBuilder::default().build().execute_with(|| { + let _ = TreasuryCurrencyAdapter::deposit_creating(&TREASURY_ACCOUNT, 110); + let _ = TreasuryCurrencyAdapter::deposit_creating(&ALICE, 1); + assert_ok!(TreasuryCurrencyAdapter::reserve(&TREASURY_ACCOUNT, 41)); + assert_ok!( + TreasuryCurrencyAdapter::repatriate_reserved(&TREASURY_ACCOUNT, &ALICE, 69, Status::Free), + 28 + ); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&TREASURY_ACCOUNT), 0); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), 69); + assert_eq!(TreasuryCurrencyAdapter::reserved_balance(&ALICE), 0); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&ALICE), 42); + }); +} + +#[test] +fn currency_adapter_transferring_too_high_value_should_not_panic() { + ExtBuilder::default().build().execute_with(|| { + TreasuryCurrencyAdapter::make_free_balance_be(&TREASURY_ACCOUNT, u64::max_value()); + TreasuryCurrencyAdapter::make_free_balance_be(&ALICE, 1); + + assert_noop!( + TreasuryCurrencyAdapter::transfer( + &TREASURY_ACCOUNT, + &ALICE, + u64::max_value(), + ExistenceRequirement::AllowDeath + ), + Error::::BalanceOverflow, + ); + + assert_eq!( + TreasuryCurrencyAdapter::free_balance(&TREASURY_ACCOUNT), + u64::max_value() + ); + assert_eq!(TreasuryCurrencyAdapter::free_balance(&ALICE), 1); + }); +}