diff --git a/Cargo.lock b/Cargo.lock index 698239ac92..742c133be4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2627,6 +2627,27 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "module-accounts" +version = "0.0.1" +dependencies = [ + "frame-support", + "frame-system", + "module-support", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "pallet-timestamp", + "pallet-transaction-payment", + "parity-scale-codec", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "module-auction-manager" version = "0.0.1" diff --git a/modules/accounts/Cargo.toml b/modules/accounts/Cargo.toml new file mode 100644 index 0000000000..cf04fe8afd --- /dev/null +++ b/modules/accounts/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "module-accounts" +version = "0.0.1" +authors = ["Acala Developers"] +edition = "2018" + +[dependencies] +serde = { version = "1.0", optional = true } +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate.git", default-features = false } +frame-support = { package = "frame-support", git = "https://github.com/paritytech/substrate.git", default-features = false } +system = { package = "frame-system", git = "https://github.com/paritytech/substrate.git", default-features = false } +rstd = { package = "sp-std", git = "https://github.com/paritytech/substrate.git", default-features = false } +orml-traits = { package = "orml-traits", path = "../../orml/traits", default-features = false } +orml-tokens = { package = "orml-tokens", path = "../../orml/tokens", default-features = false } +orml-currencies = { package = "orml-currencies", path = "../../orml/currencies", default-features = false } +support = { package = "module-support", path = "../support", default-features = false } +pallet-transaction-payment = { git = "https://github.com/paritytech/substrate.git", default-features = false } + +[dev-dependencies] +primitives = { package = "sp-core", git = "https://github.com/paritytech/substrate.git", default-features = false } +runtime-io = { package = "sp-io", git = "https://github.com/paritytech/substrate.git", default-features = false } +pallet-balances= { package = "pallet-balances", git = "https://github.com/paritytech/substrate.git", default-features = false } +pallet-timestamp= { package = "pallet-timestamp", git = "https://github.com/paritytech/substrate.git", default-features = false } + + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sp-runtime/std", + "frame-support/std", + "system/std", + "rstd/std", + "orml-traits/std", + "orml-tokens/std", + "orml-currencies/std", + "support/std", + "pallet-transaction-payment/std", +] diff --git a/modules/accounts/src/lib.rs b/modules/accounts/src/lib.rs new file mode 100644 index 0000000000..1032902339 --- /dev/null +++ b/modules/accounts/src/lib.rs @@ -0,0 +1,167 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use frame_support::{ + decl_module, decl_storage, + dispatch::Dispatchable, + traits::{Currency, ExistenceRequirement, Get, OnReapAccount, OnUnbalanced, Time, WithdrawReason}, + weights::DispatchInfo, + IsSubType, Parameter, +}; +use orml_traits::MultiCurrency; +use rstd::prelude::*; +use sp_runtime::{ + traits::{SaturatedConversion, Saturating, SignedExtension, Zero}, + transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionValidity, TransactionValidityError, ValidTransaction, + }, +}; + +mod mock; +mod tests; + +//type BalanceOf = <::Currency as MultiCurrency<::AccountId>>::Balance; +type MomentOf = <::Time as Time>::Moment; +type PalletBalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +pub trait Trait: system::Trait + pallet_transaction_payment::Trait + orml_currencies::Trait { + type FreeTransferCount: Get; + type FreeTransferPeriod: Get>; + type Time: Time; + type Currency: MultiCurrency + Send + Sync; + type Call: Parameter + + Dispatchable::Origin> + + IsSubType, Self>; +} + +decl_storage! { + trait Store for Module as Accounts { + LastFreeTransfers get(fn last_free_transfers): map T::AccountId => Vec>; + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + const FreeTransferCount: u8 = T::FreeTransferCount::get(); + const FreeTransferPeriod: MomentOf = T::FreeTransferPeriod::get(); + } +} + +impl Module { + pub fn try_free_transfer(who: &T::AccountId) -> bool { + let mut last_free_transfer = Self::last_free_transfers(who); + let now = T::Time::now(); + let free_transfer_period = T::FreeTransferPeriod::get(); + + // remove all the expired entries + last_free_transfer.retain(|&x| x.saturating_add(free_transfer_period) > now); + + // check if can transfer for free + if last_free_transfer.len() < T::FreeTransferCount::get() as usize { + // add entry to last_free_transfer + last_free_transfer.push(now); + >::insert(who, last_free_transfer); + true + } else { + false + } + } +} + +impl OnReapAccount for Module { + fn on_reap_account(who: &T::AccountId) { + >::remove(who); + } +} + +/// Require the transactor pay for themselves and maybe include a tip to gain additional priority +/// in the queue. +#[derive(Encode, Decode, Clone, Eq, PartialEq)] +pub struct ChargeTransactionPayment(#[codec(compact)] PalletBalanceOf); + +impl ChargeTransactionPayment { + /// utility constructor. Used only in client/factory code. + pub fn from(fee: PalletBalanceOf) -> Self { + Self(fee) + } +} + +impl rstd::fmt::Debug for ChargeTransactionPayment { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut rstd::fmt::Formatter) -> rstd::fmt::Result { + write!(f, "ChargeTransactionPayment<{:?}>", self.0) + } + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut rstd::fmt::Formatter) -> rstd::fmt::Result { + Ok(()) + } +} + +impl SignedExtension for ChargeTransactionPayment +where + PalletBalanceOf: Send + Sync, +{ + type AccountId = T::AccountId; + type Call = ::Call; + type AdditionalSigned = (); + type DispatchInfo = DispatchInfo; + type Pre = (); + fn additional_signed(&self) -> rstd::result::Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: Self::DispatchInfo, + len: usize, + ) -> TransactionValidity { + // pay any fees. + let tip = self.0; + let fee: PalletBalanceOf = + pallet_transaction_payment::ChargeTransactionPayment::::compute_fee(len as u32, info, tip); + + // check call type + let call = match call.is_sub_type() { + Some(call) => call, + None => return Ok(ValidTransaction::default()), + }; + let skip_pay_fee = match call { + orml_currencies::Call::transfer(..) => { + // call try_free_transfer + if >::try_free_transfer(who) { + true + } else { + false + } + } + _ => false, + }; + + // skip payment withdraw if match conditions + if !skip_pay_fee { + let imbalance = match ::Currency::withdraw( + who, + fee, + if tip.is_zero() { + WithdrawReason::TransactionPayment.into() + } else { + WithdrawReason::TransactionPayment | WithdrawReason::Tip + }, + ExistenceRequirement::KeepAlive, + ) { + Ok(imbalance) => imbalance, + Err(_) => return InvalidTransaction::Payment.into(), + }; + ::OnTransactionPayment::on_unbalanced(imbalance); + } + + let mut r = ValidTransaction::default(); + // NOTE: we probably want to maximize the _fee (of any type) per weight unit_ here, which + // will be a bit more than setting the priority to tip. For now, this is enough. + r.priority = fee.saturated_into::(); + Ok(r) + } +} diff --git a/modules/accounts/src/mock.rs b/modules/accounts/src/mock.rs new file mode 100644 index 0000000000..23324513e9 --- /dev/null +++ b/modules/accounts/src/mock.rs @@ -0,0 +1,168 @@ +//! Mocks for the cdp treasury module. + +#![cfg(test)] + +use super::*; +use frame_support::{impl_outer_dispatch, impl_outer_origin, parameter_types}; +use primitives::H256; +use sp_runtime::{ + testing::Header, + traits::{ConvertInto, IdentityLookup}, + Perbill, +}; + +impl_outer_origin! { + pub enum Origin for Runtime {} +} + +impl_outer_dispatch! { + pub enum Call for Runtime where origin: Origin { + orml_currencies::Currencies, + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const ExistentialDeposit: u64 = 0; + pub const TransferFee: u64 = 0; + pub const CreationFee: u64 = 2; + pub const GetNativeCurrencyId: CurrencyId = ACA; +} + +pub type AccountId = u64; +pub type BlockNumber = u64; +pub type Balance = u64; +pub type Amount = i64; +pub type CurrencyId = u32; +pub type Moment = u64; + +pub const ALICE: AccountId = 0; +pub const BOB: AccountId = 1; +pub const ACA: CurrencyId = 0; +pub const AUSD: CurrencyId = 1; +pub const BTC: CurrencyId = 2; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; + +impl system::Trait for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = (); + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); +} + +impl orml_tokens::Trait for Runtime { + type Event = (); + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type ExistentialDeposit = ExistentialDeposit; + type DustRemoval = (); +} +pub type Tokens = orml_tokens::Module; + +impl pallet_balances::Trait for Runtime { + type Balance = Balance; + type OnFreeBalanceZero = (); + type OnNewAccount = (); + type OnReapAccount = (); + type TransferPayment = (); + type DustRemoval = (); + type Event = (); + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; +} +pub type PalletBalances = pallet_balances::Module; + +pub type AdaptedBasicCurrency = orml_currencies::BasicCurrencyAdapter; + +impl orml_currencies::Trait for Runtime { + type Event = (); + type MultiCurrency = Tokens; + type NativeCurrency = AdaptedBasicCurrency; + type GetNativeCurrencyId = GetNativeCurrencyId; +} +pub type Currencies = orml_currencies::Module; + +parameter_types! { + pub const MinimumPeriod: Moment = 5; +} + +impl pallet_timestamp::Trait for Runtime { + type Moment = Moment; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; +} +pub type TimeModule = pallet_timestamp::Module; + +parameter_types! { + pub const TransactionBaseFee: Balance = 0; + pub const TransactionByteFee: Balance = 2; +} + +impl pallet_transaction_payment::Trait for Runtime { + type Currency = PalletBalances; + type OnTransactionPayment = (); + type TransactionBaseFee = TransactionBaseFee; + type TransactionByteFee = TransactionByteFee; + type WeightToFee = ConvertInto; + type FeeMultiplierUpdate = (); +} +pub type PalletTransactionPayment = pallet_transaction_payment::Module; + +parameter_types! { + pub const FreeTransferCount: u8 = 3; + pub const FreeTransferPeriod: Moment = 100; +} + +impl Trait for Runtime { + type FreeTransferCount = FreeTransferCount; + type FreeTransferPeriod = FreeTransferPeriod; + type Time = TimeModule; + type Currency = Currencies; + type Call = Call; +} +pub type Accounts = Module; + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + endowed_accounts: vec![(ALICE, ACA, 1000), (ALICE, AUSD, 1000), (ALICE, BTC, 1000)], + } + } +} + +impl ExtBuilder { + pub fn build(self) -> runtime_io::TestExternalities { + let mut t = system::GenesisConfig::default().build_storage::().unwrap(); + + orml_tokens::GenesisConfig:: { + endowed_accounts: self.endowed_accounts, + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() + } +} diff --git a/modules/accounts/src/tests.rs b/modules/accounts/src/tests.rs new file mode 100644 index 0000000000..724840a867 --- /dev/null +++ b/modules/accounts/src/tests.rs @@ -0,0 +1,40 @@ +//! Unit tests for the accounts module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok}; +use mock::{Accounts, Currencies, ExtBuilder, Origin, Runtime, TimeModule, ALICE, AUSD, BOB, BTC}; +use sp_runtime::traits::OnFinalize; + +#[test] +fn try_free_transfer_over_cap() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(TimeModule::now(), 0); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![]); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0]); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0, 0]); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0, 0, 0]); + assert_eq!(Accounts::try_free_transfer(&ALICE), false); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0, 0, 0]); + }); +} + +#[test] +fn remove_expired_entry() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(TimeModule::now(), 0); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![]); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0, 0, 0]); + assert_ok!(TimeModule::dispatch(pallet_timestamp::Call::set(100), Origin::NONE)); + assert_eq!(TimeModule::now(), 100); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![100]); + }); +} diff --git a/modules/cdp_treasury/src/mock.rs b/modules/cdp_treasury/src/mock.rs index bbff415bdf..8e9938f0b5 100644 --- a/modules/cdp_treasury/src/mock.rs +++ b/modules/cdp_treasury/src/mock.rs @@ -6,7 +6,6 @@ use super::*; use frame_support::{impl_outer_origin, parameter_types}; use primitives::H256; use sp_runtime::{testing::Header, traits::IdentityLookup, Perbill}; -use support::Rate; impl_outer_origin! { pub enum Origin for Runtime {} @@ -22,10 +21,6 @@ parameter_types! { pub const CreationFee: u64 = 2; pub const GetStableCurrencyId: CurrencyId = AUSD; pub const GetNativeCurrencyId: CurrencyId = ACA; - pub const MinimumIncrementSize: Rate = Rate::from_rational(1, 20); - pub const AuctionTimeToClose: u64 = 100; - pub const AuctionDurationSoftCap: u64 = 2000; - pub const GetAmountAdjustment: Rate = Rate::from_rational(1, 2); } pub type AccountId = u64;