mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-01 12:27:56 +00:00
Non-Interactive Staking (#12610)
* Improve naming. * More improvements to naming * Fungible counterpart * Shared pot instead of reserve * Transferable receipts * Better naming * Use u128 for counterpart * Partial thawing * Docs * Remove AdminOrigin * Integrate into Kitchen Sink * Thaw throttling * Remove todo * Docs * Fix benchmarks * Building * Tests work * New benchmarks * Benchmarking tests * Test new defensive_saturating_* functions Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * fmt Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Formatting * Update frame/nis/src/lib.rs Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Apply suggestions from code review Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Events added * Fix kitchensink * Update frame/nis/src/lib.rs Co-authored-by: Xiliang Chen <xlchen1291@gmail.com> * Review niggles * Remove genesis build requirements * Grumbles * Fixes * Fixes * Fixes * Update frame/nis/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Update primitives/runtime/src/traits.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Formatting * Fixes * Fix node genesis config Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Fix node chain specs Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Use free asset ID as counterpart Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Account for rounding errors in fund_deficit bench Relaxes the check for the NIS account balance in the fund_deficit bench from equality from to checking for 99.999% equality. The exact deviation for the kitchensink runtime config is 1.24e-10 percent but could vary if the config is changed. Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * clippy Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * fmt Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Fix * Rename * Fixes * Fixes * Formatting Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: Xiliang Chen <xlchen1291@gmail.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Generated
+20
-18
@@ -3314,7 +3314,6 @@ dependencies = [
|
||||
"pallet-election-provider-support-benchmarking",
|
||||
"pallet-elections-phragmen",
|
||||
"pallet-fast-unstake",
|
||||
"pallet-gilt",
|
||||
"pallet-grandpa",
|
||||
"pallet-identity",
|
||||
"pallet-im-online",
|
||||
@@ -3323,6 +3322,7 @@ dependencies = [
|
||||
"pallet-membership",
|
||||
"pallet-mmr",
|
||||
"pallet-multisig",
|
||||
"pallet-nis",
|
||||
"pallet-nomination-pools",
|
||||
"pallet-nomination-pools-benchmarking",
|
||||
"pallet-nomination-pools-runtime-api",
|
||||
@@ -4473,6 +4473,7 @@ dependencies = [
|
||||
"node-primitives",
|
||||
"node-rpc",
|
||||
"pallet-asset-tx-payment",
|
||||
"pallet-assets",
|
||||
"pallet-balances",
|
||||
"pallet-im-online",
|
||||
"pallet-timestamp",
|
||||
@@ -4731,6 +4732,7 @@ dependencies = [
|
||||
"node-executor",
|
||||
"node-primitives",
|
||||
"pallet-asset-tx-payment",
|
||||
"pallet-assets",
|
||||
"pallet-transaction-payment",
|
||||
"parity-scale-codec",
|
||||
"sc-block-builder",
|
||||
@@ -5450,23 +5452,6 @@ dependencies = [
|
||||
"substrate-test-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-gilt"
|
||||
version = "4.0.0-dev"
|
||||
dependencies = [
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"pallet-balances",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"sp-arithmetic",
|
||||
"sp-core",
|
||||
"sp-io",
|
||||
"sp-runtime",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-grandpa"
|
||||
version = "4.0.0-dev"
|
||||
@@ -5635,6 +5620,23 @@ dependencies = [
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-nis"
|
||||
version = "4.0.0-dev"
|
||||
dependencies = [
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"pallet-balances",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"sp-arithmetic",
|
||||
"sp-core",
|
||||
"sp-io",
|
||||
"sp-runtime",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-node-authorization"
|
||||
version = "4.0.0-dev"
|
||||
|
||||
@@ -101,7 +101,7 @@ members = [
|
||||
"frame/examples/basic",
|
||||
"frame/examples/offchain-worker",
|
||||
"frame/executive",
|
||||
"frame/gilt",
|
||||
"frame/nis",
|
||||
"frame/grandpa",
|
||||
"frame/identity",
|
||||
"frame/im-online",
|
||||
|
||||
@@ -84,6 +84,7 @@ sc-sysinfo = { version = "6.0.0-dev", path = "../../../client/sysinfo" }
|
||||
frame-system = { version = "4.0.0-dev", path = "../../../frame/system" }
|
||||
frame-system-rpc-runtime-api = { version = "4.0.0-dev", path = "../../../frame/system/rpc/runtime-api" }
|
||||
pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" }
|
||||
pallet-assets = { version = "4.0.0-dev", path = "../../../frame/assets/" }
|
||||
pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment/" }
|
||||
pallet-im-online = { version = "4.0.0-dev", default-features = false, path = "../../../frame/im-online" }
|
||||
|
||||
|
||||
@@ -358,8 +358,11 @@ pub fn testnet_genesis(
|
||||
max_members: 999,
|
||||
},
|
||||
vesting: Default::default(),
|
||||
assets: Default::default(),
|
||||
gilt: Default::default(),
|
||||
assets: pallet_assets::GenesisConfig {
|
||||
// This asset is used by the NIS pallet as counterpart currency.
|
||||
assets: vec![(9, get_account_id_from_seed::<sr25519::Public>("Alice"), true, 1)],
|
||||
..Default::default()
|
||||
},
|
||||
transaction_storage: Default::default(),
|
||||
transaction_payment: Default::default(),
|
||||
alliance: Default::default(),
|
||||
|
||||
@@ -68,7 +68,7 @@ pallet-election-provider-multi-phase = { version = "4.0.0-dev", default-features
|
||||
pallet-election-provider-support-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-support/benchmarking", optional = true }
|
||||
pallet-elections-phragmen = { version = "5.0.0-dev", default-features = false, path = "../../../frame/elections-phragmen" }
|
||||
pallet-fast-unstake = { version = "4.0.0-dev", default-features = false, path = "../../../frame/fast-unstake" }
|
||||
pallet-gilt = { version = "4.0.0-dev", default-features = false, path = "../../../frame/gilt" }
|
||||
pallet-nis = { version = "4.0.0-dev", default-features = false, path = "../../../frame/nis" }
|
||||
pallet-grandpa = { version = "4.0.0-dev", default-features = false, path = "../../../frame/grandpa" }
|
||||
pallet-im-online = { version = "4.0.0-dev", default-features = false, path = "../../../frame/im-online" }
|
||||
pallet-indices = { version = "4.0.0-dev", default-features = false, path = "../../../frame/indices" }
|
||||
@@ -144,7 +144,7 @@ std = [
|
||||
"pallet-elections-phragmen/std",
|
||||
"pallet-fast-unstake/std",
|
||||
"frame-executive/std",
|
||||
"pallet-gilt/std",
|
||||
"pallet-nis/std",
|
||||
"pallet-grandpa/std",
|
||||
"pallet-im-online/std",
|
||||
"pallet-indices/std",
|
||||
@@ -223,7 +223,7 @@ runtime-benchmarks = [
|
||||
"pallet-election-provider-support-benchmarking/runtime-benchmarks",
|
||||
"pallet-elections-phragmen/runtime-benchmarks",
|
||||
"pallet-fast-unstake/runtime-benchmarks",
|
||||
"pallet-gilt/runtime-benchmarks",
|
||||
"pallet-nis/runtime-benchmarks",
|
||||
"pallet-grandpa/runtime-benchmarks",
|
||||
"pallet-identity/runtime-benchmarks",
|
||||
"pallet-im-online/runtime-benchmarks",
|
||||
@@ -276,7 +276,7 @@ try-runtime = [
|
||||
"pallet-election-provider-multi-phase/try-runtime",
|
||||
"pallet-elections-phragmen/try-runtime",
|
||||
"pallet-fast-unstake/try-runtime",
|
||||
"pallet-gilt/try-runtime",
|
||||
"pallet-nis/try-runtime",
|
||||
"pallet-grandpa/try-runtime",
|
||||
"pallet-im-online/try-runtime",
|
||||
"pallet-indices/try-runtime",
|
||||
|
||||
@@ -32,9 +32,10 @@ use frame_support::{
|
||||
pallet_prelude::Get,
|
||||
parameter_types,
|
||||
traits::{
|
||||
AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, Currency, EitherOfDiverse,
|
||||
EqualPrivilegeOnly, Everything, Imbalance, InstanceFilter, KeyOwnerProofSystem,
|
||||
LockIdentifier, Nothing, OnUnbalanced, U128CurrencyToVote, WithdrawReasons,
|
||||
fungible::ItemOf, AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32,
|
||||
Currency, EitherOfDiverse, EqualPrivilegeOnly, Everything, Imbalance, InstanceFilter,
|
||||
KeyOwnerProofSystem, LockIdentifier, Nothing, OnUnbalanced, U128CurrencyToVote,
|
||||
WithdrawReasons,
|
||||
},
|
||||
weights::{
|
||||
constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},
|
||||
@@ -53,6 +54,7 @@ use pallet_grandpa::{
|
||||
fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList,
|
||||
};
|
||||
use pallet_im_online::sr25519::AuthorityId as ImOnlineId;
|
||||
use pallet_nis::WithMaximumOf;
|
||||
use pallet_session::historical::{self as pallet_session_historical};
|
||||
pub use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustment};
|
||||
use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo};
|
||||
@@ -1464,28 +1466,37 @@ parameter_types! {
|
||||
pub const QueueCount: u32 = 300;
|
||||
pub const MaxQueueLen: u32 = 1000;
|
||||
pub const FifoQueueLen: u32 = 500;
|
||||
pub const Period: BlockNumber = 30 * DAYS;
|
||||
pub const MinFreeze: Balance = 100 * DOLLARS;
|
||||
pub const NisBasePeriod: BlockNumber = 30 * DAYS;
|
||||
pub const MinBid: Balance = 100 * DOLLARS;
|
||||
pub const MinReceipt: Perquintill = Perquintill::from_percent(1);
|
||||
pub const IntakePeriod: BlockNumber = 10;
|
||||
pub const MaxIntakeBids: u32 = 10;
|
||||
pub MaxIntakeWeight: Weight = MAXIMUM_BLOCK_WEIGHT / 10;
|
||||
pub const ThawThrottle: (Perquintill, BlockNumber) = (Perquintill::from_percent(25), 5);
|
||||
pub Target: Perquintill = Perquintill::zero();
|
||||
pub const NisPalletId: PalletId = PalletId(*b"py/nis ");
|
||||
}
|
||||
|
||||
impl pallet_gilt::Config for Runtime {
|
||||
impl pallet_nis::Config for Runtime {
|
||||
type WeightInfo = pallet_nis::weights::SubstrateWeight<Runtime>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type CurrencyBalance = Balance;
|
||||
type AdminOrigin = frame_system::EnsureRoot<AccountId>;
|
||||
type FundOrigin = frame_system::EnsureSigned<AccountId>;
|
||||
type Counterpart = ItemOf<Assets, ConstU32<9u32>, AccountId>;
|
||||
type CounterpartAmount = WithMaximumOf<ConstU128<21_000_000_000_000_000_000u128>>;
|
||||
type Deficit = ();
|
||||
type Surplus = ();
|
||||
type IgnoredIssuance = IgnoredIssuance;
|
||||
type Target = Target;
|
||||
type PalletId = NisPalletId;
|
||||
type QueueCount = QueueCount;
|
||||
type MaxQueueLen = MaxQueueLen;
|
||||
type FifoQueueLen = FifoQueueLen;
|
||||
type Period = Period;
|
||||
type MinFreeze = MinFreeze;
|
||||
type BasePeriod = NisBasePeriod;
|
||||
type MinBid = MinBid;
|
||||
type MinReceipt = MinReceipt;
|
||||
type IntakePeriod = IntakePeriod;
|
||||
type MaxIntakeBids = MaxIntakeBids;
|
||||
type WeightInfo = pallet_gilt::weights::SubstrateWeight<Runtime>;
|
||||
type MaxIntakeWeight = MaxIntakeWeight;
|
||||
type ThawThrottle = ThawThrottle;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
@@ -1668,7 +1679,7 @@ construct_runtime!(
|
||||
Assets: pallet_assets,
|
||||
Mmr: pallet_mmr,
|
||||
Lottery: pallet_lottery,
|
||||
Gilt: pallet_gilt,
|
||||
Nis: pallet_nis,
|
||||
Uniques: pallet_uniques,
|
||||
TransactionStorage: pallet_transaction_storage,
|
||||
VoterList: pallet_bags_list::<Instance1>,
|
||||
@@ -1772,7 +1783,7 @@ mod benches {
|
||||
[pallet_election_provider_support_benchmarking, EPSBench::<Runtime>]
|
||||
[pallet_elections_phragmen, Elections]
|
||||
[pallet_fast_unstake, FastUnstake]
|
||||
[pallet_gilt, Gilt]
|
||||
[pallet_nis, Nis]
|
||||
[pallet_grandpa, Grandpa]
|
||||
[pallet_identity, Identity]
|
||||
[pallet_im_online, ImOnline]
|
||||
|
||||
@@ -22,6 +22,7 @@ frame-system = { version = "4.0.0-dev", path = "../../../frame/system" }
|
||||
node-executor = { version = "3.0.0-dev", path = "../executor" }
|
||||
node-primitives = { version = "2.0.0", path = "../primitives" }
|
||||
kitchensink-runtime = { version = "3.0.0-dev", path = "../runtime" }
|
||||
pallet-assets = { version = "4.0.0-dev", path = "../../../frame/assets" }
|
||||
pallet-asset-tx-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment/asset-tx-payment" }
|
||||
pallet-transaction-payment = { version = "4.0.0-dev", path = "../../../frame/transaction-payment" }
|
||||
sc-block-builder = { version = "0.10.0-dev", path = "../../../client/block-builder" }
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
|
||||
use crate::keyring::*;
|
||||
use kitchensink_runtime::{
|
||||
constants::currency::*, wasm_binary_unwrap, AccountId, BabeConfig, BalancesConfig,
|
||||
GenesisConfig, GrandpaConfig, IndicesConfig, SessionConfig, SocietyConfig, StakerStatus,
|
||||
StakingConfig, SystemConfig, BABE_GENESIS_EPOCH_CONFIG,
|
||||
constants::currency::*, wasm_binary_unwrap, AccountId, AssetsConfig, BabeConfig,
|
||||
BalancesConfig, GenesisConfig, GrandpaConfig, IndicesConfig, SessionConfig, SocietyConfig,
|
||||
StakerStatus, StakingConfig, SystemConfig, BABE_GENESIS_EPOCH_CONFIG,
|
||||
};
|
||||
use sp_keyring::{Ed25519Keyring, Sr25519Keyring};
|
||||
use sp_runtime::Perbill;
|
||||
@@ -88,8 +88,7 @@ pub fn config_endowed(code: Option<&[u8]>, extra_endowed: Vec<AccountId>) -> Gen
|
||||
treasury: Default::default(),
|
||||
society: SocietyConfig { members: vec![alice(), bob()], pot: 0, max_members: 999 },
|
||||
vesting: Default::default(),
|
||||
assets: Default::default(),
|
||||
gilt: Default::default(),
|
||||
assets: AssetsConfig { assets: vec![(9, alice(), true, 1)], ..Default::default() },
|
||||
transaction_storage: Default::default(),
|
||||
transaction_payment: Default::default(),
|
||||
alliance: Default::default(),
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2021-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Benchmarks for Gilt Pallet
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use frame_benchmarking::{benchmarks, whitelisted_caller};
|
||||
use frame_support::{
|
||||
dispatch::UnfilteredDispatchable,
|
||||
traits::{Currency, EnsureOrigin, Get},
|
||||
};
|
||||
use frame_system::RawOrigin;
|
||||
use sp_arithmetic::Perquintill;
|
||||
use sp_runtime::traits::{Bounded, Zero};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use crate::Pallet as Gilt;
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
|
||||
benchmarks! {
|
||||
place_bid {
|
||||
let l in 0..(T::MaxQueueLen::get() - 1);
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
for i in 0..l {
|
||||
Gilt::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinFreeze::get(), 1)?;
|
||||
}
|
||||
}: _(RawOrigin::Signed(caller.clone()), T::MinFreeze::get() * BalanceOf::<T>::from(2u32), 1)
|
||||
verify {
|
||||
assert_eq!(QueueTotals::<T>::get()[0], (l + 1, T::MinFreeze::get() * BalanceOf::<T>::from(l + 2)));
|
||||
}
|
||||
|
||||
place_bid_max {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let origin = RawOrigin::Signed(caller.clone());
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
for i in 0..T::MaxQueueLen::get() {
|
||||
Gilt::<T>::place_bid(origin.clone().into(), T::MinFreeze::get(), 1)?;
|
||||
}
|
||||
}: place_bid(origin, T::MinFreeze::get() * BalanceOf::<T>::from(2u32), 1)
|
||||
verify {
|
||||
assert_eq!(QueueTotals::<T>::get()[0], (
|
||||
T::MaxQueueLen::get(),
|
||||
T::MinFreeze::get() * BalanceOf::<T>::from(T::MaxQueueLen::get() + 1),
|
||||
));
|
||||
}
|
||||
|
||||
retract_bid {
|
||||
let l in 1..T::MaxQueueLen::get();
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
for i in 0..l {
|
||||
Gilt::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinFreeze::get(), 1)?;
|
||||
}
|
||||
}: _(RawOrigin::Signed(caller.clone()), T::MinFreeze::get(), 1)
|
||||
verify {
|
||||
assert_eq!(QueueTotals::<T>::get()[0], (l - 1, T::MinFreeze::get() * BalanceOf::<T>::from(l - 1)));
|
||||
}
|
||||
|
||||
set_target {
|
||||
let origin = T::AdminOrigin::successful_origin();
|
||||
}: _<T::RuntimeOrigin>(origin, Default::default())
|
||||
verify {}
|
||||
|
||||
thaw {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, T::MinFreeze::get() * BalanceOf::<T>::from(3u32));
|
||||
Gilt::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinFreeze::get(), 1)?;
|
||||
Gilt::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinFreeze::get(), 1)?;
|
||||
Gilt::<T>::enlarge(T::MinFreeze::get() * BalanceOf::<T>::from(2u32), 2);
|
||||
Active::<T>::mutate(0, |m_g| if let Some(ref mut g) = m_g { g.expiry = Zero::zero() });
|
||||
}: _(RawOrigin::Signed(caller.clone()), 0)
|
||||
verify {
|
||||
assert!(Active::<T>::get(0).is_none());
|
||||
}
|
||||
|
||||
pursue_target_noop {
|
||||
}: { Gilt::<T>::pursue_target(0) }
|
||||
|
||||
pursue_target_per_item {
|
||||
// bids taken
|
||||
let b in 0..T::MaxQueueLen::get();
|
||||
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, T::MinFreeze::get() * BalanceOf::<T>::from(b + 1));
|
||||
|
||||
for _ in 0..b {
|
||||
Gilt::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinFreeze::get(), 1)?;
|
||||
}
|
||||
|
||||
Call::<T>::set_target { target: Perquintill::from_percent(100) }
|
||||
.dispatch_bypass_filter(T::AdminOrigin::successful_origin())?;
|
||||
|
||||
}: { Gilt::<T>::pursue_target(b) }
|
||||
|
||||
pursue_target_per_queue {
|
||||
// total queues hit
|
||||
let q in 0..T::QueueCount::get();
|
||||
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, T::MinFreeze::get() * BalanceOf::<T>::from(q + 1));
|
||||
|
||||
for i in 0..q {
|
||||
Gilt::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinFreeze::get(), i + 1)?;
|
||||
}
|
||||
|
||||
Call::<T>::set_target { target: Perquintill::from_percent(100) }
|
||||
.dispatch_bypass_filter(T::AdminOrigin::successful_origin())?;
|
||||
|
||||
}: { Gilt::<T>::pursue_target(q) }
|
||||
|
||||
impl_benchmark_test_suite!(Gilt, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -1,662 +0,0 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! # Gilt Pallet
|
||||
//! A pallet allowing accounts to auction for being frozen and receive open-ended
|
||||
//! inflation-protection in return.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! Lock up tokens, for at least as long as you offer, and be free from both inflation and
|
||||
//! intermediate reward or exchange until the tokens become unlocked.
|
||||
//!
|
||||
//! ## Design
|
||||
//!
|
||||
//! Queues for each of 1-`QueueCount` periods, given in blocks (`Period`). Queues are limited in
|
||||
//! size to something sensible, `MaxQueueLen`. A secondary storage item with `QueueCount` x `u32`
|
||||
//! elements with the number of items in each queue.
|
||||
//!
|
||||
//! Queues are split into two parts. The first part is a priority queue based on bid size. The
|
||||
//! second part is just a FIFO (the size of the second part is set with `FifoQueueLen`). Items are
|
||||
//! always prepended so that removal is always O(1) since removal often happens many times under a
|
||||
//! single weighed function (`on_initialize`) yet placing bids only ever happens once per weighed
|
||||
//! function (`place_bid`). If the queue has a priority portion, then it remains sorted in order of
|
||||
//! bid size so that smaller bids fall off as it gets too large.
|
||||
//!
|
||||
//! Account may enqueue a balance with some number of `Period`s lock up, up to a maximum of
|
||||
//! `QueueCount`. The balance gets reserved. There's a minimum of `MinFreeze` to avoid dust.
|
||||
//!
|
||||
//! Until your bid is turned into an issued gilt you can retract it instantly and the funds are
|
||||
//! unreserved.
|
||||
//!
|
||||
//! There's a target proportion of effective total issuance (i.e. accounting for existing gilts)
|
||||
//! which the we attempt to have frozen at any one time. It will likely be gradually increased over
|
||||
//! time by governance.
|
||||
//!
|
||||
//! As the total funds frozen under gilts drops below `FrozenFraction` of the total effective
|
||||
//! issuance, then bids are taken from queues, with the queue of the greatest period taking
|
||||
//! priority. If the item in the queue's locked amount is greater than the amount left to be
|
||||
//! frozen, then it is split up into multiple bids and becomes partially frozen under gilt.
|
||||
//!
|
||||
//! Once an account's balance is frozen, it remains frozen until the owner thaws the balance of the
|
||||
//! account. This may happen no earlier than queue's period after the point at which the gilt is
|
||||
//! issued.
|
||||
//!
|
||||
//! ## Suggested Values
|
||||
//!
|
||||
//! - `QueueCount`: 300
|
||||
//! - `Period`: 432,000
|
||||
//! - `MaxQueueLen`: 1000
|
||||
//! - `MinFreeze`: Around CHF 100 in value.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
mod benchmarking;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod weights;
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
pub use crate::weights::WeightInfo;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{Currency, DefensiveSaturating, OnUnbalanced, ReservableCurrency},
|
||||
};
|
||||
use frame_system::pallet_prelude::*;
|
||||
use sp_arithmetic::{PerThing, Perquintill};
|
||||
use sp_runtime::traits::{Saturating, Zero};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
>>::PositiveImbalance;
|
||||
type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
>>::NegativeImbalance;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
/// Overarching event type.
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Currency type that this works on.
|
||||
type Currency: ReservableCurrency<Self::AccountId, Balance = Self::CurrencyBalance>;
|
||||
|
||||
/// Just the `Currency::Balance` type; we have this item to allow us to constrain it to
|
||||
/// `From<u64>`.
|
||||
type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
|
||||
+ codec::FullCodec
|
||||
+ Copy
|
||||
+ MaybeSerializeDeserialize
|
||||
+ sp_std::fmt::Debug
|
||||
+ Default
|
||||
+ From<u64>
|
||||
+ TypeInfo
|
||||
+ MaxEncodedLen;
|
||||
|
||||
/// Origin required for setting the target proportion to be under gilt.
|
||||
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
/// Unbalanced handler to account for funds created (in case of a higher total issuance over
|
||||
/// freezing period).
|
||||
type Deficit: OnUnbalanced<PositiveImbalanceOf<Self>>;
|
||||
|
||||
/// Unbalanced handler to account for funds destroyed (in case of a lower total issuance
|
||||
/// over freezing period).
|
||||
type Surplus: OnUnbalanced<NegativeImbalanceOf<Self>>;
|
||||
|
||||
/// The issuance to ignore. This is subtracted from the `Currency`'s `total_issuance` to get
|
||||
/// the issuance by which we inflate or deflate the gilt.
|
||||
type IgnoredIssuance: Get<BalanceOf<Self>>;
|
||||
|
||||
/// Number of duration queues in total. This sets the maximum duration supported, which is
|
||||
/// this value multiplied by `Period`.
|
||||
#[pallet::constant]
|
||||
type QueueCount: Get<u32>;
|
||||
|
||||
/// Maximum number of items that may be in each duration queue.
|
||||
#[pallet::constant]
|
||||
type MaxQueueLen: Get<u32>;
|
||||
|
||||
/// Portion of the queue which is free from ordering and just a FIFO.
|
||||
///
|
||||
/// Must be no greater than `MaxQueueLen`.
|
||||
#[pallet::constant]
|
||||
type FifoQueueLen: Get<u32>;
|
||||
|
||||
/// The base period for the duration queues. This is the common multiple across all
|
||||
/// supported freezing durations that can be bid upon.
|
||||
#[pallet::constant]
|
||||
type Period: Get<Self::BlockNumber>;
|
||||
|
||||
/// The minimum amount of funds that may be offered to freeze for a gilt. Note that this
|
||||
/// does not actually limit the amount which may be frozen in a gilt since gilts may be
|
||||
/// split up in order to satisfy the desired amount of funds under gilts.
|
||||
///
|
||||
/// It should be at least big enough to ensure that there is no possible storage spam attack
|
||||
/// or queue-filling attack.
|
||||
#[pallet::constant]
|
||||
type MinFreeze: Get<BalanceOf<Self>>;
|
||||
|
||||
/// The number of blocks between consecutive attempts to issue more gilts in an effort to
|
||||
/// get to the target amount to be frozen.
|
||||
///
|
||||
/// A larger value results in fewer storage hits each block, but a slower period to get to
|
||||
/// the target.
|
||||
#[pallet::constant]
|
||||
type IntakePeriod: Get<Self::BlockNumber>;
|
||||
|
||||
/// The maximum amount of bids that can be turned into issued gilts each block. A larger
|
||||
/// value here means less of the block available for transactions should there be a glut of
|
||||
/// bids to make into gilts to reach the target.
|
||||
#[pallet::constant]
|
||||
type MaxIntakeBids: Get<u32>;
|
||||
|
||||
/// Information on runtime weights.
|
||||
type WeightInfo: WeightInfo;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::generate_store(pub(super) trait Store)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// A single bid on a gilt, an item of a *queue* in `Queues`.
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct GiltBid<Balance, AccountId> {
|
||||
/// The amount bid.
|
||||
pub amount: Balance,
|
||||
/// The owner of the bid.
|
||||
pub who: AccountId,
|
||||
}
|
||||
|
||||
/// Information representing an active gilt.
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct ActiveGilt<Balance, AccountId, BlockNumber> {
|
||||
/// The proportion of the effective total issuance (i.e. accounting for any eventual gilt
|
||||
/// expansion or contraction that may eventually be claimed).
|
||||
pub proportion: Perquintill,
|
||||
/// The amount reserved under this gilt.
|
||||
pub amount: Balance,
|
||||
/// The account to whom this gilt belongs.
|
||||
pub who: AccountId,
|
||||
/// The time after which this gilt can be redeemed for the proportional amount of balance.
|
||||
pub expiry: BlockNumber,
|
||||
}
|
||||
|
||||
/// An index for a gilt.
|
||||
pub type ActiveIndex = u32;
|
||||
|
||||
/// Overall information package on the active gilts.
|
||||
///
|
||||
/// The way of determining the net issuance (i.e. after factoring in all maturing frozen funds)
|
||||
/// is:
|
||||
///
|
||||
/// `issuance - frozen + proportion * issuance`
|
||||
///
|
||||
/// where `issuance = total_issuance - IgnoredIssuance`
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct ActiveGiltsTotal<Balance> {
|
||||
/// The total amount of funds held in reserve for all active gilts.
|
||||
pub frozen: Balance,
|
||||
/// The proportion of funds that the `frozen` balance represents to total issuance.
|
||||
pub proportion: Perquintill,
|
||||
/// The total number of gilts issued so far.
|
||||
pub index: ActiveIndex,
|
||||
/// The target proportion of gilts within total issuance.
|
||||
pub target: Perquintill,
|
||||
}
|
||||
|
||||
/// The totals of items and balances within each queue. Saves a lot of storage reads in the
|
||||
/// case of sparsely packed queues.
|
||||
///
|
||||
/// The vector is indexed by duration in `Period`s, offset by one, so information on the queue
|
||||
/// whose duration is one `Period` would be storage `0`.
|
||||
#[pallet::storage]
|
||||
pub type QueueTotals<T: Config> =
|
||||
StorageValue<_, BoundedVec<(u32, BalanceOf<T>), T::QueueCount>, ValueQuery>;
|
||||
|
||||
/// The queues of bids ready to become gilts. Indexed by duration (in `Period`s).
|
||||
#[pallet::storage]
|
||||
pub type Queues<T: Config> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
u32,
|
||||
BoundedVec<GiltBid<BalanceOf<T>, T::AccountId>, T::MaxQueueLen>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
/// Information relating to the gilts currently active.
|
||||
#[pallet::storage]
|
||||
pub type ActiveTotal<T> = StorageValue<_, ActiveGiltsTotal<BalanceOf<T>>, ValueQuery>;
|
||||
|
||||
/// The currently active gilts, indexed according to the order of creation.
|
||||
#[pallet::storage]
|
||||
pub type Active<T> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
ActiveIndex,
|
||||
ActiveGilt<
|
||||
BalanceOf<T>,
|
||||
<T as frame_system::Config>::AccountId,
|
||||
<T as frame_system::Config>::BlockNumber,
|
||||
>,
|
||||
OptionQuery,
|
||||
>;
|
||||
|
||||
#[pallet::genesis_config]
|
||||
#[derive(Default)]
|
||||
pub struct GenesisConfig;
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config> GenesisBuild<T> for GenesisConfig {
|
||||
fn build(&self) {
|
||||
let unbounded = vec![(0, BalanceOf::<T>::zero()); T::QueueCount::get() as usize];
|
||||
let bounded: BoundedVec<_, _> = unbounded
|
||||
.try_into()
|
||||
.expect("QueueTotals should support up to QueueCount items. qed");
|
||||
QueueTotals::<T>::put(bounded);
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// A bid was successfully placed.
|
||||
BidPlaced { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
|
||||
/// A bid was successfully removed (before being accepted as a gilt).
|
||||
BidRetracted { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
|
||||
/// A bid was accepted as a gilt. The balance may not be released until expiry.
|
||||
GiltIssued {
|
||||
index: ActiveIndex,
|
||||
expiry: T::BlockNumber,
|
||||
who: T::AccountId,
|
||||
amount: BalanceOf<T>,
|
||||
},
|
||||
/// An expired gilt has been thawed.
|
||||
GiltThawed {
|
||||
index: ActiveIndex,
|
||||
who: T::AccountId,
|
||||
original_amount: BalanceOf<T>,
|
||||
additional_amount: BalanceOf<T>,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// The duration of the bid is less than one.
|
||||
DurationTooSmall,
|
||||
/// The duration is the bid is greater than the number of queues.
|
||||
DurationTooBig,
|
||||
/// The amount of the bid is less than the minimum allowed.
|
||||
AmountTooSmall,
|
||||
/// The queue for the bid's duration is full and the amount bid is too low to get in
|
||||
/// through replacing an existing bid.
|
||||
BidTooLow,
|
||||
/// Gilt index is unknown.
|
||||
Unknown,
|
||||
/// Not the owner of the gilt.
|
||||
NotOwner,
|
||||
/// Gilt not yet at expiry date.
|
||||
NotExpired,
|
||||
/// The given bid for retraction is not found.
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_initialize(n: T::BlockNumber) -> Weight {
|
||||
if (n % T::IntakePeriod::get()).is_zero() {
|
||||
Self::pursue_target(T::MaxIntakeBids::get())
|
||||
} else {
|
||||
Weight::zero()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Place a bid for a gilt to be issued.
|
||||
///
|
||||
/// Origin must be Signed, and account must have at least `amount` in free balance.
|
||||
///
|
||||
/// - `amount`: The amount of the bid; these funds will be reserved. If the bid is
|
||||
/// successfully elevated into an issued gilt, then these funds will continue to be
|
||||
/// reserved until the gilt expires. Must be at least `MinFreeze`.
|
||||
/// - `duration`: The number of periods for which the funds will be locked if the gilt is
|
||||
/// issued. It will expire only after this period has elapsed after the point of issuance.
|
||||
/// Must be greater than 1 and no more than `QueueCount`.
|
||||
///
|
||||
/// Complexities:
|
||||
/// - `Queues[duration].len()` (just take max).
|
||||
#[pallet::weight(T::WeightInfo::place_bid_max())]
|
||||
pub fn place_bid(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] amount: BalanceOf<T>,
|
||||
duration: u32,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
ensure!(amount >= T::MinFreeze::get(), Error::<T>::AmountTooSmall);
|
||||
let queue_count = T::QueueCount::get() as usize;
|
||||
let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
|
||||
ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
|
||||
|
||||
let net = Queues::<T>::try_mutate(
|
||||
duration,
|
||||
|q| -> Result<(u32, BalanceOf<T>), DispatchError> {
|
||||
let queue_full = q.len() == T::MaxQueueLen::get() as usize;
|
||||
ensure!(!queue_full || q[0].amount < amount, Error::<T>::BidTooLow);
|
||||
T::Currency::reserve(&who, amount)?;
|
||||
|
||||
// queue is <Ordered: Lowest ... Highest><Fifo: Last ... First>
|
||||
let mut bid = GiltBid { amount, who: who.clone() };
|
||||
let net = if queue_full {
|
||||
sp_std::mem::swap(&mut q[0], &mut bid);
|
||||
T::Currency::unreserve(&bid.who, bid.amount);
|
||||
(0, amount - bid.amount)
|
||||
} else {
|
||||
q.try_insert(0, bid).expect("verified queue was not full above. qed.");
|
||||
(1, amount)
|
||||
};
|
||||
|
||||
let sorted_item_count = q.len().saturating_sub(T::FifoQueueLen::get() as usize);
|
||||
if sorted_item_count > 1 {
|
||||
q[0..sorted_item_count].sort_by_key(|x| x.amount);
|
||||
}
|
||||
|
||||
Ok(net)
|
||||
},
|
||||
)?;
|
||||
QueueTotals::<T>::mutate(|qs| {
|
||||
qs.bounded_resize(queue_count, (0, Zero::zero()));
|
||||
qs[queue_index].0 += net.0;
|
||||
qs[queue_index].1 = qs[queue_index].1.saturating_add(net.1);
|
||||
});
|
||||
Self::deposit_event(Event::BidPlaced { who, amount, duration });
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
/// Retract a previously placed bid.
|
||||
///
|
||||
/// Origin must be Signed, and the account should have previously issued a still-active bid
|
||||
/// of `amount` for `duration`.
|
||||
///
|
||||
/// - `amount`: The amount of the previous bid.
|
||||
/// - `duration`: The duration of the previous bid.
|
||||
#[pallet::weight(T::WeightInfo::place_bid(T::MaxQueueLen::get()))]
|
||||
pub fn retract_bid(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] amount: BalanceOf<T>,
|
||||
duration: u32,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let queue_count = T::QueueCount::get() as usize;
|
||||
let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
|
||||
ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
|
||||
|
||||
let bid = GiltBid { amount, who };
|
||||
let new_len = Queues::<T>::try_mutate(duration, |q| -> Result<u32, DispatchError> {
|
||||
let pos = q.iter().position(|i| i == &bid).ok_or(Error::<T>::NotFound)?;
|
||||
q.remove(pos);
|
||||
Ok(q.len() as u32)
|
||||
})?;
|
||||
|
||||
QueueTotals::<T>::mutate(|qs| {
|
||||
qs.bounded_resize(queue_count, (0, Zero::zero()));
|
||||
qs[queue_index].0 = new_len;
|
||||
qs[queue_index].1 = qs[queue_index].1.saturating_sub(bid.amount);
|
||||
});
|
||||
|
||||
T::Currency::unreserve(&bid.who, bid.amount);
|
||||
Self::deposit_event(Event::BidRetracted { who: bid.who, amount: bid.amount, duration });
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
/// Set target proportion of gilt-funds.
|
||||
///
|
||||
/// Origin must be `AdminOrigin`.
|
||||
///
|
||||
/// - `target`: The target proportion of effective issued funds that should be under gilts
|
||||
/// at any one time.
|
||||
#[pallet::weight(T::WeightInfo::set_target())]
|
||||
pub fn set_target(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] target: Perquintill,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
T::AdminOrigin::ensure_origin(origin)?;
|
||||
ActiveTotal::<T>::mutate(|totals| totals.target = target);
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
/// Remove an active but expired gilt. Reserved funds under gilt are freed and balance is
|
||||
/// adjusted to ensure that the funds grow or shrink to maintain the equivalent proportion
|
||||
/// of effective total issued funds.
|
||||
///
|
||||
/// Origin must be Signed and the account must be the owner of the gilt of the given index.
|
||||
///
|
||||
/// - `index`: The index of the gilt to be thawed.
|
||||
#[pallet::weight(T::WeightInfo::thaw())]
|
||||
pub fn thaw(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] index: ActiveIndex,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
// Look for `index`
|
||||
let gilt = Active::<T>::get(index).ok_or(Error::<T>::Unknown)?;
|
||||
// If found, check the owner is `who`.
|
||||
ensure!(gilt.who == who, Error::<T>::NotOwner);
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
ensure!(now >= gilt.expiry, Error::<T>::NotExpired);
|
||||
// Remove it
|
||||
Active::<T>::remove(index);
|
||||
|
||||
// Multiply the proportion it is by the total issued.
|
||||
let total_issuance =
|
||||
T::Currency::total_issuance().saturating_sub(T::IgnoredIssuance::get());
|
||||
ActiveTotal::<T>::mutate(|totals| {
|
||||
let nongilt_issuance = total_issuance.saturating_sub(totals.frozen);
|
||||
let effective_issuance =
|
||||
totals.proportion.left_from_one().saturating_reciprocal_mul(nongilt_issuance);
|
||||
let gilt_value = gilt.proportion * effective_issuance;
|
||||
|
||||
totals.frozen = totals.frozen.saturating_sub(gilt.amount);
|
||||
totals.proportion = totals.proportion.saturating_sub(gilt.proportion);
|
||||
|
||||
// Remove or mint the additional to the amount using `Deficit`/`Surplus`.
|
||||
if gilt_value > gilt.amount {
|
||||
// Unreserve full amount.
|
||||
T::Currency::unreserve(&gilt.who, gilt.amount);
|
||||
let amount = gilt_value - gilt.amount;
|
||||
let deficit = T::Currency::deposit_creating(&gilt.who, amount);
|
||||
T::Deficit::on_unbalanced(deficit);
|
||||
} else {
|
||||
if gilt_value < gilt.amount {
|
||||
// We take anything reserved beyond the gilt's final value.
|
||||
let rest = gilt.amount - gilt_value;
|
||||
// `slash` might seem a little aggressive, but it's the only way to do it
|
||||
// in case it's locked into the staking system.
|
||||
let surplus = T::Currency::slash_reserved(&gilt.who, rest).0;
|
||||
T::Surplus::on_unbalanced(surplus);
|
||||
}
|
||||
// Unreserve only its new value (less than the amount reserved). Everything
|
||||
// should add up, but (defensive) in case it doesn't, unreserve takes lower
|
||||
// priority over the funds.
|
||||
let err_amt = T::Currency::unreserve(&gilt.who, gilt_value);
|
||||
debug_assert!(err_amt.is_zero());
|
||||
}
|
||||
|
||||
let e = Event::GiltThawed {
|
||||
index,
|
||||
who: gilt.who,
|
||||
original_amount: gilt.amount,
|
||||
additional_amount: gilt_value,
|
||||
};
|
||||
Self::deposit_event(e);
|
||||
});
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Issuance information returned by `issuance()`.
|
||||
pub struct IssuanceInfo<Balance> {
|
||||
/// The balance held in reserve over all active gilts.
|
||||
pub reserved: Balance,
|
||||
/// The issuance not held in reserve for active gilts. Together with `reserved` this sums
|
||||
/// to `Currency::total_issuance`.
|
||||
pub non_gilt: Balance,
|
||||
/// The balance that `reserved` is effectively worth, at present. This is not issued funds
|
||||
/// and could be less than `reserved` (though in most cases should be greater).
|
||||
pub effective: Balance,
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Get the target amount of Gilts that we're aiming for.
|
||||
pub fn target() -> Perquintill {
|
||||
ActiveTotal::<T>::get().target
|
||||
}
|
||||
|
||||
/// Returns information on the issuance of gilts.
|
||||
pub fn issuance() -> IssuanceInfo<BalanceOf<T>> {
|
||||
let totals = ActiveTotal::<T>::get();
|
||||
|
||||
let total_issuance = T::Currency::total_issuance();
|
||||
let non_gilt = total_issuance.saturating_sub(totals.frozen);
|
||||
let effective = totals.proportion.left_from_one().saturating_reciprocal_mul(non_gilt);
|
||||
|
||||
IssuanceInfo { reserved: totals.frozen, non_gilt, effective }
|
||||
}
|
||||
|
||||
/// Attempt to enlarge our gilt-set from bids in order to satisfy our desired target amount
|
||||
/// of funds frozen into gilts.
|
||||
pub fn pursue_target(max_bids: u32) -> Weight {
|
||||
let totals = ActiveTotal::<T>::get();
|
||||
if totals.proportion < totals.target {
|
||||
let missing = totals.target.saturating_sub(totals.proportion);
|
||||
|
||||
let total_issuance =
|
||||
T::Currency::total_issuance().saturating_sub(T::IgnoredIssuance::get());
|
||||
let nongilt_issuance = total_issuance.saturating_sub(totals.frozen);
|
||||
let effective_issuance =
|
||||
totals.proportion.left_from_one().saturating_reciprocal_mul(nongilt_issuance);
|
||||
let intake = missing * effective_issuance;
|
||||
|
||||
let (bids_taken, queues_hit) = Self::enlarge(intake, max_bids);
|
||||
let first_from_each_queue = T::WeightInfo::pursue_target_per_queue(queues_hit);
|
||||
let rest_from_each_queue = T::WeightInfo::pursue_target_per_item(bids_taken)
|
||||
.saturating_sub(T::WeightInfo::pursue_target_per_item(queues_hit));
|
||||
first_from_each_queue + rest_from_each_queue
|
||||
} else {
|
||||
T::WeightInfo::pursue_target_noop()
|
||||
}
|
||||
}
|
||||
|
||||
/// Freeze additional funds from queue of bids up to `amount`. Use at most `max_bids`
|
||||
/// from the queue.
|
||||
///
|
||||
/// Return the number of bids taken and the number of distinct queues taken from.
|
||||
pub fn enlarge(amount: BalanceOf<T>, max_bids: u32) -> (u32, u32) {
|
||||
let total_issuance =
|
||||
T::Currency::total_issuance().saturating_sub(T::IgnoredIssuance::get());
|
||||
let mut remaining = amount;
|
||||
let mut bids_taken = 0;
|
||||
let mut queues_hit = 0;
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
|
||||
ActiveTotal::<T>::mutate(|totals| {
|
||||
QueueTotals::<T>::mutate(|qs| {
|
||||
for duration in (1..=T::QueueCount::get()).rev() {
|
||||
if qs[duration as usize - 1].0 == 0 {
|
||||
continue
|
||||
}
|
||||
let queue_index = duration as usize - 1;
|
||||
let expiry =
|
||||
now.saturating_add(T::Period::get().saturating_mul(duration.into()));
|
||||
Queues::<T>::mutate(duration, |q| {
|
||||
while let Some(mut bid) = q.pop() {
|
||||
if remaining < bid.amount {
|
||||
let overflow = bid.amount - remaining;
|
||||
bid.amount = remaining;
|
||||
q.try_push(GiltBid { amount: overflow, who: bid.who.clone() })
|
||||
.expect("just popped, so there must be space. qed");
|
||||
}
|
||||
let amount = bid.amount;
|
||||
// Can never overflow due to block above.
|
||||
remaining -= amount;
|
||||
// Should never underflow since it should track the total of the
|
||||
// bids exactly, but we'll be defensive.
|
||||
qs[queue_index].1 =
|
||||
qs[queue_index].1.defensive_saturating_sub(bid.amount);
|
||||
|
||||
// Now to activate the bid...
|
||||
let nongilt_issuance =
|
||||
total_issuance.defensive_saturating_sub(totals.frozen);
|
||||
let effective_issuance = totals
|
||||
.proportion
|
||||
.left_from_one()
|
||||
.saturating_reciprocal_mul(nongilt_issuance);
|
||||
let n = amount;
|
||||
let d = effective_issuance;
|
||||
let proportion = Perquintill::from_rational(n, d);
|
||||
let who = bid.who;
|
||||
let index = totals.index;
|
||||
totals.frozen += bid.amount;
|
||||
totals.proportion =
|
||||
totals.proportion.defensive_saturating_add(proportion);
|
||||
totals.index += 1;
|
||||
let e =
|
||||
Event::GiltIssued { index, expiry, who: who.clone(), amount };
|
||||
Self::deposit_event(e);
|
||||
let gilt = ActiveGilt { amount, proportion, who, expiry };
|
||||
Active::<T>::insert(index, gilt);
|
||||
|
||||
bids_taken += 1;
|
||||
|
||||
if remaining.is_zero() || bids_taken == max_bids {
|
||||
break
|
||||
}
|
||||
}
|
||||
queues_hit += 1;
|
||||
qs[queue_index].0 = q.len() as u32;
|
||||
});
|
||||
if remaining.is_zero() || bids_taken == max_bids {
|
||||
break
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
(bids_taken, queues_hit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Tests for Gilt pallet.
|
||||
|
||||
use super::*;
|
||||
use crate::{mock::*, Error};
|
||||
use frame_support::{assert_noop, assert_ok, dispatch::DispatchError, traits::Currency};
|
||||
use pallet_balances::Error as BalancesError;
|
||||
use sp_arithmetic::Perquintill;
|
||||
|
||||
#[test]
|
||||
fn basic_setup_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
|
||||
for q in 0..3 {
|
||||
assert!(Queues::<Test>::get(q).is_empty());
|
||||
}
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 0,
|
||||
proportion: Perquintill::zero(),
|
||||
index: 0,
|
||||
target: Perquintill::zero(),
|
||||
}
|
||||
);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(0, 0); 3]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_target_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
let e = DispatchError::BadOrigin;
|
||||
assert_noop!(Gilt::set_target(RuntimeOrigin::signed(2), Perquintill::from_percent(50)), e);
|
||||
assert_ok!(Gilt::set_target(RuntimeOrigin::signed(1), Perquintill::from_percent(50)));
|
||||
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 0,
|
||||
proportion: Perquintill::zero(),
|
||||
index: 0,
|
||||
target: Perquintill::from_percent(50),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_bid_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_noop!(
|
||||
Gilt::place_bid(RuntimeOrigin::signed(1), 1, 2),
|
||||
Error::<Test>::AmountTooSmall
|
||||
);
|
||||
assert_noop!(
|
||||
Gilt::place_bid(RuntimeOrigin::signed(1), 101, 2),
|
||||
BalancesError::<Test>::InsufficientBalance
|
||||
);
|
||||
assert_noop!(
|
||||
Gilt::place_bid(RuntimeOrigin::signed(1), 10, 4),
|
||||
Error::<Test>::DurationTooBig
|
||||
);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_eq!(Balances::reserved_balance(1), 10);
|
||||
assert_eq!(Queues::<Test>::get(2), vec![GiltBid { amount: 10, who: 1 }]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(0, 0), (1, 10), (0, 0)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_bid_queuing_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 20, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 5, 2));
|
||||
assert_noop!(Gilt::place_bid(RuntimeOrigin::signed(1), 5, 2), Error::<Test>::BidTooLow);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 15, 2));
|
||||
assert_eq!(Balances::reserved_balance(1), 45);
|
||||
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 25, 2));
|
||||
assert_eq!(Balances::reserved_balance(1), 60);
|
||||
assert_noop!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2), Error::<Test>::BidTooLow);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(2),
|
||||
vec![
|
||||
GiltBid { amount: 15, who: 1 },
|
||||
GiltBid { amount: 25, who: 1 },
|
||||
GiltBid { amount: 20, who: 1 },
|
||||
]
|
||||
);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(0, 0), (3, 60), (0, 0)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_bid_fails_when_queue_full() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 10, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(3), 10, 2));
|
||||
assert_noop!(Gilt::place_bid(RuntimeOrigin::signed(4), 10, 2), Error::<Test>::BidTooLow);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(4), 10, 3));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_place_bids_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 3));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 10, 2));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 40);
|
||||
assert_eq!(Balances::reserved_balance(2), 10);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![GiltBid { amount: 10, who: 1 },]);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(2),
|
||||
vec![
|
||||
GiltBid { amount: 10, who: 2 },
|
||||
GiltBid { amount: 10, who: 1 },
|
||||
GiltBid { amount: 10, who: 1 },
|
||||
]
|
||||
);
|
||||
assert_eq!(Queues::<Test>::get(3), vec![GiltBid { amount: 10, who: 1 },]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 10), (3, 30), (1, 10)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retract_single_item_queue_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Gilt::retract_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 10);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![]);
|
||||
assert_eq!(Queues::<Test>::get(2), vec![GiltBid { amount: 10, who: 1 }]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(0, 0), (1, 10), (0, 0)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retract_with_other_and_duplicate_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 10, 2));
|
||||
|
||||
assert_ok!(Gilt::retract_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_eq!(Balances::reserved_balance(1), 20);
|
||||
assert_eq!(Balances::reserved_balance(2), 10);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![GiltBid { amount: 10, who: 1 },]);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(2),
|
||||
vec![GiltBid { amount: 10, who: 2 }, GiltBid { amount: 10, who: 1 },]
|
||||
);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 10), (2, 20), (0, 0)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retract_non_existent_item_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_noop!(Gilt::retract_bid(RuntimeOrigin::signed(1), 10, 1), Error::<Test>::NotFound);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
assert_noop!(Gilt::retract_bid(RuntimeOrigin::signed(1), 20, 1), Error::<Test>::NotFound);
|
||||
assert_noop!(Gilt::retract_bid(RuntimeOrigin::signed(1), 10, 2), Error::<Test>::NotFound);
|
||||
assert_noop!(Gilt::retract_bid(RuntimeOrigin::signed(2), 10, 1), Error::<Test>::NotFound);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_enlarge_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 40, 2));
|
||||
Gilt::enlarge(40, 2);
|
||||
|
||||
// Takes 2/2, then stopped because it reaches its max amount
|
||||
assert_eq!(Balances::reserved_balance(1), 40);
|
||||
assert_eq!(Balances::reserved_balance(2), 40);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![GiltBid { amount: 40, who: 1 }]);
|
||||
assert_eq!(Queues::<Test>::get(2), vec![]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 40), (0, 0), (0, 0)]);
|
||||
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 40,
|
||||
proportion: Perquintill::from_percent(10),
|
||||
index: 1,
|
||||
target: Perquintill::zero(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Active::<Test>::get(0).unwrap(),
|
||||
ActiveGilt { proportion: Perquintill::from_percent(10), amount: 40, who: 2, expiry: 7 }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enlarge_respects_bids_limit() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 40, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(3), 40, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(4), 40, 3));
|
||||
Gilt::enlarge(100, 2);
|
||||
|
||||
// Should have taken 4/3 and 2/2, then stopped because it's only allowed 2.
|
||||
assert_eq!(Queues::<Test>::get(1), vec![GiltBid { amount: 40, who: 1 }]);
|
||||
assert_eq!(Queues::<Test>::get(2), vec![GiltBid { amount: 40, who: 3 }]);
|
||||
assert_eq!(Queues::<Test>::get(3), vec![]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 40), (1, 40), (0, 0)]);
|
||||
|
||||
assert_eq!(
|
||||
Active::<Test>::get(0).unwrap(),
|
||||
ActiveGilt {
|
||||
proportion: Perquintill::from_percent(10),
|
||||
amount: 40,
|
||||
who: 4,
|
||||
expiry: 10,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Active::<Test>::get(1).unwrap(),
|
||||
ActiveGilt { proportion: Perquintill::from_percent(10), amount: 40, who: 2, expiry: 7 }
|
||||
);
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 80,
|
||||
proportion: Perquintill::from_percent(20),
|
||||
index: 2,
|
||||
target: Perquintill::zero(),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enlarge_respects_amount_limit_and_will_split() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 80, 1));
|
||||
Gilt::enlarge(40, 2);
|
||||
|
||||
// Takes 2/2, then stopped because it reaches its max amount
|
||||
assert_eq!(Queues::<Test>::get(1), vec![GiltBid { amount: 40, who: 1 }]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 40), (0, 0), (0, 0)]);
|
||||
|
||||
assert_eq!(
|
||||
Active::<Test>::get(0).unwrap(),
|
||||
ActiveGilt { proportion: Perquintill::from_percent(10), amount: 40, who: 1, expiry: 4 }
|
||||
);
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 40,
|
||||
proportion: Perquintill::from_percent(10),
|
||||
index: 1,
|
||||
target: Perquintill::zero(),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_thaw_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
Gilt::enlarge(40, 1);
|
||||
run_to_block(3);
|
||||
assert_noop!(Gilt::thaw(RuntimeOrigin::signed(1), 0), Error::<Test>::NotExpired);
|
||||
run_to_block(4);
|
||||
assert_noop!(Gilt::thaw(RuntimeOrigin::signed(1), 1), Error::<Test>::Unknown);
|
||||
assert_noop!(Gilt::thaw(RuntimeOrigin::signed(2), 0), Error::<Test>::NotOwner);
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(1), 0));
|
||||
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 0,
|
||||
proportion: Perquintill::zero(),
|
||||
index: 1,
|
||||
target: Perquintill::zero(),
|
||||
}
|
||||
);
|
||||
assert_eq!(Active::<Test>::get(0), None);
|
||||
assert_eq!(Balances::free_balance(1), 100);
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thaw_when_issuance_higher_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 100, 1));
|
||||
Gilt::enlarge(100, 1);
|
||||
|
||||
// Everybody else's balances goes up by 50%
|
||||
Balances::make_free_balance_be(&2, 150);
|
||||
Balances::make_free_balance_be(&3, 150);
|
||||
Balances::make_free_balance_be(&4, 150);
|
||||
|
||||
run_to_block(4);
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(1), 0));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 150);
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thaw_with_ignored_issuance_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
// Give account zero some balance.
|
||||
Balances::make_free_balance_be(&0, 200);
|
||||
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 100, 1));
|
||||
Gilt::enlarge(100, 1);
|
||||
|
||||
// Account zero transfers 50 into everyone else's accounts.
|
||||
assert_ok!(Balances::transfer(RuntimeOrigin::signed(0), 2, 50));
|
||||
assert_ok!(Balances::transfer(RuntimeOrigin::signed(0), 3, 50));
|
||||
assert_ok!(Balances::transfer(RuntimeOrigin::signed(0), 4, 50));
|
||||
|
||||
run_to_block(4);
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(1), 0));
|
||||
|
||||
// Account zero changes have been ignored.
|
||||
assert_eq!(Balances::free_balance(1), 150);
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thaw_when_issuance_lower_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 100, 1));
|
||||
Gilt::enlarge(100, 1);
|
||||
|
||||
// Everybody else's balances goes down by 25%
|
||||
Balances::make_free_balance_be(&2, 75);
|
||||
Balances::make_free_balance_be(&3, 75);
|
||||
Balances::make_free_balance_be(&4, 75);
|
||||
|
||||
run_to_block(4);
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(1), 0));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 75);
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_thaws_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 60, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 50, 1));
|
||||
Gilt::enlarge(200, 3);
|
||||
|
||||
// Double everyone's free balances.
|
||||
Balances::make_free_balance_be(&2, 100);
|
||||
Balances::make_free_balance_be(&3, 200);
|
||||
Balances::make_free_balance_be(&4, 200);
|
||||
|
||||
run_to_block(4);
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(1), 0));
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(1), 1));
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(2), 2));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 200);
|
||||
assert_eq!(Balances::free_balance(2), 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_thaws_works_in_alternative_thaw_order() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 60, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 50, 1));
|
||||
Gilt::enlarge(200, 3);
|
||||
|
||||
// Double everyone's free balances.
|
||||
Balances::make_free_balance_be(&2, 100);
|
||||
Balances::make_free_balance_be(&3, 200);
|
||||
Balances::make_free_balance_be(&4, 200);
|
||||
|
||||
run_to_block(4);
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(2), 2));
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(1), 1));
|
||||
assert_ok!(Gilt::thaw(RuntimeOrigin::signed(1), 0));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 200);
|
||||
assert_eq!(Balances::free_balance(2), 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enlargement_to_target_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(2);
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(1), 40, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 40, 2));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(2), 40, 3));
|
||||
assert_ok!(Gilt::place_bid(RuntimeOrigin::signed(3), 40, 3));
|
||||
assert_ok!(Gilt::set_target(RuntimeOrigin::signed(1), Perquintill::from_percent(40)));
|
||||
|
||||
run_to_block(3);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![GiltBid { amount: 40, who: 1 },]);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(2),
|
||||
vec![GiltBid { amount: 40, who: 2 }, GiltBid { amount: 40, who: 1 },]
|
||||
);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(3),
|
||||
vec![GiltBid { amount: 40, who: 3 }, GiltBid { amount: 40, who: 2 },]
|
||||
);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 40), (2, 80), (2, 80)]);
|
||||
|
||||
run_to_block(4);
|
||||
// Two new gilts should have been issued to 2 & 3 for 40 each & duration of 3.
|
||||
assert_eq!(
|
||||
Active::<Test>::get(0).unwrap(),
|
||||
ActiveGilt {
|
||||
proportion: Perquintill::from_percent(10),
|
||||
amount: 40,
|
||||
who: 2,
|
||||
expiry: 13,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Active::<Test>::get(1).unwrap(),
|
||||
ActiveGilt {
|
||||
proportion: Perquintill::from_percent(10),
|
||||
amount: 40,
|
||||
who: 3,
|
||||
expiry: 13,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 80,
|
||||
proportion: Perquintill::from_percent(20),
|
||||
index: 2,
|
||||
target: Perquintill::from_percent(40),
|
||||
}
|
||||
);
|
||||
|
||||
run_to_block(5);
|
||||
// No change
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 80,
|
||||
proportion: Perquintill::from_percent(20),
|
||||
index: 2,
|
||||
target: Perquintill::from_percent(40),
|
||||
}
|
||||
);
|
||||
|
||||
run_to_block(6);
|
||||
// Two new gilts should have been issued to 1 & 2 for 40 each & duration of 2.
|
||||
assert_eq!(
|
||||
Active::<Test>::get(2).unwrap(),
|
||||
ActiveGilt {
|
||||
proportion: Perquintill::from_percent(10),
|
||||
amount: 40,
|
||||
who: 1,
|
||||
expiry: 12,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Active::<Test>::get(3).unwrap(),
|
||||
ActiveGilt {
|
||||
proportion: Perquintill::from_percent(10),
|
||||
amount: 40,
|
||||
who: 2,
|
||||
expiry: 12,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 160,
|
||||
proportion: Perquintill::from_percent(40),
|
||||
index: 4,
|
||||
target: Perquintill::from_percent(40),
|
||||
}
|
||||
);
|
||||
|
||||
run_to_block(8);
|
||||
// No change now.
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 160,
|
||||
proportion: Perquintill::from_percent(40),
|
||||
index: 4,
|
||||
target: Perquintill::from_percent(40),
|
||||
}
|
||||
);
|
||||
|
||||
// Set target a bit higher to use up the remaining bid.
|
||||
assert_ok!(Gilt::set_target(RuntimeOrigin::signed(1), Perquintill::from_percent(60)));
|
||||
run_to_block(10);
|
||||
|
||||
// Two new gilts should have been issued to 1 & 2 for 40 each & duration of 2.
|
||||
assert_eq!(
|
||||
Active::<Test>::get(4).unwrap(),
|
||||
ActiveGilt {
|
||||
proportion: Perquintill::from_percent(10),
|
||||
amount: 40,
|
||||
who: 1,
|
||||
expiry: 13,
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ActiveTotal::<Test>::get(),
|
||||
ActiveGiltsTotal {
|
||||
frozen: 200,
|
||||
proportion: Perquintill::from_percent(50),
|
||||
index: 5,
|
||||
target: Perquintill::from_percent(60),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "pallet-gilt"
|
||||
name = "pallet-nis"
|
||||
version = "4.0.0-dev"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2021"
|
||||
@@ -19,12 +19,12 @@ frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional
|
||||
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
|
||||
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
|
||||
sp-arithmetic = { version = "6.0.0", default-features = false, path = "../../primitives/arithmetic" }
|
||||
sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" }
|
||||
sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" }
|
||||
sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" }
|
||||
|
||||
[dev-dependencies]
|
||||
pallet-balances = { version = "4.0.0-dev", path = "../balances" }
|
||||
sp-core = { version = "7.0.0", path = "../../primitives/core" }
|
||||
sp-io = { version = "7.0.0", path = "../../primitives/io" }
|
||||
|
||||
[features]
|
||||
@@ -36,6 +36,7 @@ std = [
|
||||
"frame-system/std",
|
||||
"scale-info/std",
|
||||
"sp-arithmetic/std",
|
||||
"sp-core/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
@@ -0,0 +1,182 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2021-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Benchmarks for NIS Pallet
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use frame_benchmarking::{account, benchmarks, whitelisted_caller};
|
||||
use frame_support::traits::{Currency, EnsureOrigin, Get};
|
||||
use frame_system::RawOrigin;
|
||||
use sp_arithmetic::Perquintill;
|
||||
use sp_runtime::{
|
||||
traits::{Bounded, One, Zero},
|
||||
DispatchError, PerThing,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use crate::Pallet as Nis;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
|
||||
fn fill_queues<T: Config>() -> Result<(), DispatchError> {
|
||||
// filling queues involves filling the first queue entirely and placing a single item in all
|
||||
// other queues.
|
||||
|
||||
let queues = T::QueueCount::get();
|
||||
let bids = T::MaxQueueLen::get();
|
||||
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(
|
||||
&caller,
|
||||
T::MinBid::get() * BalanceOf::<T>::from(queues + bids),
|
||||
);
|
||||
|
||||
for _ in 0..bids {
|
||||
Nis::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?;
|
||||
}
|
||||
for d in 1..queues {
|
||||
Nis::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1 + d)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
benchmarks! {
|
||||
place_bid {
|
||||
let l in 0..(T::MaxQueueLen::get() - 1);
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
for i in 0..l {
|
||||
Nis::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?;
|
||||
}
|
||||
}: _(RawOrigin::Signed(caller.clone()), T::MinBid::get() * BalanceOf::<T>::from(2u32), 1)
|
||||
verify {
|
||||
assert_eq!(QueueTotals::<T>::get()[0], (l + 1, T::MinBid::get() * BalanceOf::<T>::from(l + 2)));
|
||||
}
|
||||
|
||||
place_bid_max {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let origin = RawOrigin::Signed(caller.clone());
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
for i in 0..T::MaxQueueLen::get() {
|
||||
Nis::<T>::place_bid(origin.clone().into(), T::MinBid::get(), 1)?;
|
||||
}
|
||||
}: place_bid(origin, T::MinBid::get() * BalanceOf::<T>::from(2u32), 1)
|
||||
verify {
|
||||
assert_eq!(QueueTotals::<T>::get()[0], (
|
||||
T::MaxQueueLen::get(),
|
||||
T::MinBid::get() * BalanceOf::<T>::from(T::MaxQueueLen::get() + 1),
|
||||
));
|
||||
}
|
||||
|
||||
retract_bid {
|
||||
let l in 1..T::MaxQueueLen::get();
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
for i in 0..l {
|
||||
Nis::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?;
|
||||
}
|
||||
}: _(RawOrigin::Signed(caller.clone()), T::MinBid::get(), 1)
|
||||
verify {
|
||||
assert_eq!(QueueTotals::<T>::get()[0], (l - 1, T::MinBid::get() * BalanceOf::<T>::from(l - 1)));
|
||||
}
|
||||
|
||||
fund_deficit {
|
||||
let origin = T::FundOrigin::successful_origin();
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let bid = T::MinBid::get().max(One::one());
|
||||
T::Currency::make_free_balance_be(&caller, bid);
|
||||
Nis::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), bid, 1)?;
|
||||
Nis::<T>::process_queues(Perquintill::one(), 1, 1, &mut WeightCounter::unlimited());
|
||||
let original = T::Currency::free_balance(&Nis::<T>::account_id());
|
||||
T::Currency::make_free_balance_be(&Nis::<T>::account_id(), BalanceOf::<T>::min_value());
|
||||
}: _<T::RuntimeOrigin>(origin)
|
||||
verify {
|
||||
// Must fund at least 99.999% of the required amount.
|
||||
let missing = Perquintill::from_rational(
|
||||
T::Currency::free_balance(&Nis::<T>::account_id()), original).left_from_one();
|
||||
assert!(missing <= Perquintill::one() / 100_000);
|
||||
}
|
||||
|
||||
thaw {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, T::MinBid::get() * BalanceOf::<T>::from(3u32));
|
||||
Nis::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?;
|
||||
Nis::<T>::place_bid(RawOrigin::Signed(caller.clone()).into(), T::MinBid::get(), 1)?;
|
||||
Nis::<T>::process_queues(Perquintill::one(), 1, 2, &mut WeightCounter::unlimited());
|
||||
Receipts::<T>::mutate(0, |m_g| if let Some(ref mut g) = m_g { g.expiry = Zero::zero() });
|
||||
}: _(RawOrigin::Signed(caller.clone()), 0, None)
|
||||
verify {
|
||||
assert!(Receipts::<T>::get(0).is_none());
|
||||
}
|
||||
|
||||
process_queues {
|
||||
fill_queues::<T>()?;
|
||||
}: {
|
||||
Nis::<T>::process_queues(
|
||||
Perquintill::one(),
|
||||
Zero::zero(),
|
||||
u32::max_value(),
|
||||
&mut WeightCounter::unlimited(),
|
||||
)
|
||||
}
|
||||
|
||||
process_queue {
|
||||
let our_account = Nis::<T>::account_id();
|
||||
let issuance = Nis::<T>::issuance();
|
||||
let mut summary = Summary::<T>::get();
|
||||
}: {
|
||||
Nis::<T>::process_queue(
|
||||
1u32,
|
||||
1u32.into(),
|
||||
&our_account,
|
||||
&issuance,
|
||||
0,
|
||||
&mut Bounded::max_value(),
|
||||
&mut (T::MaxQueueLen::get(), Bounded::max_value()),
|
||||
&mut summary,
|
||||
&mut WeightCounter::unlimited(),
|
||||
)
|
||||
}
|
||||
|
||||
process_bid {
|
||||
let who = account::<T::AccountId>("bidder", 0, SEED);
|
||||
let bid = Bid {
|
||||
amount: T::MinBid::get(),
|
||||
who,
|
||||
};
|
||||
let our_account = Nis::<T>::account_id();
|
||||
let issuance = Nis::<T>::issuance();
|
||||
let mut summary = Summary::<T>::get();
|
||||
}: {
|
||||
Nis::<T>::process_bid(
|
||||
bid,
|
||||
2u32.into(),
|
||||
&our_account,
|
||||
&issuance,
|
||||
&mut Bounded::max_value(),
|
||||
&mut Bounded::max_value(),
|
||||
&mut summary,
|
||||
)
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Nis, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,936 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! # Non-Interactive Staking (NIS) Pallet
|
||||
//! A pallet allowing accounts to auction for being frozen and receive open-ended
|
||||
//! inflation-protection in return.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! Lock up tokens, for at least as long as you offer, and be free from both inflation and
|
||||
//! intermediate reward or exchange until the tokens become unlocked.
|
||||
//!
|
||||
//! ## Design
|
||||
//!
|
||||
//! Queues for each of `1..QueueCount` periods, given in blocks (`Period`). Queues are limited in
|
||||
//! size to something sensible, `MaxQueueLen`. A secondary storage item with `QueueCount` x `u32`
|
||||
//! elements with the number of items in each queue.
|
||||
//!
|
||||
//! Queues are split into two parts. The first part is a priority queue based on bid size. The
|
||||
//! second part is just a FIFO (the size of the second part is set with `FifoQueueLen`). Items are
|
||||
//! always prepended so that removal is always O(1) since removal often happens many times under a
|
||||
//! single weighed function (`on_initialize`) yet placing bids only ever happens once per weighed
|
||||
//! function (`place_bid`). If the queue has a priority portion, then it remains sorted in order of
|
||||
//! bid size so that smaller bids fall off as it gets too large.
|
||||
//!
|
||||
//! Account may enqueue a balance with some number of `Period`s lock up, up to a maximum of
|
||||
//! `QueueCount`. The balance gets reserved. There's a minimum of `MinBid` to avoid dust.
|
||||
//!
|
||||
//! Until your bid is consolidated and you receive a receipt, you can retract it instantly and the
|
||||
//! funds are unreserved.
|
||||
//!
|
||||
//! There's a target proportion of effective total issuance (i.e. accounting for existing receipts)
|
||||
//! which the pallet attempts to have frozen at any one time. It will likely be gradually increased
|
||||
//! over time by governance.
|
||||
//!
|
||||
//! As the proportion of effective total issuance represented by outstanding receipts drops below
|
||||
//! `FrozenFraction`, then bids are taken from queues and consolidated into receipts, with the queue
|
||||
//! of the greatest period taking priority. If the item in the queue's locked amount is greater than
|
||||
//! the amount remaining to achieve `FrozenFraction`, then it is split up into multiple bids and
|
||||
//! becomes partially consolidated.
|
||||
//!
|
||||
//! With the consolidation of a bid, the bid amount is taken from the owner and a receipt is issued.
|
||||
//! The receipt records the proportion of the bid compared to effective total issuance at the time
|
||||
//! of consolidation. The receipt has two independent elements: a "main" non-fungible receipt and
|
||||
//! a second set of fungible "counterpart" tokens. The accounting functionality of the latter must
|
||||
//! be provided through the `Counterpart` trait item. The main non-fungible receipt may have its
|
||||
//! owner transferred through the pallet's implementation of `nonfungible::Transfer`.
|
||||
//!
|
||||
//! A later `thaw` function may be called in order to reduce the recorded proportion or entirely
|
||||
//! remove the receipt in return for the appropriate proportion of the effective total issuance.
|
||||
//! This may happen no earlier than queue's period after the point at which the receipt was issued.
|
||||
//! The call must be made by the owner of both the "main" non-fungible receipt and the appropriate
|
||||
//! amount of counterpart tokens.
|
||||
//!
|
||||
//! `NoCounterpart` may be provided as an implementation for the counterpart token system in which
|
||||
//! case they are completely disregarded from the thawing logic.
|
||||
//!
|
||||
//! ## Terms
|
||||
//!
|
||||
//! - *Effective total issuance*: The total issuance of balances in the system, including all claims
|
||||
//! of all outstanding receipts but excluding `IgnoredIssuance`.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use frame_support::{
|
||||
dispatch::{DispatchError, DispatchResult},
|
||||
traits::fungible::{Inspect as FungibleInspect, Mutate as FungibleMutate},
|
||||
};
|
||||
pub use pallet::*;
|
||||
use sp_arithmetic::{traits::Unsigned, RationalArg};
|
||||
use sp_core::TypedGet;
|
||||
use sp_runtime::{
|
||||
traits::{Convert, ConvertBack},
|
||||
Perquintill,
|
||||
};
|
||||
|
||||
mod benchmarking;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod weights;
|
||||
|
||||
pub struct WithMaximumOf<A: TypedGet>(sp_std::marker::PhantomData<A>);
|
||||
impl<A: TypedGet> Convert<Perquintill, A::Type> for WithMaximumOf<A>
|
||||
where
|
||||
A::Type: Clone + Unsigned + From<u64>,
|
||||
u64: TryFrom<A::Type>,
|
||||
{
|
||||
fn convert(a: Perquintill) -> A::Type {
|
||||
a * A::get()
|
||||
}
|
||||
}
|
||||
impl<A: TypedGet> ConvertBack<Perquintill, A::Type> for WithMaximumOf<A>
|
||||
where
|
||||
A::Type: RationalArg + From<u64>,
|
||||
u64: TryFrom<A::Type>,
|
||||
u128: TryFrom<A::Type>,
|
||||
{
|
||||
fn convert_back(a: A::Type) -> Perquintill {
|
||||
Perquintill::from_rational(a, A::get())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoCounterpart<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T> FungibleInspect<T> for NoCounterpart<T> {
|
||||
type Balance = u32;
|
||||
fn total_issuance() -> u32 {
|
||||
0
|
||||
}
|
||||
fn minimum_balance() -> u32 {
|
||||
0
|
||||
}
|
||||
fn balance(_who: &T) -> u32 {
|
||||
0
|
||||
}
|
||||
fn reducible_balance(_who: &T, _keep_alive: bool) -> u32 {
|
||||
0
|
||||
}
|
||||
fn can_deposit(
|
||||
_who: &T,
|
||||
_amount: u32,
|
||||
_mint: bool,
|
||||
) -> frame_support::traits::tokens::DepositConsequence {
|
||||
frame_support::traits::tokens::DepositConsequence::Success
|
||||
}
|
||||
fn can_withdraw(
|
||||
_who: &T,
|
||||
_amount: u32,
|
||||
) -> frame_support::traits::tokens::WithdrawConsequence<u32> {
|
||||
frame_support::traits::tokens::WithdrawConsequence::Success
|
||||
}
|
||||
}
|
||||
impl<T> FungibleMutate<T> for NoCounterpart<T> {
|
||||
fn mint_into(_who: &T, _amount: u32) -> DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
fn burn_from(_who: &T, _amount: u32) -> Result<u32, DispatchError> {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
impl<T> Convert<Perquintill, u32> for NoCounterpart<T> {
|
||||
fn convert(_: Perquintill) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::{FungibleInspect, FungibleMutate};
|
||||
pub use crate::weights::WeightInfo;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{
|
||||
nonfungible::{Inspect as NonfungibleInspect, Transfer as NonfungibleTransfer},
|
||||
Currency, Defensive, DefensiveSaturating,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
OnUnbalanced, ReservableCurrency,
|
||||
},
|
||||
PalletId,
|
||||
};
|
||||
use frame_system::pallet_prelude::*;
|
||||
use sp_arithmetic::{PerThing, Perquintill};
|
||||
use sp_runtime::{
|
||||
traits::{AccountIdConversion, Bounded, Convert, ConvertBack, Saturating, Zero},
|
||||
TokenError,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
>>::PositiveImbalance;
|
||||
type ReceiptRecordOf<T> = ReceiptRecord<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
<T as frame_system::Config>::BlockNumber,
|
||||
>;
|
||||
type IssuanceInfoOf<T> = IssuanceInfo<BalanceOf<T>>;
|
||||
type SummaryRecordOf<T> = SummaryRecord<<T as frame_system::Config>::BlockNumber>;
|
||||
type BidOf<T> = Bid<BalanceOf<T>, <T as frame_system::Config>::AccountId>;
|
||||
type QueueTotalsTypeOf<T> = BoundedVec<(u32, BalanceOf<T>), <T as Config>::QueueCount>;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
/// Information on runtime weights.
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// Overarching event type.
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// The treasury's pallet id, used for deriving its sovereign account ID.
|
||||
#[pallet::constant]
|
||||
type PalletId: Get<PalletId>;
|
||||
|
||||
/// Currency type that this works on.
|
||||
type Currency: ReservableCurrency<Self::AccountId, Balance = Self::CurrencyBalance>;
|
||||
|
||||
/// Just the `Currency::Balance` type; we have this item to allow us to constrain it to
|
||||
/// `From<u64>`.
|
||||
type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
|
||||
+ codec::FullCodec
|
||||
+ Copy
|
||||
+ MaybeSerializeDeserialize
|
||||
+ sp_std::fmt::Debug
|
||||
+ Default
|
||||
+ From<u64>
|
||||
+ TypeInfo
|
||||
+ MaxEncodedLen;
|
||||
|
||||
/// Origin required for auto-funding the deficit.
|
||||
type FundOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
/// The issuance to ignore. This is subtracted from the `Currency`'s `total_issuance` to get
|
||||
/// the issuance with which we determine the thawed value of a given proportion.
|
||||
type IgnoredIssuance: Get<BalanceOf<Self>>;
|
||||
|
||||
/// The accounting system for the fungible counterpart tokens.
|
||||
type Counterpart: FungibleMutate<Self::AccountId>;
|
||||
|
||||
/// The system to convert an overall proportion of issuance into a number of fungible
|
||||
/// counterpart tokens.
|
||||
///
|
||||
/// In general it's best to use `WithMaximumOf`.
|
||||
type CounterpartAmount: ConvertBack<
|
||||
Perquintill,
|
||||
<Self::Counterpart as FungibleInspect<Self::AccountId>>::Balance,
|
||||
>;
|
||||
|
||||
/// Unbalanced handler to account for funds created (in case of a higher total issuance over
|
||||
/// freezing period).
|
||||
type Deficit: OnUnbalanced<PositiveImbalanceOf<Self>>;
|
||||
|
||||
/// The target sum of all receipts' proportions.
|
||||
type Target: Get<Perquintill>;
|
||||
|
||||
/// Number of duration queues in total. This sets the maximum duration supported, which is
|
||||
/// this value multiplied by `Period`.
|
||||
#[pallet::constant]
|
||||
type QueueCount: Get<u32>;
|
||||
|
||||
/// Maximum number of items that may be in each duration queue.
|
||||
///
|
||||
/// Must be larger than zero.
|
||||
#[pallet::constant]
|
||||
type MaxQueueLen: Get<u32>;
|
||||
|
||||
/// Portion of the queue which is free from ordering and just a FIFO.
|
||||
///
|
||||
/// Must be no greater than `MaxQueueLen`.
|
||||
#[pallet::constant]
|
||||
type FifoQueueLen: Get<u32>;
|
||||
|
||||
/// The base period for the duration queues. This is the common multiple across all
|
||||
/// supported freezing durations that can be bid upon.
|
||||
#[pallet::constant]
|
||||
type BasePeriod: Get<Self::BlockNumber>;
|
||||
|
||||
/// The minimum amount of funds that may be placed in a bid. Note that this
|
||||
/// does not actually limit the amount which may be represented in a receipt since bids may
|
||||
/// be split up by the system.
|
||||
///
|
||||
/// It should be at least big enough to ensure that there is no possible storage spam attack
|
||||
/// or queue-filling attack.
|
||||
#[pallet::constant]
|
||||
type MinBid: Get<BalanceOf<Self>>;
|
||||
|
||||
/// The minimum amount of funds which may intentionally be left remaining under a single
|
||||
/// receipt.
|
||||
#[pallet::constant]
|
||||
type MinReceipt: Get<Perquintill>;
|
||||
|
||||
/// The number of blocks between consecutive attempts to dequeue bids and create receipts.
|
||||
///
|
||||
/// A larger value results in fewer storage hits each block, but a slower period to get to
|
||||
/// the target.
|
||||
#[pallet::constant]
|
||||
type IntakePeriod: Get<Self::BlockNumber>;
|
||||
|
||||
/// The maximum amount of bids that can consolidated into receipts in a single intake. A
|
||||
/// larger value here means less of the block available for transactions should there be a
|
||||
/// glut of bids.
|
||||
#[pallet::constant]
|
||||
type MaxIntakeWeight: Get<Weight>;
|
||||
|
||||
/// The maximum proportion which may be thawed and the period over which it is reset.
|
||||
#[pallet::constant]
|
||||
type ThawThrottle: Get<(Perquintill, Self::BlockNumber)>;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::generate_store(pub(super) trait Store)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// A single bid, an item of a *queue* in `Queues`.
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct Bid<Balance, AccountId> {
|
||||
/// The amount bid.
|
||||
pub amount: Balance,
|
||||
/// The owner of the bid.
|
||||
pub who: AccountId,
|
||||
}
|
||||
|
||||
/// Information representing a receipt.
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct ReceiptRecord<AccountId, BlockNumber> {
|
||||
/// The proportion of the effective total issuance.
|
||||
pub proportion: Perquintill,
|
||||
/// The account to whom this receipt belongs.
|
||||
pub who: AccountId,
|
||||
/// The time after which this receipt can be thawed.
|
||||
pub expiry: BlockNumber,
|
||||
}
|
||||
|
||||
/// An index for a receipt.
|
||||
pub type ReceiptIndex = u32;
|
||||
|
||||
/// Overall information package on the outstanding receipts.
|
||||
///
|
||||
/// The way of determining the net issuance (i.e. after factoring in all maturing frozen funds)
|
||||
/// is:
|
||||
///
|
||||
/// `issuance - frozen + proportion * issuance`
|
||||
///
|
||||
/// where `issuance = total_issuance - IgnoredIssuance`
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct SummaryRecord<BlockNumber> {
|
||||
/// The total proportion over all outstanding receipts.
|
||||
pub proportion_owed: Perquintill,
|
||||
/// The total number of receipts created so far.
|
||||
pub index: ReceiptIndex,
|
||||
/// The amount (as a proportion of ETI) which has been thawed in this period so far.
|
||||
pub thawed: Perquintill,
|
||||
/// The current thaw period's beginning.
|
||||
pub last_period: BlockNumber,
|
||||
}
|
||||
|
||||
pub struct OnEmptyQueueTotals<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> Get<QueueTotalsTypeOf<T>> for OnEmptyQueueTotals<T> {
|
||||
fn get() -> QueueTotalsTypeOf<T> {
|
||||
BoundedVec::truncate_from(vec![
|
||||
(0, Zero::zero());
|
||||
<T as Config>::QueueCount::get() as usize
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// The totals of items and balances within each queue. Saves a lot of storage reads in the
|
||||
/// case of sparsely packed queues.
|
||||
///
|
||||
/// The vector is indexed by duration in `Period`s, offset by one, so information on the queue
|
||||
/// whose duration is one `Period` would be storage `0`.
|
||||
#[pallet::storage]
|
||||
pub type QueueTotals<T: Config> =
|
||||
StorageValue<_, QueueTotalsTypeOf<T>, ValueQuery, OnEmptyQueueTotals<T>>;
|
||||
|
||||
/// The queues of bids. Indexed by duration (in `Period`s).
|
||||
#[pallet::storage]
|
||||
pub type Queues<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, u32, BoundedVec<BidOf<T>, T::MaxQueueLen>, ValueQuery>;
|
||||
|
||||
/// Summary information over the general state.
|
||||
#[pallet::storage]
|
||||
pub type Summary<T> = StorageValue<_, SummaryRecordOf<T>, ValueQuery>;
|
||||
|
||||
/// The currently outstanding receipts, indexed according to the order of creation.
|
||||
#[pallet::storage]
|
||||
pub type Receipts<T> =
|
||||
StorageMap<_, Blake2_128Concat, ReceiptIndex, ReceiptRecordOf<T>, OptionQuery>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// A bid was successfully placed.
|
||||
BidPlaced { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
|
||||
/// A bid was successfully removed (before being accepted).
|
||||
BidRetracted { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
|
||||
/// A bid was dropped from a queue because of another, more substantial, bid was present.
|
||||
BidDropped { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
|
||||
/// A bid was accepted. The balance may not be released until expiry.
|
||||
Issued {
|
||||
/// The identity of the receipt.
|
||||
index: ReceiptIndex,
|
||||
/// The block number at which the receipt may be thawed.
|
||||
expiry: T::BlockNumber,
|
||||
/// The owner of the receipt.
|
||||
who: T::AccountId,
|
||||
/// The proportion of the effective total issuance which the receipt represents.
|
||||
proportion: Perquintill,
|
||||
/// The amount of funds which were debited from the owner.
|
||||
amount: BalanceOf<T>,
|
||||
},
|
||||
/// An receipt has been (at least partially) thawed.
|
||||
Thawed {
|
||||
/// The identity of the receipt.
|
||||
index: ReceiptIndex,
|
||||
/// The owner.
|
||||
who: T::AccountId,
|
||||
/// The proportion of the effective total issuance by which the owner was debited.
|
||||
proportion: Perquintill,
|
||||
/// The amount by which the owner was credited.
|
||||
amount: BalanceOf<T>,
|
||||
/// If `true` then the receipt is done.
|
||||
dropped: bool,
|
||||
},
|
||||
/// An automatic funding of the deficit was made.
|
||||
Funded { deficit: BalanceOf<T> },
|
||||
/// A receipt was transfered.
|
||||
Transferred { from: T::AccountId, to: T::AccountId, index: ReceiptIndex },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// The duration of the bid is less than one.
|
||||
DurationTooSmall,
|
||||
/// The duration is the bid is greater than the number of queues.
|
||||
DurationTooBig,
|
||||
/// The amount of the bid is less than the minimum allowed.
|
||||
AmountTooSmall,
|
||||
/// The queue for the bid's duration is full and the amount bid is too low to get in
|
||||
/// through replacing an existing bid.
|
||||
BidTooLow,
|
||||
/// Bond index is unknown.
|
||||
Unknown,
|
||||
/// Not the owner of the receipt.
|
||||
NotOwner,
|
||||
/// Bond not yet at expiry date.
|
||||
NotExpired,
|
||||
/// The given bid for retraction is not found.
|
||||
NotFound,
|
||||
/// The portion supplied is beyond the value of the receipt.
|
||||
TooMuch,
|
||||
/// Not enough funds are held to pay out.
|
||||
Unfunded,
|
||||
/// There are enough funds for what is required.
|
||||
Funded,
|
||||
/// The thaw throttle has been reached for this period.
|
||||
Throttled,
|
||||
/// The operation would result in a receipt worth an insignficant value.
|
||||
MakesDust,
|
||||
}
|
||||
|
||||
pub(crate) struct WeightCounter {
|
||||
pub(crate) used: Weight,
|
||||
pub(crate) limit: Weight,
|
||||
}
|
||||
impl WeightCounter {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn unlimited() -> Self {
|
||||
WeightCounter { used: Weight::zero(), limit: Weight::max_value() }
|
||||
}
|
||||
fn check_accrue(&mut self, w: Weight) -> bool {
|
||||
let test = self.used.saturating_add(w);
|
||||
if test.any_gt(self.limit) {
|
||||
false
|
||||
} else {
|
||||
self.used = test;
|
||||
true
|
||||
}
|
||||
}
|
||||
fn can_accrue(&mut self, w: Weight) -> bool {
|
||||
self.used.saturating_add(w).all_lte(self.limit)
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_initialize(n: T::BlockNumber) -> Weight {
|
||||
let mut weight_counter =
|
||||
WeightCounter { used: Weight::zero(), limit: T::MaxIntakeWeight::get() };
|
||||
if T::IntakePeriod::get().is_zero() || (n % T::IntakePeriod::get()).is_zero() {
|
||||
if weight_counter.check_accrue(T::WeightInfo::process_queues()) {
|
||||
Self::process_queues(
|
||||
T::Target::get(),
|
||||
T::QueueCount::get(),
|
||||
u32::max_value(),
|
||||
&mut weight_counter,
|
||||
)
|
||||
}
|
||||
}
|
||||
weight_counter.used
|
||||
}
|
||||
|
||||
fn integrity_test() {
|
||||
assert!(!T::IntakePeriod::get().is_zero());
|
||||
assert!(!T::MaxQueueLen::get().is_zero());
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Place a bid.
|
||||
///
|
||||
/// Origin must be Signed, and account must have at least `amount` in free balance.
|
||||
///
|
||||
/// - `amount`: The amount of the bid; these funds will be reserved, and if/when
|
||||
/// consolidated, removed. Must be at least `MinBid`.
|
||||
/// - `duration`: The number of periods before which the newly consolidated bid may be
|
||||
/// thawed. Must be greater than 1 and no more than `QueueCount`.
|
||||
///
|
||||
/// Complexities:
|
||||
/// - `Queues[duration].len()` (just take max).
|
||||
#[pallet::weight(T::WeightInfo::place_bid_max())]
|
||||
pub fn place_bid(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] amount: BalanceOf<T>,
|
||||
duration: u32,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
ensure!(amount >= T::MinBid::get(), Error::<T>::AmountTooSmall);
|
||||
let queue_count = T::QueueCount::get() as usize;
|
||||
let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
|
||||
ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
|
||||
|
||||
let net = Queues::<T>::try_mutate(
|
||||
duration,
|
||||
|q| -> Result<(u32, BalanceOf<T>), DispatchError> {
|
||||
let queue_full = q.len() == T::MaxQueueLen::get() as usize;
|
||||
ensure!(!queue_full || q[0].amount < amount, Error::<T>::BidTooLow);
|
||||
T::Currency::reserve(&who, amount)?;
|
||||
|
||||
// queue is <Ordered: Lowest ... Highest><Fifo: Last ... First>
|
||||
let mut bid = Bid { amount, who: who.clone() };
|
||||
let net = if queue_full {
|
||||
sp_std::mem::swap(&mut q[0], &mut bid);
|
||||
let _ = T::Currency::unreserve(&bid.who, bid.amount);
|
||||
Self::deposit_event(Event::<T>::BidDropped {
|
||||
who: bid.who,
|
||||
amount: bid.amount,
|
||||
duration,
|
||||
});
|
||||
(0, amount - bid.amount)
|
||||
} else {
|
||||
q.try_insert(0, bid).expect("verified queue was not full above. qed.");
|
||||
(1, amount)
|
||||
};
|
||||
|
||||
let sorted_item_count = q.len().saturating_sub(T::FifoQueueLen::get() as usize);
|
||||
if sorted_item_count > 1 {
|
||||
q[0..sorted_item_count].sort_by_key(|x| x.amount);
|
||||
}
|
||||
|
||||
Ok(net)
|
||||
},
|
||||
)?;
|
||||
QueueTotals::<T>::mutate(|qs| {
|
||||
qs.bounded_resize(queue_count, (0, Zero::zero()));
|
||||
qs[queue_index].0 += net.0;
|
||||
qs[queue_index].1.saturating_accrue(net.1);
|
||||
});
|
||||
Self::deposit_event(Event::BidPlaced { who, amount, duration });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retract a previously placed bid.
|
||||
///
|
||||
/// Origin must be Signed, and the account should have previously issued a still-active bid
|
||||
/// of `amount` for `duration`.
|
||||
///
|
||||
/// - `amount`: The amount of the previous bid.
|
||||
/// - `duration`: The duration of the previous bid.
|
||||
#[pallet::weight(T::WeightInfo::retract_bid(T::MaxQueueLen::get()))]
|
||||
pub fn retract_bid(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] amount: BalanceOf<T>,
|
||||
duration: u32,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let queue_count = T::QueueCount::get() as usize;
|
||||
let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
|
||||
ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
|
||||
|
||||
let bid = Bid { amount, who };
|
||||
let new_len = Queues::<T>::try_mutate(duration, |q| -> Result<u32, DispatchError> {
|
||||
let pos = q.iter().position(|i| i == &bid).ok_or(Error::<T>::NotFound)?;
|
||||
q.remove(pos);
|
||||
Ok(q.len() as u32)
|
||||
})?;
|
||||
|
||||
QueueTotals::<T>::mutate(|qs| {
|
||||
qs.bounded_resize(queue_count, (0, Zero::zero()));
|
||||
qs[queue_index].0 = new_len;
|
||||
qs[queue_index].1.saturating_reduce(bid.amount);
|
||||
});
|
||||
|
||||
T::Currency::unreserve(&bid.who, bid.amount);
|
||||
Self::deposit_event(Event::BidRetracted { who: bid.who, amount: bid.amount, duration });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure we have sufficient funding for all potential payouts.
|
||||
///
|
||||
/// - `origin`: Must be accepted by `FundOrigin`.
|
||||
#[pallet::weight(T::WeightInfo::fund_deficit())]
|
||||
pub fn fund_deficit(origin: OriginFor<T>) -> DispatchResult {
|
||||
T::FundOrigin::ensure_origin(origin)?;
|
||||
let summary: SummaryRecordOf<T> = Summary::<T>::get();
|
||||
let our_account = Self::account_id();
|
||||
let issuance = Self::issuance_with(&our_account, &summary);
|
||||
let deficit = issuance.required.saturating_sub(issuance.holdings);
|
||||
ensure!(!deficit.is_zero(), Error::<T>::Funded);
|
||||
T::Deficit::on_unbalanced(T::Currency::deposit_creating(&our_account, deficit));
|
||||
Self::deposit_event(Event::<T>::Funded { deficit });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reduce or remove an outstanding receipt, placing the according proportion of funds into
|
||||
/// the account of the owner.
|
||||
///
|
||||
/// - `origin`: Must be Signed and the account must be the owner of the receipt `index` as
|
||||
/// well as any fungible counterpart.
|
||||
/// - `index`: The index of the receipt.
|
||||
/// - `portion`: If `Some`, then only the given portion of the receipt should be thawed. If
|
||||
/// `None`, then all of it should be.
|
||||
#[pallet::weight(T::WeightInfo::thaw())]
|
||||
pub fn thaw(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] index: ReceiptIndex,
|
||||
portion: Option<<T::Counterpart as FungibleInspect<T::AccountId>>::Balance>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
// Look for `index`
|
||||
let mut receipt: ReceiptRecordOf<T> =
|
||||
Receipts::<T>::get(index).ok_or(Error::<T>::Unknown)?;
|
||||
// If found, check the owner is `who`.
|
||||
ensure!(receipt.who == who, Error::<T>::NotOwner);
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
|
||||
|
||||
let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
|
||||
|
||||
let proportion = if let Some(counterpart) = portion {
|
||||
let proportion = T::CounterpartAmount::convert_back(counterpart);
|
||||
ensure!(proportion <= receipt.proportion, Error::<T>::TooMuch);
|
||||
let remaining = receipt.proportion.saturating_sub(proportion);
|
||||
ensure!(
|
||||
remaining.is_zero() || remaining >= T::MinReceipt::get(),
|
||||
Error::<T>::MakesDust
|
||||
);
|
||||
proportion
|
||||
} else {
|
||||
receipt.proportion
|
||||
};
|
||||
|
||||
let (throttle, throttle_period) = T::ThawThrottle::get();
|
||||
if now.saturating_sub(summary.last_period) >= throttle_period {
|
||||
summary.thawed = Zero::zero();
|
||||
summary.last_period = now;
|
||||
}
|
||||
summary.thawed.saturating_accrue(proportion);
|
||||
ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
|
||||
|
||||
T::Counterpart::burn_from(&who, T::CounterpartAmount::convert(proportion))?;
|
||||
|
||||
// Multiply the proportion it is by the total issued.
|
||||
let our_account = Self::account_id();
|
||||
let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
|
||||
let amount = proportion * effective_issuance;
|
||||
|
||||
receipt.proportion.saturating_reduce(proportion);
|
||||
summary.proportion_owed.saturating_reduce(proportion);
|
||||
|
||||
T::Currency::transfer(&our_account, &who, amount, AllowDeath)
|
||||
.map_err(|_| Error::<T>::Unfunded)?;
|
||||
|
||||
let dropped = receipt.proportion.is_zero();
|
||||
if dropped {
|
||||
Receipts::<T>::remove(index);
|
||||
} else {
|
||||
Receipts::<T>::insert(index, &receipt);
|
||||
}
|
||||
Summary::<T>::put(&summary);
|
||||
|
||||
Self::deposit_event(Event::Thawed { index, who, amount, proportion, dropped });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Issuance information returned by `issuance()`.
|
||||
#[derive(RuntimeDebug)]
|
||||
pub struct IssuanceInfo<Balance> {
|
||||
/// The balance held in reserve by this pallet instance.
|
||||
pub holdings: Balance,
|
||||
/// The (non-ignored) issuance in the system, not including this pallet's account.
|
||||
pub other: Balance,
|
||||
/// The effective total issuance, hypothetically if all outstanding receipts were thawed at
|
||||
/// present.
|
||||
pub effective: Balance,
|
||||
/// The amount needed to be the pallet instance's account in case all outstanding receipts
|
||||
/// were thawed at present.
|
||||
pub required: Balance,
|
||||
}
|
||||
|
||||
impl<T: Config> NonfungibleInspect<T::AccountId> for Pallet<T> {
|
||||
type ItemId = ReceiptIndex;
|
||||
|
||||
fn owner(item: &ReceiptIndex) -> Option<T::AccountId> {
|
||||
Receipts::<T>::get(item).map(|r| r.who)
|
||||
}
|
||||
|
||||
fn attribute(item: &Self::ItemId, key: &[u8]) -> Option<Vec<u8>> {
|
||||
let item = Receipts::<T>::get(item)?;
|
||||
match key {
|
||||
b"proportion" => Some(item.proportion.encode()),
|
||||
b"expiry" => Some(item.expiry.encode()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> NonfungibleTransfer<T::AccountId> for Pallet<T> {
|
||||
fn transfer(index: &ReceiptIndex, destination: &T::AccountId) -> DispatchResult {
|
||||
let mut item = Receipts::<T>::get(index).ok_or(TokenError::UnknownAsset)?;
|
||||
let from = item.who;
|
||||
item.who = destination.clone();
|
||||
Receipts::<T>::insert(&index, &item);
|
||||
Pallet::<T>::deposit_event(Event::<T>::Transferred {
|
||||
from,
|
||||
to: item.who,
|
||||
index: *index,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// The account ID of the reserves.
|
||||
///
|
||||
/// This actually does computation. If you need to keep using it, then make sure you cache
|
||||
/// the value and only call this once.
|
||||
pub fn account_id() -> T::AccountId {
|
||||
T::PalletId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
/// Returns information on the issuance within the system.
|
||||
pub fn issuance() -> IssuanceInfo<BalanceOf<T>> {
|
||||
Self::issuance_with(&Self::account_id(), &Summary::<T>::get())
|
||||
}
|
||||
|
||||
/// Returns information on the issuance within the system
|
||||
///
|
||||
/// This function is equivalent to `issuance`, except that it accepts arguments rather than
|
||||
/// queries state. If the arguments are already known, then this may be slightly more
|
||||
/// performant.
|
||||
///
|
||||
/// - `our_account`: The value returned by `Self::account_id()`.
|
||||
/// - `summary`: The value returned by `Summary::<T>::get()`.
|
||||
pub fn issuance_with(
|
||||
our_account: &T::AccountId,
|
||||
summary: &SummaryRecordOf<T>,
|
||||
) -> IssuanceInfo<BalanceOf<T>> {
|
||||
let total_issuance =
|
||||
T::Currency::total_issuance().saturating_sub(T::IgnoredIssuance::get());
|
||||
let holdings = T::Currency::free_balance(our_account);
|
||||
let other = total_issuance.saturating_sub(holdings);
|
||||
let effective =
|
||||
summary.proportion_owed.left_from_one().saturating_reciprocal_mul(other);
|
||||
let required = summary.proportion_owed * effective;
|
||||
IssuanceInfo { holdings, other, effective, required }
|
||||
}
|
||||
|
||||
/// Process some bids into receipts up to a `target` total of all receipts.
|
||||
///
|
||||
/// Touch at most `max_queues`.
|
||||
///
|
||||
/// Return the weight used.
|
||||
pub(crate) fn process_queues(
|
||||
target: Perquintill,
|
||||
max_queues: u32,
|
||||
max_bids: u32,
|
||||
weight: &mut WeightCounter,
|
||||
) {
|
||||
let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
|
||||
if summary.proportion_owed >= target {
|
||||
return
|
||||
}
|
||||
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
let our_account = Self::account_id();
|
||||
let issuance: IssuanceInfoOf<T> = Self::issuance_with(&our_account, &summary);
|
||||
let mut remaining = target.saturating_sub(summary.proportion_owed) * issuance.effective;
|
||||
|
||||
let mut queues_hit = 0;
|
||||
let mut bids_hit = 0;
|
||||
let mut totals = QueueTotals::<T>::get();
|
||||
let queue_count = T::QueueCount::get();
|
||||
totals.bounded_resize(queue_count as usize, (0, Zero::zero()));
|
||||
for duration in (1..=queue_count).rev() {
|
||||
if totals[duration as usize - 1].0.is_zero() {
|
||||
continue
|
||||
}
|
||||
if remaining.is_zero() || queues_hit >= max_queues
|
||||
|| !weight.check_accrue(T::WeightInfo::process_queue())
|
||||
// No point trying to process a queue if we can't process a single bid.
|
||||
|| !weight.can_accrue(T::WeightInfo::process_bid())
|
||||
{
|
||||
break
|
||||
}
|
||||
|
||||
let b = Self::process_queue(
|
||||
duration,
|
||||
now,
|
||||
&our_account,
|
||||
&issuance,
|
||||
max_bids.saturating_sub(bids_hit),
|
||||
&mut remaining,
|
||||
&mut totals[duration as usize - 1],
|
||||
&mut summary,
|
||||
weight,
|
||||
);
|
||||
|
||||
bids_hit.saturating_accrue(b);
|
||||
queues_hit.saturating_inc();
|
||||
}
|
||||
QueueTotals::<T>::put(&totals);
|
||||
Summary::<T>::put(&summary);
|
||||
}
|
||||
|
||||
pub(crate) fn process_queue(
|
||||
duration: u32,
|
||||
now: T::BlockNumber,
|
||||
our_account: &T::AccountId,
|
||||
issuance: &IssuanceInfo<BalanceOf<T>>,
|
||||
max_bids: u32,
|
||||
remaining: &mut BalanceOf<T>,
|
||||
queue_total: &mut (u32, BalanceOf<T>),
|
||||
summary: &mut SummaryRecordOf<T>,
|
||||
weight: &mut WeightCounter,
|
||||
) -> u32 {
|
||||
let mut queue: BoundedVec<BidOf<T>, _> = Queues::<T>::get(&duration);
|
||||
let expiry = now.saturating_add(T::BasePeriod::get().saturating_mul(duration.into()));
|
||||
let mut count = 0;
|
||||
|
||||
while count < max_bids &&
|
||||
!queue.is_empty() &&
|
||||
!remaining.is_zero() &&
|
||||
weight.check_accrue(T::WeightInfo::process_bid())
|
||||
{
|
||||
let bid = match queue.pop() {
|
||||
Some(b) => b,
|
||||
None => break,
|
||||
};
|
||||
if let Some(bid) = Self::process_bid(
|
||||
bid,
|
||||
expiry,
|
||||
our_account,
|
||||
issuance,
|
||||
remaining,
|
||||
&mut queue_total.1,
|
||||
summary,
|
||||
) {
|
||||
queue.try_push(bid).expect("just popped, so there must be space. qed");
|
||||
// This should exit at the next iteration (though nothing will break if it
|
||||
// doesn't).
|
||||
}
|
||||
count.saturating_inc();
|
||||
}
|
||||
queue_total.0 = queue.len() as u32;
|
||||
Queues::<T>::insert(&duration, &queue);
|
||||
count
|
||||
}
|
||||
|
||||
pub(crate) fn process_bid(
|
||||
mut bid: BidOf<T>,
|
||||
expiry: T::BlockNumber,
|
||||
our_account: &T::AccountId,
|
||||
issuance: &IssuanceInfo<BalanceOf<T>>,
|
||||
remaining: &mut BalanceOf<T>,
|
||||
queue_amount: &mut BalanceOf<T>,
|
||||
summary: &mut SummaryRecordOf<T>,
|
||||
) -> Option<BidOf<T>> {
|
||||
let result = if *remaining < bid.amount {
|
||||
let overflow = bid.amount - *remaining;
|
||||
bid.amount = *remaining;
|
||||
Some(Bid { amount: overflow, who: bid.who.clone() })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let amount = bid.amount.saturating_sub(T::Currency::unreserve(&bid.who, bid.amount));
|
||||
if T::Currency::transfer(&bid.who, &our_account, amount, AllowDeath).is_err() {
|
||||
return result
|
||||
}
|
||||
|
||||
// Can never overflow due to block above.
|
||||
remaining.saturating_reduce(amount);
|
||||
// Should never underflow since it should track the total of the
|
||||
// bids exactly, but we'll be defensive.
|
||||
queue_amount.defensive_saturating_reduce(amount);
|
||||
|
||||
// Now to activate the bid...
|
||||
let n = amount;
|
||||
let d = issuance.effective;
|
||||
let proportion = Perquintill::from_rational(n, d);
|
||||
let who = bid.who;
|
||||
let index = summary.index;
|
||||
summary.proportion_owed.defensive_saturating_accrue(proportion);
|
||||
summary.index += 1;
|
||||
|
||||
let e = Event::Issued { index, expiry, who: who.clone(), amount, proportion };
|
||||
Self::deposit_event(e);
|
||||
let receipt = ReceiptRecord { proportion, who: who.clone(), expiry };
|
||||
Receipts::<T>::insert(index, receipt);
|
||||
|
||||
// issue the fungible counterpart
|
||||
let fung_eq = T::CounterpartAmount::convert(proportion);
|
||||
let _ = T::Counterpart::mint_into(&who, fung_eq).defensive();
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test environment for Gilt pallet.
|
||||
//! Test environment for NIS pallet.
|
||||
|
||||
use crate as pallet_gilt;
|
||||
use crate::{self as pallet_nis, Perquintill, WithMaximumOf};
|
||||
|
||||
use frame_support::{
|
||||
ord_parameter_types, parameter_types,
|
||||
traits::{ConstU16, ConstU32, ConstU64, Currency, GenesisBuild, OnFinalize, OnInitialize},
|
||||
traits::{ConstU16, ConstU32, ConstU64, Currency, OnFinalize, OnInitialize, StorageMapShim},
|
||||
weights::Weight,
|
||||
PalletId,
|
||||
};
|
||||
use sp_core::H256;
|
||||
use pallet_balances::{Instance1, Instance2};
|
||||
use sp_core::{ConstU128, H256};
|
||||
use sp_runtime::{
|
||||
testing::Header,
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
@@ -39,9 +42,10 @@ frame_support::construct_runtime!(
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Config<T>, Storage, Event<T>},
|
||||
Gilt: pallet_gilt::{Pallet, Call, Config, Storage, Event<T>},
|
||||
System: frame_system,
|
||||
Balances: pallet_balances::<Instance1>,
|
||||
NisBalances: pallet_balances::<Instance2>,
|
||||
Nis: pallet_nis,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -72,7 +76,7 @@ impl frame_system::Config for Test {
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
}
|
||||
|
||||
impl pallet_balances::Config for Test {
|
||||
impl pallet_balances::Config<Instance1> for Test {
|
||||
type Balance = u64;
|
||||
type DustRemoval = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
@@ -84,52 +88,79 @@ impl pallet_balances::Config for Test {
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
}
|
||||
|
||||
impl pallet_balances::Config<Instance2> for Test {
|
||||
type Balance = u128;
|
||||
type DustRemoval = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = frame_support::traits::ConstU128<1>;
|
||||
type AccountStore = StorageMapShim<
|
||||
pallet_balances::Account<Test, Instance2>,
|
||||
frame_system::Provider<Test>,
|
||||
u64,
|
||||
pallet_balances::AccountData<u128>,
|
||||
>;
|
||||
type WeightInfo = ();
|
||||
type MaxLocks = ();
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub IgnoredIssuance: u64 = Balances::total_balance(&0); // Account zero is ignored.
|
||||
pub const NisPalletId: PalletId = PalletId(*b"py/nis ");
|
||||
pub static Target: Perquintill = Perquintill::zero();
|
||||
pub const MinReceipt: Perquintill = Perquintill::from_percent(1);
|
||||
pub const ThawThrottle: (Perquintill, u64) = (Perquintill::from_percent(25), 5);
|
||||
pub static MaxIntakeWeight: Weight = Weight::from_ref_time(2_000_000_000_000);
|
||||
}
|
||||
|
||||
ord_parameter_types! {
|
||||
pub const One: u64 = 1;
|
||||
}
|
||||
|
||||
impl pallet_gilt::Config for Test {
|
||||
impl pallet_nis::Config for Test {
|
||||
type WeightInfo = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type PalletId = NisPalletId;
|
||||
type Currency = Balances;
|
||||
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
|
||||
type AdminOrigin = frame_system::EnsureSignedBy<One, Self::AccountId>;
|
||||
type CurrencyBalance = <Self as pallet_balances::Config<Instance1>>::Balance;
|
||||
type FundOrigin = frame_system::EnsureSigned<Self::AccountId>;
|
||||
type Deficit = ();
|
||||
type Surplus = ();
|
||||
type IgnoredIssuance = IgnoredIssuance;
|
||||
type Counterpart = NisBalances;
|
||||
type CounterpartAmount = WithMaximumOf<ConstU128<21_000_000u128>>;
|
||||
type Target = Target;
|
||||
type QueueCount = ConstU32<3>;
|
||||
type MaxQueueLen = ConstU32<3>;
|
||||
type FifoQueueLen = ConstU32<1>;
|
||||
type Period = ConstU64<3>;
|
||||
type MinFreeze = ConstU64<2>;
|
||||
type BasePeriod = ConstU64<3>;
|
||||
type MinBid = ConstU64<2>;
|
||||
type IntakePeriod = ConstU64<2>;
|
||||
type MaxIntakeBids = ConstU32<2>;
|
||||
type WeightInfo = ();
|
||||
type MaxIntakeWeight = MaxIntakeWeight;
|
||||
type MinReceipt = MinReceipt;
|
||||
type ThawThrottle = ThawThrottle;
|
||||
}
|
||||
|
||||
// This function basically just builds a genesis storage key/value store according to
|
||||
// our desired mockup.
|
||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Test> {
|
||||
pallet_balances::GenesisConfig::<Test, Instance1> {
|
||||
balances: vec![(1, 100), (2, 100), (3, 100), (4, 100)],
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
GenesisBuild::<Test>::assimilate_storage(&crate::GenesisConfig, &mut t).unwrap();
|
||||
t.into()
|
||||
}
|
||||
|
||||
pub fn run_to_block(n: u64) {
|
||||
while System::block_number() < n {
|
||||
Gilt::on_finalize(System::block_number());
|
||||
Nis::on_finalize(System::block_number());
|
||||
Balances::on_finalize(System::block_number());
|
||||
System::on_finalize(System::block_number());
|
||||
System::set_block_number(System::block_number() + 1);
|
||||
System::on_initialize(System::block_number());
|
||||
Balances::on_initialize(System::block_number());
|
||||
Gilt::on_initialize(System::block_number());
|
||||
Nis::on_initialize(System::block_number());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Tests for NIS pallet.
|
||||
|
||||
use super::*;
|
||||
use crate::{mock::*, Error};
|
||||
use frame_support::{
|
||||
assert_noop, assert_ok,
|
||||
traits::{
|
||||
nonfungible::{Inspect, Transfer},
|
||||
Currency,
|
||||
},
|
||||
};
|
||||
use pallet_balances::{Error as BalancesError, Instance1};
|
||||
use sp_arithmetic::Perquintill;
|
||||
use sp_runtime::{Saturating, TokenError};
|
||||
|
||||
fn pot() -> u64 {
|
||||
Balances::free_balance(&Nis::account_id())
|
||||
}
|
||||
|
||||
fn enlarge(amount: u64, max_bids: u32) {
|
||||
let summary: SummaryRecord<u64> = Summary::<Test>::get();
|
||||
let increase_in_proportion_owed = Perquintill::from_rational(amount, Nis::issuance().effective);
|
||||
let target = summary.proportion_owed.saturating_add(increase_in_proportion_owed);
|
||||
Nis::process_queues(target, u32::max_value(), max_bids, &mut WeightCounter::unlimited());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_setup_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
|
||||
for q in 0..3 {
|
||||
assert!(Queues::<Test>::get(q).is_empty());
|
||||
}
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::zero(),
|
||||
index: 0,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero()
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_bid_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_noop!(Nis::place_bid(RuntimeOrigin::signed(1), 1, 2), Error::<Test>::AmountTooSmall);
|
||||
assert_noop!(
|
||||
Nis::place_bid(RuntimeOrigin::signed(1), 101, 2),
|
||||
BalancesError::<Test, Instance1>::InsufficientBalance
|
||||
);
|
||||
assert_noop!(
|
||||
Nis::place_bid(RuntimeOrigin::signed(1), 10, 4),
|
||||
Error::<Test>::DurationTooBig
|
||||
);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_eq!(Balances::reserved_balance(1), 10);
|
||||
assert_eq!(Queues::<Test>::get(2), vec![Bid { amount: 10, who: 1 }]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(0, 0), (1, 10), (0, 0)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_bid_queuing_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 20, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 5, 2));
|
||||
assert_noop!(Nis::place_bid(RuntimeOrigin::signed(1), 5, 2), Error::<Test>::BidTooLow);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 15, 2));
|
||||
assert_eq!(Balances::reserved_balance(1), 45);
|
||||
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 25, 2));
|
||||
assert_eq!(Balances::reserved_balance(1), 60);
|
||||
assert_noop!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2), Error::<Test>::BidTooLow);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(2),
|
||||
vec![
|
||||
Bid { amount: 15, who: 1 },
|
||||
Bid { amount: 25, who: 1 },
|
||||
Bid { amount: 20, who: 1 },
|
||||
]
|
||||
);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(0, 0), (3, 60), (0, 0)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_bid_fails_when_queue_full() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 10, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(3), 10, 2));
|
||||
assert_noop!(Nis::place_bid(RuntimeOrigin::signed(4), 10, 2), Error::<Test>::BidTooLow);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(4), 10, 3));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_place_bids_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 3));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 10, 2));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 40);
|
||||
assert_eq!(Balances::reserved_balance(2), 10);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![Bid { amount: 10, who: 1 },]);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(2),
|
||||
vec![
|
||||
Bid { amount: 10, who: 2 },
|
||||
Bid { amount: 10, who: 1 },
|
||||
Bid { amount: 10, who: 1 },
|
||||
]
|
||||
);
|
||||
assert_eq!(Queues::<Test>::get(3), vec![Bid { amount: 10, who: 1 },]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 10), (3, 30), (1, 10)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retract_single_item_queue_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Nis::retract_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(1), 10);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![]);
|
||||
assert_eq!(Queues::<Test>::get(2), vec![Bid { amount: 10, who: 1 }]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(0, 0), (1, 10), (0, 0)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retract_with_other_and_duplicate_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 10, 2));
|
||||
|
||||
assert_ok!(Nis::retract_bid(RuntimeOrigin::signed(1), 10, 2));
|
||||
assert_eq!(Balances::reserved_balance(1), 20);
|
||||
assert_eq!(Balances::reserved_balance(2), 10);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![Bid { amount: 10, who: 1 },]);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(2),
|
||||
vec![Bid { amount: 10, who: 2 }, Bid { amount: 10, who: 1 },]
|
||||
);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 10), (2, 20), (0, 0)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retract_non_existent_item_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_noop!(Nis::retract_bid(RuntimeOrigin::signed(1), 10, 1), Error::<Test>::NotFound);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 10, 1));
|
||||
assert_noop!(Nis::retract_bid(RuntimeOrigin::signed(1), 20, 1), Error::<Test>::NotFound);
|
||||
assert_noop!(Nis::retract_bid(RuntimeOrigin::signed(1), 10, 2), Error::<Test>::NotFound);
|
||||
assert_noop!(Nis::retract_bid(RuntimeOrigin::signed(2), 10, 1), Error::<Test>::NotFound);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_enlarge_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 40, 2));
|
||||
enlarge(40, 2);
|
||||
|
||||
// Takes 2/2, then stopped because it reaches its max amount
|
||||
assert_eq!(Balances::reserved_balance(1), 40);
|
||||
assert_eq!(Balances::reserved_balance(2), 0);
|
||||
assert_eq!(pot(), 40);
|
||||
|
||||
assert_eq!(Queues::<Test>::get(1), vec![Bid { amount: 40, who: 1 }]);
|
||||
assert_eq!(Queues::<Test>::get(2), vec![]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 40), (0, 0), (0, 0)]);
|
||||
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::from_percent(10),
|
||||
index: 1,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(0).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 2, expiry: 7 }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enlarge_respects_bids_limit() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 40, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(3), 40, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(4), 40, 3));
|
||||
enlarge(100, 2);
|
||||
|
||||
// Should have taken 4/3 and 2/2, then stopped because it's only allowed 2.
|
||||
assert_eq!(Queues::<Test>::get(1), vec![Bid { amount: 40, who: 1 }]);
|
||||
assert_eq!(Queues::<Test>::get(2), vec![Bid { amount: 40, who: 3 }]);
|
||||
assert_eq!(Queues::<Test>::get(3), vec![]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 40), (1, 40), (0, 0)]);
|
||||
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(0).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 4, expiry: 10 }
|
||||
);
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(1).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 2, expiry: 7 }
|
||||
);
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::from_percent(20),
|
||||
index: 2,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero()
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enlarge_respects_amount_limit_and_will_split() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 80, 1));
|
||||
enlarge(40, 2);
|
||||
|
||||
// Takes 2/2, then stopped because it reaches its max amount
|
||||
assert_eq!(Queues::<Test>::get(1), vec![Bid { amount: 40, who: 1 }]);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 40), (0, 0), (0, 0)]);
|
||||
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(0).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 1, expiry: 4 }
|
||||
);
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::from_percent(10),
|
||||
index: 1,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero()
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_thaw_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_eq!(Nis::issuance().effective, 400);
|
||||
assert_eq!(Balances::free_balance(1), 60);
|
||||
assert_eq!(Balances::reserved_balance(1), 40);
|
||||
assert_eq!(pot(), 0);
|
||||
|
||||
enlarge(40, 1);
|
||||
assert_eq!(Nis::issuance().effective, 400);
|
||||
assert_eq!(Balances::free_balance(1), 60);
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
assert_eq!(pot(), 40);
|
||||
|
||||
run_to_block(3);
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(1), 0, None), Error::<Test>::NotExpired);
|
||||
run_to_block(4);
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(1), 1, None), Error::<Test>::Unknown);
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(2), 0, None), Error::<Test>::NotOwner);
|
||||
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 0, None));
|
||||
assert_eq!(NisBalances::free_balance(1), 0);
|
||||
assert_eq!(Nis::typed_attribute::<_, Perquintill>(&0, b"proportion"), None);
|
||||
assert_eq!(Nis::issuance().effective, 400);
|
||||
assert_eq!(Balances::free_balance(1), 100);
|
||||
assert_eq!(pot(), 0);
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::zero(),
|
||||
index: 1,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::from_percent(10)
|
||||
}
|
||||
);
|
||||
assert_eq!(Receipts::<Test>::get(0), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_thaw_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 80, 1));
|
||||
enlarge(80, 1);
|
||||
assert_eq!(pot(), 80);
|
||||
|
||||
run_to_block(4);
|
||||
assert_noop!(
|
||||
Nis::thaw(RuntimeOrigin::signed(1), 0, Some(4_100_000)),
|
||||
Error::<Test>::MakesDust
|
||||
);
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 0, Some(1_050_000)));
|
||||
|
||||
assert_eq!(NisBalances::free_balance(1), 3_150_000);
|
||||
assert_eq!(
|
||||
Nis::typed_attribute::<_, Perquintill>(&0, b"proportion"),
|
||||
Some(Perquintill::from_rational(3_150_000u64, 21_000_000u64)),
|
||||
);
|
||||
|
||||
assert_eq!(Nis::issuance().effective, 400);
|
||||
assert_eq!(Balances::free_balance(1), 40);
|
||||
assert_eq!(pot(), 60);
|
||||
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 0, None));
|
||||
|
||||
assert_eq!(Nis::issuance().effective, 400);
|
||||
assert_eq!(Balances::free_balance(1), 100);
|
||||
assert_eq!(pot(), 0);
|
||||
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::zero(),
|
||||
index: 1,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::from_percent(20)
|
||||
}
|
||||
);
|
||||
assert_eq!(Receipts::<Test>::get(0), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thaw_respects_transfers() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
enlarge(40, 1);
|
||||
run_to_block(4);
|
||||
|
||||
assert_eq!(Nis::owner(&0), Some(1));
|
||||
assert_ok!(Nis::transfer(&0, &2));
|
||||
|
||||
// Transfering the receipt...
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(1), 0, None), Error::<Test>::NotOwner);
|
||||
// ...can't be thawed due to missing counterpart
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(2), 0, None), TokenError::NoFunds);
|
||||
|
||||
// Transfer the counterpart also...
|
||||
assert_ok!(NisBalances::transfer(RuntimeOrigin::signed(1), 2, 2100000));
|
||||
// ...and thawing is possible.
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(2), 0, None));
|
||||
|
||||
assert_eq!(Balances::free_balance(2), 140);
|
||||
assert_eq!(Balances::free_balance(1), 60);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thaw_when_issuance_higher_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 100, 1));
|
||||
enlarge(100, 1);
|
||||
|
||||
assert_eq!(NisBalances::free_balance(1), 5_250_000); // (25% of 21m)
|
||||
|
||||
// Everybody else's balances goes up by 50%
|
||||
Balances::make_free_balance_be(&2, 150);
|
||||
Balances::make_free_balance_be(&3, 150);
|
||||
Balances::make_free_balance_be(&4, 150);
|
||||
|
||||
run_to_block(4);
|
||||
|
||||
// Unfunded initially...
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(1), 0, None), Error::<Test>::Unfunded);
|
||||
// ...so we fund.
|
||||
assert_ok!(Nis::fund_deficit(RuntimeOrigin::signed(1)));
|
||||
|
||||
// Transfer counterpart away...
|
||||
assert_ok!(NisBalances::transfer(RuntimeOrigin::signed(1), 2, 250_000));
|
||||
// ...and it's not thawable.
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(1), 0, None), TokenError::NoFunds);
|
||||
|
||||
// Transfer counterpart back...
|
||||
assert_ok!(NisBalances::transfer(RuntimeOrigin::signed(2), 1, 250_000));
|
||||
// ...and it is.
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 0, None));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 150);
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thaw_with_ignored_issuance_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
// Give account zero some balance.
|
||||
Balances::make_free_balance_be(&0, 200);
|
||||
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 100, 1));
|
||||
enlarge(100, 1);
|
||||
|
||||
// Account zero transfers 50 into everyone else's accounts.
|
||||
assert_ok!(Balances::transfer(RuntimeOrigin::signed(0), 2, 50));
|
||||
assert_ok!(Balances::transfer(RuntimeOrigin::signed(0), 3, 50));
|
||||
assert_ok!(Balances::transfer(RuntimeOrigin::signed(0), 4, 50));
|
||||
|
||||
run_to_block(4);
|
||||
// Unfunded initially...
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(1), 0, None), Error::<Test>::Unfunded);
|
||||
// ...so we fund...
|
||||
assert_ok!(Nis::fund_deficit(RuntimeOrigin::signed(1)));
|
||||
// ...and then it's ok.
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 0, None));
|
||||
|
||||
// Account zero changes have been ignored.
|
||||
assert_eq!(Balances::free_balance(1), 150);
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thaw_when_issuance_lower_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 100, 1));
|
||||
enlarge(100, 1);
|
||||
|
||||
// Everybody else's balances goes down by 25%
|
||||
Balances::make_free_balance_be(&2, 75);
|
||||
Balances::make_free_balance_be(&3, 75);
|
||||
Balances::make_free_balance_be(&4, 75);
|
||||
|
||||
run_to_block(4);
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 0, None));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 75);
|
||||
assert_eq!(Balances::reserved_balance(1), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_thaws_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 60, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 50, 1));
|
||||
enlarge(200, 3);
|
||||
|
||||
// Double everyone's free balances.
|
||||
Balances::make_free_balance_be(&2, 100);
|
||||
Balances::make_free_balance_be(&3, 200);
|
||||
Balances::make_free_balance_be(&4, 200);
|
||||
assert_ok!(Nis::fund_deficit(RuntimeOrigin::signed(1)));
|
||||
|
||||
run_to_block(4);
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 0, None));
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 1, None));
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(2), 2, None), Error::<Test>::Throttled);
|
||||
run_to_block(5);
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(2), 2, None));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 200);
|
||||
assert_eq!(Balances::free_balance(2), 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_thaws_works_in_alternative_thaw_order() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(1);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 60, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 50, 1));
|
||||
enlarge(200, 3);
|
||||
|
||||
// Double everyone's free balances.
|
||||
Balances::make_free_balance_be(&2, 100);
|
||||
Balances::make_free_balance_be(&3, 200);
|
||||
Balances::make_free_balance_be(&4, 200);
|
||||
assert_ok!(Nis::fund_deficit(RuntimeOrigin::signed(1)));
|
||||
|
||||
run_to_block(4);
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(2), 2, None));
|
||||
assert_noop!(Nis::thaw(RuntimeOrigin::signed(1), 1, None), Error::<Test>::Throttled);
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 0, None));
|
||||
|
||||
run_to_block(5);
|
||||
assert_ok!(Nis::thaw(RuntimeOrigin::signed(1), 1, None));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 200);
|
||||
assert_eq!(Balances::free_balance(2), 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enlargement_to_target_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(2);
|
||||
let w = <() as WeightInfo>::process_queues() +
|
||||
<() as WeightInfo>::process_queue() +
|
||||
(<() as WeightInfo>::process_bid() * 2);
|
||||
super::mock::MaxIntakeWeight::set(w);
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 40, 1));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(1), 40, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 40, 2));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(2), 40, 3));
|
||||
assert_ok!(Nis::place_bid(RuntimeOrigin::signed(3), 40, 3));
|
||||
Target::set(Perquintill::from_percent(40));
|
||||
|
||||
run_to_block(3);
|
||||
assert_eq!(Queues::<Test>::get(1), vec![Bid { amount: 40, who: 1 },]);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(2),
|
||||
vec![Bid { amount: 40, who: 2 }, Bid { amount: 40, who: 1 },]
|
||||
);
|
||||
assert_eq!(
|
||||
Queues::<Test>::get(3),
|
||||
vec![Bid { amount: 40, who: 3 }, Bid { amount: 40, who: 2 },]
|
||||
);
|
||||
assert_eq!(QueueTotals::<Test>::get(), vec![(1, 40), (2, 80), (2, 80)]);
|
||||
|
||||
run_to_block(4);
|
||||
// Two new items should have been issued to 2 & 3 for 40 each & duration of 3.
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(0).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 2, expiry: 13 }
|
||||
);
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(1).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 3, expiry: 13 }
|
||||
);
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::from_percent(20),
|
||||
index: 2,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero(),
|
||||
}
|
||||
);
|
||||
|
||||
run_to_block(5);
|
||||
// No change
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::from_percent(20),
|
||||
index: 2,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero()
|
||||
}
|
||||
);
|
||||
|
||||
run_to_block(6);
|
||||
// Two new items should have been issued to 1 & 2 for 40 each & duration of 2.
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(2).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 1, expiry: 12 }
|
||||
);
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(3).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 2, expiry: 12 }
|
||||
);
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::from_percent(40),
|
||||
index: 4,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero()
|
||||
}
|
||||
);
|
||||
|
||||
run_to_block(8);
|
||||
// No change now.
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::from_percent(40),
|
||||
index: 4,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero()
|
||||
}
|
||||
);
|
||||
|
||||
// Set target a bit higher to use up the remaining bid.
|
||||
Target::set(Perquintill::from_percent(60));
|
||||
run_to_block(10);
|
||||
|
||||
// Two new items should have been issued to 1 & 2 for 40 each & duration of 2.
|
||||
assert_eq!(
|
||||
Receipts::<Test>::get(4).unwrap(),
|
||||
ReceiptRecord { proportion: Perquintill::from_percent(10), who: 1, expiry: 13 }
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Summary::<Test>::get(),
|
||||
SummaryRecord {
|
||||
proportion_owed: Perquintill::from_percent(50),
|
||||
index: 5,
|
||||
last_period: 0,
|
||||
thawed: Perquintill::zero()
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Autogenerated weights for pallet_gilt
|
||||
//! Autogenerated weights for pallet_nis
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2022-11-07, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
@@ -29,14 +29,12 @@
|
||||
// --chain=dev
|
||||
// --steps=50
|
||||
// --repeat=20
|
||||
// --pallet=pallet_gilt
|
||||
// --pallet=pallet_nis
|
||||
// --extrinsic=*
|
||||
// --execution=wasm
|
||||
// --wasm-execution=compiled
|
||||
// --heap-pages=4096
|
||||
// --output=./frame/gilt/src/weights.rs
|
||||
// --header=./HEADER-APACHE2
|
||||
// --template=./.maintain/frame-weight-template.hbs
|
||||
// --output=./frame/nis/src/weights.rs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
@@ -45,24 +43,23 @@
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for pallet_gilt.
|
||||
/// Weight functions needed for pallet_nis.
|
||||
pub trait WeightInfo {
|
||||
fn place_bid(l: u32, ) -> Weight;
|
||||
fn place_bid_max() -> Weight;
|
||||
fn retract_bid(l: u32, ) -> Weight;
|
||||
fn set_target() -> Weight;
|
||||
fn thaw() -> Weight;
|
||||
fn pursue_target_noop() -> Weight;
|
||||
fn pursue_target_per_item(b: u32, ) -> Weight;
|
||||
fn pursue_target_per_queue(q: u32, ) -> Weight;
|
||||
fn fund_deficit() -> Weight;
|
||||
fn process_queues() -> Weight;
|
||||
fn process_queue() -> Weight;
|
||||
fn process_bid() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for pallet_gilt using the Substrate node and recommended hardware.
|
||||
/// Weights for pallet_nis using the Substrate node and recommended hardware.
|
||||
pub struct SubstrateWeight<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
// Storage: Gilt Queues (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
/// The range of component `l` is `[0, 999]`.
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
fn place_bid(l: u32, ) -> Weight {
|
||||
// Minimum execution time: 42_332 nanoseconds.
|
||||
Weight::from_ref_time(45_584_514 as u64)
|
||||
@@ -71,17 +68,16 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
.saturating_add(T::DbWeight::get().reads(2 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Gilt Queues (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
fn place_bid_max() -> Weight {
|
||||
// Minimum execution time: 85_866 nanoseconds.
|
||||
Weight::from_ref_time(87_171_000 as u64)
|
||||
.saturating_add(T::DbWeight::get().reads(2 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Gilt Queues (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
/// The range of component `l` is `[1, 1000]`.
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
fn retract_bid(l: u32, ) -> Weight {
|
||||
// Minimum execution time: 44_605 nanoseconds.
|
||||
Weight::from_ref_time(46_850_108 as u64)
|
||||
@@ -90,63 +86,52 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
.saturating_add(T::DbWeight::get().reads(2 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Gilt ActiveTotal (r:1 w:1)
|
||||
fn set_target() -> Weight {
|
||||
// Minimum execution time: 7_331 nanoseconds.
|
||||
Weight::from_ref_time(7_619_000 as u64)
|
||||
.saturating_add(T::DbWeight::get().reads(1 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as u64))
|
||||
}
|
||||
// Storage: Gilt Active (r:1 w:1)
|
||||
// Storage: Gilt ActiveTotal (r:1 w:1)
|
||||
// Storage: Nis Active (r:1 w:1)
|
||||
// Storage: Nis ActiveTotal (r:1 w:1)
|
||||
fn thaw() -> Weight {
|
||||
// Minimum execution time: 55_143 nanoseconds.
|
||||
Weight::from_ref_time(55_845_000 as u64)
|
||||
.saturating_add(T::DbWeight::get().reads(2 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Gilt ActiveTotal (r:1 w:0)
|
||||
fn pursue_target_noop() -> Weight {
|
||||
// Minimum execution time: 3_386 nanoseconds.
|
||||
Weight::from_ref_time(3_461_000 as u64)
|
||||
// Storage: Nis Active (r:1 w:1)
|
||||
// Storage: Nis ActiveTotal (r:1 w:1)
|
||||
fn fund_deficit() -> Weight {
|
||||
Weight::from_ref_time(47_753_000 as u64)
|
||||
.saturating_add(T::DbWeight::get().reads(2 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Nis ActiveTotal (r:1 w:0)
|
||||
fn process_queues() -> Weight {
|
||||
Weight::from_ref_time(1_663_000 as u64)
|
||||
.saturating_add(T::DbWeight::get().reads(1 as u64))
|
||||
}
|
||||
// Storage: Gilt ActiveTotal (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
// Storage: Gilt Queues (r:1 w:1)
|
||||
// Storage: Gilt Active (r:0 w:20)
|
||||
/// The range of component `b` is `[0, 1000]`.
|
||||
fn pursue_target_per_item(b: u32, ) -> Weight {
|
||||
// Minimum execution time: 34_156 nanoseconds.
|
||||
Weight::from_ref_time(45_262_859 as u64)
|
||||
// Standard Error: 1_529
|
||||
.saturating_add(Weight::from_ref_time(4_181_654 as u64).saturating_mul(b as u64))
|
||||
// Storage: Nis ActiveTotal (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis Active (r:0 w:1)
|
||||
fn process_queue() -> Weight {
|
||||
Weight::from_ref_time(40_797_000 as u64)
|
||||
// Standard Error: 1_000
|
||||
.saturating_add(T::DbWeight::get().reads(3 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(b as u64)))
|
||||
}
|
||||
// Storage: Gilt ActiveTotal (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
// Storage: Gilt Queues (r:6 w:6)
|
||||
// Storage: Gilt Active (r:0 w:6)
|
||||
/// The range of component `q` is `[0, 300]`.
|
||||
fn pursue_target_per_queue(q: u32, ) -> Weight {
|
||||
// Minimum execution time: 33_526 nanoseconds.
|
||||
Weight::from_ref_time(37_255_562 as u64)
|
||||
// Standard Error: 3_611
|
||||
.saturating_add(Weight::from_ref_time(7_193_128 as u64).saturating_mul(q as u64))
|
||||
// Storage: Nis ActiveTotal (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis Active (r:0 w:1)
|
||||
fn process_bid() -> Weight {
|
||||
Weight::from_ref_time(14_944_000 as u64)
|
||||
// Standard Error: 6_000
|
||||
.saturating_add(T::DbWeight::get().reads(2 as u64))
|
||||
.saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(q as u64)))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as u64))
|
||||
.saturating_add(T::DbWeight::get().writes((2 as u64).saturating_mul(q as u64)))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
// Storage: Gilt Queues (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
/// The range of component `l` is `[0, 999]`.
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
fn place_bid(l: u32, ) -> Weight {
|
||||
// Minimum execution time: 42_332 nanoseconds.
|
||||
Weight::from_ref_time(45_584_514 as u64)
|
||||
@@ -155,17 +140,16 @@ impl WeightInfo for () {
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Gilt Queues (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
fn place_bid_max() -> Weight {
|
||||
// Minimum execution time: 85_866 nanoseconds.
|
||||
Weight::from_ref_time(87_171_000 as u64)
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Gilt Queues (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
/// The range of component `l` is `[1, 1000]`.
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
fn retract_bid(l: u32, ) -> Weight {
|
||||
// Minimum execution time: 44_605 nanoseconds.
|
||||
Weight::from_ref_time(46_850_108 as u64)
|
||||
@@ -174,54 +158,44 @@ impl WeightInfo for () {
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Gilt ActiveTotal (r:1 w:1)
|
||||
fn set_target() -> Weight {
|
||||
// Minimum execution time: 7_331 nanoseconds.
|
||||
Weight::from_ref_time(7_619_000 as u64)
|
||||
.saturating_add(RocksDbWeight::get().reads(1 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as u64))
|
||||
}
|
||||
// Storage: Gilt Active (r:1 w:1)
|
||||
// Storage: Gilt ActiveTotal (r:1 w:1)
|
||||
// Storage: Nis Active (r:1 w:1)
|
||||
// Storage: Nis ActiveTotal (r:1 w:1)
|
||||
fn thaw() -> Weight {
|
||||
// Minimum execution time: 55_143 nanoseconds.
|
||||
Weight::from_ref_time(55_845_000 as u64)
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Gilt ActiveTotal (r:1 w:0)
|
||||
fn pursue_target_noop() -> Weight {
|
||||
// Minimum execution time: 3_386 nanoseconds.
|
||||
Weight::from_ref_time(3_461_000 as u64)
|
||||
// Storage: Nis Active (r:1 w:1)
|
||||
// Storage: Nis ActiveTotal (r:1 w:1)
|
||||
fn fund_deficit() -> Weight {
|
||||
Weight::from_ref_time(47_753_000 as u64)
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as u64))
|
||||
}
|
||||
// Storage: Nis ActiveTotal (r:1 w:0)
|
||||
fn process_queues() -> Weight {
|
||||
Weight::from_ref_time(1_663_000 as u64)
|
||||
.saturating_add(RocksDbWeight::get().reads(1 as u64))
|
||||
}
|
||||
// Storage: Gilt ActiveTotal (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
// Storage: Gilt Queues (r:1 w:1)
|
||||
// Storage: Gilt Active (r:0 w:20)
|
||||
/// The range of component `b` is `[0, 1000]`.
|
||||
fn pursue_target_per_item(b: u32, ) -> Weight {
|
||||
// Minimum execution time: 34_156 nanoseconds.
|
||||
Weight::from_ref_time(45_262_859 as u64)
|
||||
// Standard Error: 1_529
|
||||
.saturating_add(Weight::from_ref_time(4_181_654 as u64).saturating_mul(b as u64))
|
||||
// Storage: Nis ActiveTotal (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis Active (r:0 w:1)
|
||||
fn process_queue() -> Weight {
|
||||
Weight::from_ref_time(40_797_000 as u64)
|
||||
// Standard Error: 1_000
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(b as u64)))
|
||||
}
|
||||
// Storage: Gilt ActiveTotal (r:1 w:1)
|
||||
// Storage: Gilt QueueTotals (r:1 w:1)
|
||||
// Storage: Gilt Queues (r:6 w:6)
|
||||
// Storage: Gilt Active (r:0 w:6)
|
||||
/// The range of component `q` is `[0, 300]`.
|
||||
fn pursue_target_per_queue(q: u32, ) -> Weight {
|
||||
// Minimum execution time: 33_526 nanoseconds.
|
||||
Weight::from_ref_time(37_255_562 as u64)
|
||||
// Standard Error: 3_611
|
||||
.saturating_add(Weight::from_ref_time(7_193_128 as u64).saturating_mul(q as u64))
|
||||
// Storage: Nis ActiveTotal (r:1 w:1)
|
||||
// Storage: Nis QueueTotals (r:1 w:1)
|
||||
// Storage: Nis Queues (r:1 w:1)
|
||||
// Storage: Nis Active (r:0 w:1)
|
||||
fn process_bid() -> Weight {
|
||||
Weight::from_ref_time(14_944_000 as u64)
|
||||
// Standard Error: 6_000
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as u64))
|
||||
.saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(q as u64)))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as u64))
|
||||
.saturating_add(RocksDbWeight::get().writes((2 as u64).saturating_mul(q as u64)))
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ pub use tokens::{
|
||||
},
|
||||
fungible, fungibles,
|
||||
imbalance::{Imbalance, OnUnbalanced, SignedImbalance},
|
||||
BalanceStatus, ExistenceRequirement, Locker, WithdrawReasons,
|
||||
nonfungible, nonfungibles, BalanceStatus, ExistenceRequirement, Locker, WithdrawReasons,
|
||||
};
|
||||
|
||||
mod members;
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::dispatch::Parameter;
|
||||
use codec::{CompactLen, Decode, DecodeLimit, Encode, EncodeLike, Input, MaxEncodedLen};
|
||||
use impl_trait_for_tuples::impl_for_tuples;
|
||||
use scale_info::{build::Fields, meta_type, Path, Type, TypeInfo, TypeParameter};
|
||||
use sp_arithmetic::traits::{CheckedAdd, CheckedMul, CheckedSub, Saturating};
|
||||
use sp_arithmetic::traits::{CheckedAdd, CheckedMul, CheckedSub, One, Saturating};
|
||||
use sp_core::bounded::bounded_vec::TruncateFrom;
|
||||
#[doc(hidden)]
|
||||
pub use sp_runtime::traits::{
|
||||
@@ -348,17 +348,25 @@ impl<T> DefensiveOption<T> for Option<T> {
|
||||
/// A variant of [`Defensive`] with the same rationale, for the arithmetic operations where in
|
||||
/// case an infallible operation fails, it saturates.
|
||||
pub trait DefensiveSaturating {
|
||||
/// Add `self` and `other` defensively.
|
||||
/// Return `self` plus `other` defensively.
|
||||
fn defensive_saturating_add(self, other: Self) -> Self;
|
||||
/// Subtract `other` from `self` defensively.
|
||||
/// Return `self` minus `other` defensively.
|
||||
fn defensive_saturating_sub(self, other: Self) -> Self;
|
||||
/// Multiply `self` and `other` defensively.
|
||||
/// Return the product of `self` and `other` defensively.
|
||||
fn defensive_saturating_mul(self, other: Self) -> Self;
|
||||
/// Increase `self` by `other` defensively.
|
||||
fn defensive_saturating_accrue(&mut self, other: Self);
|
||||
/// Reduce `self` by `other` defensively.
|
||||
fn defensive_saturating_reduce(&mut self, other: Self);
|
||||
/// Increment `self` by one defensively.
|
||||
fn defensive_saturating_inc(&mut self);
|
||||
/// Decrement `self` by one defensively.
|
||||
fn defensive_saturating_dec(&mut self);
|
||||
}
|
||||
|
||||
// NOTE: A bit unfortunate, since T has to be bound by all the traits needed. Could make it
|
||||
// `DefensiveSaturating<T>` to mitigate.
|
||||
impl<T: Saturating + CheckedAdd + CheckedMul + CheckedSub> DefensiveSaturating for T {
|
||||
impl<T: Saturating + CheckedAdd + CheckedMul + CheckedSub + One> DefensiveSaturating for T {
|
||||
fn defensive_saturating_add(self, other: Self) -> Self {
|
||||
self.checked_add(&other).defensive_unwrap_or_else(|| self.saturating_add(other))
|
||||
}
|
||||
@@ -368,6 +376,20 @@ impl<T: Saturating + CheckedAdd + CheckedMul + CheckedSub> DefensiveSaturating f
|
||||
fn defensive_saturating_mul(self, other: Self) -> Self {
|
||||
self.checked_mul(&other).defensive_unwrap_or_else(|| self.saturating_mul(other))
|
||||
}
|
||||
fn defensive_saturating_accrue(&mut self, other: Self) {
|
||||
// Use `replace` here since `take` would require `T: Default`.
|
||||
*self = sp_std::mem::replace(self, One::one()).defensive_saturating_add(other);
|
||||
}
|
||||
fn defensive_saturating_reduce(&mut self, other: Self) {
|
||||
// Use `replace` here since `take` would require `T: Default`.
|
||||
*self = sp_std::mem::replace(self, One::one()).defensive_saturating_sub(other);
|
||||
}
|
||||
fn defensive_saturating_inc(&mut self) {
|
||||
self.defensive_saturating_accrue(One::one());
|
||||
}
|
||||
fn defensive_saturating_dec(&mut self) {
|
||||
self.defensive_saturating_reduce(One::one());
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an object by defensively truncating an input if the `TryFrom` conversion fails.
|
||||
@@ -1119,6 +1141,92 @@ mod test {
|
||||
use sp_core::bounded::{BoundedSlice, BoundedVec};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn defensive_saturating_accrue_works() {
|
||||
let mut v = 1_u32;
|
||||
v.defensive_saturating_accrue(2);
|
||||
assert_eq!(v, 3);
|
||||
v.defensive_saturating_accrue(u32::MAX);
|
||||
assert_eq!(v, u32::MAX);
|
||||
v.defensive_saturating_accrue(1);
|
||||
assert_eq!(v, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
#[should_panic(expected = "Defensive")]
|
||||
fn defensive_saturating_accrue_panics() {
|
||||
let mut v = u32::MAX;
|
||||
v.defensive_saturating_accrue(1); // defensive failure
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn defensive_saturating_reduce_works() {
|
||||
let mut v = u32::MAX;
|
||||
v.defensive_saturating_reduce(3);
|
||||
assert_eq!(v, u32::MAX - 3);
|
||||
v.defensive_saturating_reduce(u32::MAX);
|
||||
assert_eq!(v, 0);
|
||||
v.defensive_saturating_reduce(1);
|
||||
assert_eq!(v, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
#[should_panic(expected = "Defensive")]
|
||||
fn defensive_saturating_reduce_panics() {
|
||||
let mut v = 0_u32;
|
||||
v.defensive_saturating_reduce(1); // defensive failure
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn defensive_saturating_inc_works() {
|
||||
let mut v = 0_u32;
|
||||
for i in 1..10 {
|
||||
v.defensive_saturating_inc();
|
||||
assert_eq!(v, i);
|
||||
}
|
||||
v += u32::MAX - 10;
|
||||
v.defensive_saturating_inc();
|
||||
assert_eq!(v, u32::MAX);
|
||||
v.defensive_saturating_inc();
|
||||
assert_eq!(v, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
#[should_panic(expected = "Defensive")]
|
||||
fn defensive_saturating_inc_panics() {
|
||||
let mut v = u32::MAX;
|
||||
v.defensive_saturating_inc(); // defensive failure
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn defensive_saturating_dec_works() {
|
||||
let mut v = u32::MAX;
|
||||
for i in 1..10 {
|
||||
v.defensive_saturating_dec();
|
||||
assert_eq!(v, u32::MAX - i);
|
||||
}
|
||||
v -= u32::MAX - 10;
|
||||
v.defensive_saturating_dec();
|
||||
assert_eq!(v, 0);
|
||||
v.defensive_saturating_dec();
|
||||
assert_eq!(v, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
#[should_panic(expected = "Defensive")]
|
||||
fn defensive_saturating_dec_panics() {
|
||||
let mut v = 0_u32;
|
||||
v.defensive_saturating_dec(); // defensive failure
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn defensive_truncating_from_vec_defensive_works() {
|
||||
|
||||
@@ -17,8 +17,14 @@
|
||||
|
||||
//! The reservable currency trait.
|
||||
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::Get;
|
||||
|
||||
use super::{super::misc::BalanceStatus, Currency};
|
||||
use crate::dispatch::{DispatchError, DispatchResult};
|
||||
use crate::{
|
||||
dispatch::{DispatchError, DispatchResult},
|
||||
traits::{ExistenceRequirement, SignedImbalance, WithdrawReasons},
|
||||
};
|
||||
|
||||
/// A currency where funds can be reserved from the user.
|
||||
pub trait ReservableCurrency<AccountId>: Currency<AccountId> {
|
||||
@@ -111,7 +117,7 @@ impl<AccountId> ReservableCurrency<AccountId> for () {
|
||||
pub trait NamedReservableCurrency<AccountId>: ReservableCurrency<AccountId> {
|
||||
/// An identifier for a reserve. Used for disambiguating different reserves so that
|
||||
/// they can be individually replaced or removed.
|
||||
type ReserveIdentifier;
|
||||
type ReserveIdentifier: codec::Encode + TypeInfo + 'static;
|
||||
|
||||
/// Deducts up to `value` from reserved balance of `who`. This function cannot fail.
|
||||
///
|
||||
@@ -236,3 +242,144 @@ pub trait NamedReservableCurrency<AccountId>: ReservableCurrency<AccountId> {
|
||||
Self::repatriate_reserved_named(id, slashed, beneficiary, value, status).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter to allow a `NamedReservableCurrency` to be passed as regular `ReservableCurrency`
|
||||
/// together with an `Id`.
|
||||
///
|
||||
/// All "anonymous" operations are then implemented as their named counterparts with the given `Id`.
|
||||
pub struct WithName<NamedReservable, Id, AccountId>(
|
||||
sp_std::marker::PhantomData<(NamedReservable, Id, AccountId)>,
|
||||
);
|
||||
impl<
|
||||
NamedReservable: NamedReservableCurrency<AccountId>,
|
||||
Id: Get<NamedReservable::ReserveIdentifier>,
|
||||
AccountId,
|
||||
> Currency<AccountId> for WithName<NamedReservable, Id, AccountId>
|
||||
{
|
||||
type Balance = <NamedReservable as Currency<AccountId>>::Balance;
|
||||
type PositiveImbalance = <NamedReservable as Currency<AccountId>>::PositiveImbalance;
|
||||
type NegativeImbalance = <NamedReservable as Currency<AccountId>>::NegativeImbalance;
|
||||
|
||||
fn total_balance(who: &AccountId) -> Self::Balance {
|
||||
NamedReservable::total_balance(who)
|
||||
}
|
||||
fn can_slash(who: &AccountId, value: Self::Balance) -> bool {
|
||||
NamedReservable::can_slash(who, value)
|
||||
}
|
||||
fn total_issuance() -> Self::Balance {
|
||||
NamedReservable::total_issuance()
|
||||
}
|
||||
fn minimum_balance() -> Self::Balance {
|
||||
NamedReservable::minimum_balance()
|
||||
}
|
||||
fn burn(amount: Self::Balance) -> Self::PositiveImbalance {
|
||||
NamedReservable::burn(amount)
|
||||
}
|
||||
fn issue(amount: Self::Balance) -> Self::NegativeImbalance {
|
||||
NamedReservable::issue(amount)
|
||||
}
|
||||
fn pair(amount: Self::Balance) -> (Self::PositiveImbalance, Self::NegativeImbalance) {
|
||||
NamedReservable::pair(amount)
|
||||
}
|
||||
fn free_balance(who: &AccountId) -> Self::Balance {
|
||||
NamedReservable::free_balance(who)
|
||||
}
|
||||
fn ensure_can_withdraw(
|
||||
who: &AccountId,
|
||||
amount: Self::Balance,
|
||||
reasons: WithdrawReasons,
|
||||
new_balance: Self::Balance,
|
||||
) -> DispatchResult {
|
||||
NamedReservable::ensure_can_withdraw(who, amount, reasons, new_balance)
|
||||
}
|
||||
|
||||
fn transfer(
|
||||
source: &AccountId,
|
||||
dest: &AccountId,
|
||||
value: Self::Balance,
|
||||
existence_requirement: ExistenceRequirement,
|
||||
) -> DispatchResult {
|
||||
NamedReservable::transfer(source, dest, value, existence_requirement)
|
||||
}
|
||||
fn slash(who: &AccountId, value: Self::Balance) -> (Self::NegativeImbalance, Self::Balance) {
|
||||
NamedReservable::slash(who, value)
|
||||
}
|
||||
fn deposit_into_existing(
|
||||
who: &AccountId,
|
||||
value: Self::Balance,
|
||||
) -> Result<Self::PositiveImbalance, DispatchError> {
|
||||
NamedReservable::deposit_into_existing(who, value)
|
||||
}
|
||||
fn resolve_into_existing(
|
||||
who: &AccountId,
|
||||
value: Self::NegativeImbalance,
|
||||
) -> Result<(), Self::NegativeImbalance> {
|
||||
NamedReservable::resolve_into_existing(who, value)
|
||||
}
|
||||
fn deposit_creating(who: &AccountId, value: Self::Balance) -> Self::PositiveImbalance {
|
||||
NamedReservable::deposit_creating(who, value)
|
||||
}
|
||||
fn resolve_creating(who: &AccountId, value: Self::NegativeImbalance) {
|
||||
NamedReservable::resolve_creating(who, value)
|
||||
}
|
||||
fn withdraw(
|
||||
who: &AccountId,
|
||||
value: Self::Balance,
|
||||
reasons: WithdrawReasons,
|
||||
liveness: ExistenceRequirement,
|
||||
) -> Result<Self::NegativeImbalance, DispatchError> {
|
||||
NamedReservable::withdraw(who, value, reasons, liveness)
|
||||
}
|
||||
fn settle(
|
||||
who: &AccountId,
|
||||
value: Self::PositiveImbalance,
|
||||
reasons: WithdrawReasons,
|
||||
liveness: ExistenceRequirement,
|
||||
) -> Result<(), Self::PositiveImbalance> {
|
||||
NamedReservable::settle(who, value, reasons, liveness)
|
||||
}
|
||||
fn make_free_balance_be(
|
||||
who: &AccountId,
|
||||
balance: Self::Balance,
|
||||
) -> SignedImbalance<Self::Balance, Self::PositiveImbalance> {
|
||||
NamedReservable::make_free_balance_be(who, balance)
|
||||
}
|
||||
}
|
||||
impl<
|
||||
NamedReservable: NamedReservableCurrency<AccountId>,
|
||||
Id: Get<NamedReservable::ReserveIdentifier>,
|
||||
AccountId,
|
||||
> ReservableCurrency<AccountId> for WithName<NamedReservable, Id, AccountId>
|
||||
{
|
||||
fn can_reserve(who: &AccountId, value: Self::Balance) -> bool {
|
||||
NamedReservable::can_reserve(who, value)
|
||||
}
|
||||
|
||||
fn slash_reserved(
|
||||
who: &AccountId,
|
||||
value: Self::Balance,
|
||||
) -> (Self::NegativeImbalance, Self::Balance) {
|
||||
NamedReservable::slash_reserved_named(&Id::get(), who, value)
|
||||
}
|
||||
|
||||
fn reserved_balance(who: &AccountId) -> Self::Balance {
|
||||
NamedReservable::reserved_balance_named(&Id::get(), who)
|
||||
}
|
||||
|
||||
fn reserve(who: &AccountId, value: Self::Balance) -> DispatchResult {
|
||||
NamedReservable::reserve_named(&Id::get(), who, value)
|
||||
}
|
||||
|
||||
fn unreserve(who: &AccountId, value: Self::Balance) -> Self::Balance {
|
||||
NamedReservable::unreserve_named(&Id::get(), who, value)
|
||||
}
|
||||
|
||||
fn repatriate_reserved(
|
||||
slashed: &AccountId,
|
||||
beneficiary: &AccountId,
|
||||
value: Self::Balance,
|
||||
status: BalanceStatus,
|
||||
) -> Result<Self::Balance, DispatchError> {
|
||||
NamedReservable::repatriate_reserved_named(&Id::get(), slashed, beneficiary, value, status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ pub trait Mutate<AccountId>: Inspect<AccountId> {
|
||||
/// is returned and nothing is changed. If successful, the amount of tokens reduced is returned.
|
||||
///
|
||||
/// The default implementation just uses `withdraw` along with `reducible_balance` to ensure
|
||||
/// that is doesn't fail.
|
||||
/// that it doesn't fail.
|
||||
fn slash(who: &AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
|
||||
Self::burn_from(who, Self::reducible_balance(who, false).min(amount))
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ pub mod traits;
|
||||
|
||||
pub use fixed_point::{FixedI128, FixedI64, FixedPointNumber, FixedPointOperand, FixedU128};
|
||||
pub use per_things::{
|
||||
InnerOf, PerThing, PerU16, Perbill, Percent, Permill, Perquintill, Rounding, SignedRounding,
|
||||
UpperOf,
|
||||
InnerOf, MultiplyArg, PerThing, PerU16, Perbill, Percent, Permill, Perquintill, RationalArg,
|
||||
ReciprocalArg, Rounding, SignedRounding, UpperOf,
|
||||
};
|
||||
pub use rational::{Rational128, RationalInfinite};
|
||||
|
||||
|
||||
@@ -36,6 +36,57 @@ pub type InnerOf<P> = <P as PerThing>::Inner;
|
||||
/// Get the upper type of a `PerThing`.
|
||||
pub type UpperOf<P> = <P as PerThing>::Upper;
|
||||
|
||||
pub trait RationalArg:
|
||||
Clone
|
||||
+ Ord
|
||||
+ ops::Div<Self, Output = Self>
|
||||
+ ops::Rem<Self, Output = Self>
|
||||
+ ops::Add<Self, Output = Self>
|
||||
+ ops::AddAssign<Self>
|
||||
+ Unsigned
|
||||
+ Zero
|
||||
+ One
|
||||
{
|
||||
}
|
||||
|
||||
impl<
|
||||
T: Clone
|
||||
+ Ord
|
||||
+ ops::Div<Self, Output = Self>
|
||||
+ ops::Rem<Self, Output = Self>
|
||||
+ ops::Add<Self, Output = Self>
|
||||
+ ops::AddAssign<Self>
|
||||
+ Unsigned
|
||||
+ Zero
|
||||
+ One,
|
||||
> RationalArg for T
|
||||
{
|
||||
}
|
||||
|
||||
pub trait MultiplyArg:
|
||||
Clone
|
||||
+ ops::Rem<Self, Output = Self>
|
||||
+ ops::Div<Self, Output = Self>
|
||||
+ ops::Mul<Self, Output = Self>
|
||||
+ ops::Add<Self, Output = Self>
|
||||
+ Unsigned
|
||||
{
|
||||
}
|
||||
|
||||
impl<
|
||||
T: Clone
|
||||
+ ops::Rem<Self, Output = Self>
|
||||
+ ops::Div<Self, Output = Self>
|
||||
+ ops::Mul<Self, Output = Self>
|
||||
+ ops::Add<Self, Output = Self>
|
||||
+ Unsigned,
|
||||
> MultiplyArg for T
|
||||
{
|
||||
}
|
||||
|
||||
pub trait ReciprocalArg: MultiplyArg + Saturating {}
|
||||
impl<T: MultiplyArg + Saturating> ReciprocalArg for T {}
|
||||
|
||||
/// Something that implements a fixed point ration with an arbitrary granularity `X`, as _parts per
|
||||
/// `X`_.
|
||||
pub trait PerThing:
|
||||
@@ -160,13 +211,7 @@ pub trait PerThing:
|
||||
/// ```
|
||||
fn mul_floor<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone
|
||||
+ UniqueSaturatedInto<Self::Inner>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Mul<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ Unsigned,
|
||||
N: MultiplyArg + UniqueSaturatedInto<Self::Inner>,
|
||||
Self::Inner: Into<N>,
|
||||
{
|
||||
overflow_prune_mul::<N, Self>(b, self.deconstruct(), Rounding::Down)
|
||||
@@ -189,13 +234,7 @@ pub trait PerThing:
|
||||
/// ```
|
||||
fn mul_ceil<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone
|
||||
+ UniqueSaturatedInto<Self::Inner>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Mul<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ Unsigned,
|
||||
N: MultiplyArg + UniqueSaturatedInto<Self::Inner>,
|
||||
Self::Inner: Into<N>,
|
||||
{
|
||||
overflow_prune_mul::<N, Self>(b, self.deconstruct(), Rounding::Up)
|
||||
@@ -212,14 +251,7 @@ pub trait PerThing:
|
||||
/// ```
|
||||
fn saturating_reciprocal_mul<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone
|
||||
+ UniqueSaturatedInto<Self::Inner>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Mul<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ Saturating
|
||||
+ Unsigned,
|
||||
N: ReciprocalArg + UniqueSaturatedInto<Self::Inner>,
|
||||
Self::Inner: Into<N>,
|
||||
{
|
||||
saturating_reciprocal_mul::<N, Self>(b, self.deconstruct(), Rounding::NearestPrefUp)
|
||||
@@ -239,14 +271,7 @@ pub trait PerThing:
|
||||
/// ```
|
||||
fn saturating_reciprocal_mul_floor<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone
|
||||
+ UniqueSaturatedInto<Self::Inner>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Mul<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ Saturating
|
||||
+ Unsigned,
|
||||
N: ReciprocalArg + UniqueSaturatedInto<Self::Inner>,
|
||||
Self::Inner: Into<N>,
|
||||
{
|
||||
saturating_reciprocal_mul::<N, Self>(b, self.deconstruct(), Rounding::Down)
|
||||
@@ -266,14 +291,7 @@ pub trait PerThing:
|
||||
/// ```
|
||||
fn saturating_reciprocal_mul_ceil<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone
|
||||
+ UniqueSaturatedInto<Self::Inner>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Mul<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ Saturating
|
||||
+ Unsigned,
|
||||
N: ReciprocalArg + UniqueSaturatedInto<Self::Inner>,
|
||||
Self::Inner: Into<N>,
|
||||
{
|
||||
saturating_reciprocal_mul::<N, Self>(b, self.deconstruct(), Rounding::Up)
|
||||
@@ -316,17 +334,7 @@ pub trait PerThing:
|
||||
/// ```
|
||||
fn from_rational<N>(p: N, q: N) -> Self
|
||||
where
|
||||
N: Clone
|
||||
+ Ord
|
||||
+ TryInto<Self::Inner>
|
||||
+ TryInto<Self::Upper>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ ops::AddAssign<N>
|
||||
+ Unsigned
|
||||
+ Zero
|
||||
+ One,
|
||||
N: RationalArg + TryInto<Self::Inner> + TryInto<Self::Upper>,
|
||||
Self::Inner: Into<N>,
|
||||
{
|
||||
Self::from_rational_with_rounding(p, q, Rounding::Down).unwrap_or_else(|_| Self::one())
|
||||
@@ -388,34 +396,14 @@ pub trait PerThing:
|
||||
/// ```
|
||||
fn from_rational_with_rounding<N>(p: N, q: N, rounding: Rounding) -> Result<Self, ()>
|
||||
where
|
||||
N: Clone
|
||||
+ Ord
|
||||
+ TryInto<Self::Inner>
|
||||
+ TryInto<Self::Upper>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ ops::AddAssign<N>
|
||||
+ Unsigned
|
||||
+ Zero
|
||||
+ One,
|
||||
N: RationalArg + TryInto<Self::Inner> + TryInto<Self::Upper>,
|
||||
Self::Inner: Into<N>;
|
||||
|
||||
/// Same as `Self::from_rational`.
|
||||
#[deprecated = "Use from_rational instead"]
|
||||
fn from_rational_approximation<N>(p: N, q: N) -> Self
|
||||
where
|
||||
N: Clone
|
||||
+ Ord
|
||||
+ TryInto<Self::Inner>
|
||||
+ TryInto<Self::Upper>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ ops::AddAssign<N>
|
||||
+ Unsigned
|
||||
+ Zero
|
||||
+ One,
|
||||
N: RationalArg + TryInto<Self::Inner> + TryInto<Self::Upper>,
|
||||
Self::Inner: Into<N>,
|
||||
{
|
||||
Self::from_rational(p, q)
|
||||
@@ -495,13 +483,7 @@ where
|
||||
/// Overflow-prune multiplication. Accurately multiply a value by `self` without overflowing.
|
||||
fn overflow_prune_mul<N, P>(x: N, part: P::Inner, rounding: Rounding) -> N
|
||||
where
|
||||
N: Clone
|
||||
+ UniqueSaturatedInto<P::Inner>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Mul<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ Unsigned,
|
||||
N: MultiplyArg + UniqueSaturatedInto<P::Inner>,
|
||||
P: PerThing,
|
||||
P::Inner: Into<N>,
|
||||
{
|
||||
@@ -517,12 +499,7 @@ where
|
||||
/// to `x / denom * numer` for an accurate result.
|
||||
fn rational_mul_correction<N, P>(x: N, numer: P::Inner, denom: P::Inner, rounding: Rounding) -> N
|
||||
where
|
||||
N: UniqueSaturatedInto<P::Inner>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Mul<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ Unsigned,
|
||||
N: MultiplyArg + UniqueSaturatedInto<P::Inner>,
|
||||
P: PerThing,
|
||||
P::Inner: Into<N>,
|
||||
{
|
||||
@@ -803,17 +780,7 @@ macro_rules! implement_per_thing {
|
||||
#[deprecated = "Use `PerThing::from_rational` instead"]
|
||||
pub fn from_rational_approximation<N>(p: N, q: N) -> Self
|
||||
where
|
||||
N: Clone
|
||||
+ Ord
|
||||
+ TryInto<$type>
|
||||
+ TryInto<$upper_type>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ ops::AddAssign<N>
|
||||
+ Unsigned
|
||||
+ Zero
|
||||
+ One,
|
||||
N: RationalArg+ TryInto<$type> + TryInto<$upper_type>,
|
||||
$type: Into<N>
|
||||
{
|
||||
<Self as PerThing>::from_rational(p, q)
|
||||
@@ -822,17 +789,7 @@ macro_rules! implement_per_thing {
|
||||
/// See [`PerThing::from_rational`].
|
||||
pub fn from_rational<N>(p: N, q: N) -> Self
|
||||
where
|
||||
N: Clone
|
||||
+ Ord
|
||||
+ TryInto<$type>
|
||||
+ TryInto<$upper_type>
|
||||
+ ops::Div<N, Output = N>
|
||||
+ ops::Rem<N, Output = N>
|
||||
+ ops::Add<N, Output = N>
|
||||
+ ops::AddAssign<N>
|
||||
+ Unsigned
|
||||
+ Zero
|
||||
+ One,
|
||||
N: RationalArg+ TryInto<$type> + TryInto<$upper_type>,
|
||||
$type: Into<N>
|
||||
{
|
||||
<Self as PerThing>::from_rational(p, q)
|
||||
@@ -851,9 +808,7 @@ macro_rules! implement_per_thing {
|
||||
/// See [`PerThing::mul_floor`].
|
||||
pub fn mul_floor<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone + UniqueSaturatedInto<$type> +
|
||||
ops::Rem<N, Output=N> + ops::Div<N, Output=N> + ops::Mul<N, Output=N> +
|
||||
ops::Add<N, Output=N> + Unsigned,
|
||||
N: MultiplyArg + UniqueSaturatedInto<$type>,
|
||||
$type: Into<N>,
|
||||
|
||||
{
|
||||
@@ -863,9 +818,7 @@ macro_rules! implement_per_thing {
|
||||
/// See [`PerThing::mul_ceil`].
|
||||
pub fn mul_ceil<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone + UniqueSaturatedInto<$type> +
|
||||
ops::Rem<N, Output=N> + ops::Div<N, Output=N> + ops::Mul<N, Output=N> +
|
||||
ops::Add<N, Output=N> + Unsigned,
|
||||
N: MultiplyArg + UniqueSaturatedInto<$type>,
|
||||
$type: Into<N>,
|
||||
{
|
||||
PerThing::mul_ceil(self, b)
|
||||
@@ -874,9 +827,7 @@ macro_rules! implement_per_thing {
|
||||
/// See [`PerThing::saturating_reciprocal_mul`].
|
||||
pub fn saturating_reciprocal_mul<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone + UniqueSaturatedInto<$type> + ops::Rem<N, Output=N> +
|
||||
ops::Div<N, Output=N> + ops::Mul<N, Output=N> + ops::Add<N, Output=N> +
|
||||
Saturating + Unsigned,
|
||||
N: ReciprocalArg + UniqueSaturatedInto<$type>,
|
||||
$type: Into<N>,
|
||||
{
|
||||
PerThing::saturating_reciprocal_mul(self, b)
|
||||
@@ -885,9 +836,7 @@ macro_rules! implement_per_thing {
|
||||
/// See [`PerThing::saturating_reciprocal_mul_floor`].
|
||||
pub fn saturating_reciprocal_mul_floor<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone + UniqueSaturatedInto<$type> + ops::Rem<N, Output=N> +
|
||||
ops::Div<N, Output=N> + ops::Mul<N, Output=N> + ops::Add<N, Output=N> +
|
||||
Saturating + Unsigned,
|
||||
N: ReciprocalArg + UniqueSaturatedInto<$type>,
|
||||
$type: Into<N>,
|
||||
{
|
||||
PerThing::saturating_reciprocal_mul_floor(self, b)
|
||||
@@ -896,9 +845,7 @@ macro_rules! implement_per_thing {
|
||||
/// See [`PerThing::saturating_reciprocal_mul_ceil`].
|
||||
pub fn saturating_reciprocal_mul_ceil<N>(self, b: N) -> N
|
||||
where
|
||||
N: Clone + UniqueSaturatedInto<$type> + ops::Rem<N, Output=N> +
|
||||
ops::Div<N, Output=N> + ops::Mul<N, Output=N> + ops::Add<N, Output=N> +
|
||||
Saturating + Unsigned,
|
||||
N: ReciprocalArg + UniqueSaturatedInto<$type>,
|
||||
$type: Into<N>,
|
||||
{
|
||||
PerThing::saturating_reciprocal_mul_ceil(self, b)
|
||||
@@ -1133,6 +1080,11 @@ macro_rules! implement_per_thing {
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::traits::One for $name {
|
||||
fn one() -> Self {
|
||||
Self::one()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod $test_mod {
|
||||
|
||||
@@ -515,6 +515,11 @@ impl<T> Convert<T, T> for Identity {
|
||||
a
|
||||
}
|
||||
}
|
||||
impl<T> ConvertBack<T, T> for Identity {
|
||||
fn convert_back(a: T) -> T {
|
||||
a
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure that performs standard conversion using the standard Rust conversion traits.
|
||||
pub struct ConvertInto;
|
||||
@@ -524,6 +529,12 @@ impl<A, B: From<A>> Convert<A, B> for ConvertInto {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensible conversion trait. Generic over both source and destination types.
|
||||
pub trait ConvertBack<A, B>: Convert<A, B> {
|
||||
/// Make conversion back.
|
||||
fn convert_back(b: B) -> A;
|
||||
}
|
||||
|
||||
/// Convenience type to work around the highly unergonomic syntax needed
|
||||
/// to invoke the functions of overloaded generic traits, in this case
|
||||
/// `TryFrom` and `TryInto`.
|
||||
|
||||
Reference in New Issue
Block a user