#![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 {
type WeightInfo: WeightInfo;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
#[pallet::constant]
type PalletId: Get<PalletId>;
type Currency: ReservableCurrency<Self::AccountId, Balance = Self::CurrencyBalance>;
type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
+ codec::FullCodec
+ Copy
+ MaybeSerializeDeserialize
+ sp_std::fmt::Debug
+ Default
+ From<u64>
+ TypeInfo
+ MaxEncodedLen;
type FundOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type IgnoredIssuance: Get<BalanceOf<Self>>;
type Counterpart: FungibleMutate<Self::AccountId>;
type CounterpartAmount: ConvertBack<
Perquintill,
<Self::Counterpart as FungibleInspect<Self::AccountId>>::Balance,
>;
type Deficit: OnUnbalanced<PositiveImbalanceOf<Self>>;
type Target: Get<Perquintill>;
#[pallet::constant]
type QueueCount: Get<u32>;
#[pallet::constant]
type MaxQueueLen: Get<u32>;
#[pallet::constant]
type FifoQueueLen: Get<u32>;
#[pallet::constant]
type BasePeriod: Get<Self::BlockNumber>;
#[pallet::constant]
type MinBid: Get<BalanceOf<Self>>;
#[pallet::constant]
type MinReceipt: Get<Perquintill>;
#[pallet::constant]
type IntakePeriod: Get<Self::BlockNumber>;
#[pallet::constant]
type MaxIntakeWeight: Get<Weight>;
#[pallet::constant]
type ThawThrottle: Get<(Perquintill, Self::BlockNumber)>;
}
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);
#[derive(
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
)]
pub struct Bid<Balance, AccountId> {
pub amount: Balance,
pub who: AccountId,
}
#[derive(
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
)]
pub struct ReceiptRecord<AccountId, BlockNumber> {
pub proportion: Perquintill,
pub who: AccountId,
pub expiry: BlockNumber,
}
pub type ReceiptIndex = u32;
#[derive(
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
)]
pub struct SummaryRecord<BlockNumber> {
pub proportion_owed: Perquintill,
pub index: ReceiptIndex,
pub thawed: Perquintill,
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
])
}
}
#[pallet::storage]
pub type QueueTotals<T: Config> =
StorageValue<_, QueueTotalsTypeOf<T>, ValueQuery, OnEmptyQueueTotals<T>>;
#[pallet::storage]
pub type Queues<T: Config> =
StorageMap<_, Blake2_128Concat, u32, BoundedVec<BidOf<T>, T::MaxQueueLen>, ValueQuery>;
#[pallet::storage]
pub type Summary<T> = StorageValue<_, SummaryRecordOf<T>, ValueQuery>;
#[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> {
BidPlaced { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
BidRetracted { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
BidDropped { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
Issued {
index: ReceiptIndex,
expiry: T::BlockNumber,
who: T::AccountId,
proportion: Perquintill,
amount: BalanceOf<T>,
},
Thawed {
index: ReceiptIndex,
who: T::AccountId,
proportion: Perquintill,
amount: BalanceOf<T>,
dropped: bool,
},
Funded { deficit: BalanceOf<T> },
Transferred { from: T::AccountId, to: T::AccountId, index: ReceiptIndex },
}
#[pallet::error]
pub enum Error<T> {
DurationTooSmall,
DurationTooBig,
AmountTooSmall,
BidTooLow,
Unknown,
NotOwner,
NotExpired,
NotFound,
TooMuch,
Unfunded,
Funded,
Throttled,
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> {
#[pallet::call_index(0)]
#[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)?;
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(())
}
#[pallet::call_index(1)]
#[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(())
}
#[pallet::call_index(2)]
#[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(())
}
#[pallet::call_index(3)]
#[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)?;
let mut receipt: ReceiptRecordOf<T> =
Receipts::<T>::get(index).ok_or(Error::<T>::Unknown)?;
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))?;
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(())
}
}
#[derive(RuntimeDebug)]
pub struct IssuanceInfo<Balance> {
pub holdings: Balance,
pub other: Balance,
pub effective: Balance,
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> {
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
pub fn issuance() -> IssuanceInfo<BalanceOf<T>> {
Self::issuance_with(&Self::account_id(), &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 }
}
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())
|| !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");
}
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
}
remaining.saturating_reduce(amount);
queue_amount.defensive_saturating_reduce(amount);
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);
let fung_eq = T::CounterpartAmount::convert(proportion);
let _ = T::Counterpart::mint_into(&who, fung_eq).defensive();
result
}
}
}