use alloc::collections::btree_map::BTreeMap;
use core::iter::Sum;
use frame_support::{
pallet_prelude::OptionQuery,
storage_alias,
traits::{Currency, LockableCurrency, OnRuntimeUpgrade, ReservableCurrency},
weights::RuntimeDbWeight,
Parameter, Twox64Concat,
};
use sp_runtime::{traits::Zero, Saturating};
#[cfg(feature = "try-runtime")]
const LOG_TARGET: &str = "runtime::tips::migrations::unreserve_deposits";
type BalanceOf<T, I> =
<<T as UnlockConfig<I>>::Currency as Currency<<T as UnlockConfig<I>>::AccountId>>::Balance;
pub trait UnlockConfig<I>: 'static {
type Hash: Parameter;
type AccountId: Parameter + Ord;
type Currency: LockableCurrency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
type TipReportDepositBase: sp_core::Get<BalanceOf<Self, I>>;
type DataDepositPerByte: sp_core::Get<BalanceOf<Self, I>>;
type PalletName: sp_core::Get<&'static str>;
type DbWeight: sp_core::Get<RuntimeDbWeight>;
type BlockNumber: Parameter + Zero + Copy + Ord;
}
#[storage_alias(dynamic)]
type Tips<T: UnlockConfig<I>, I: 'static> = StorageMap<
<T as UnlockConfig<I>>::PalletName,
Twox64Concat,
<T as UnlockConfig<I>>::Hash,
crate::OpenTip<
<T as UnlockConfig<I>>::AccountId,
BalanceOf<T, I>,
<T as UnlockConfig<I>>::BlockNumber,
<T as UnlockConfig<I>>::Hash,
>,
OptionQuery,
>;
pub struct UnreserveDeposits<T: UnlockConfig<I>, I: 'static>(core::marker::PhantomData<(T, I)>);
impl<T: UnlockConfig<I>, I: 'static> UnreserveDeposits<T, I> {
fn get_deposits() -> (BTreeMap<T::AccountId, BalanceOf<T, I>>, frame_support::weights::Weight) {
use sp_core::Get;
let mut tips_len = 0;
let account_deposits: BTreeMap<T::AccountId, BalanceOf<T, I>> = Tips::<T, I>::iter()
.map(|(_hash, open_tip)| open_tip)
.fold(BTreeMap::new(), |mut acc, tip| {
tips_len.saturating_inc();
acc.entry(tip.finder).or_insert(Zero::zero()).saturating_accrue(tip.deposit);
acc
});
(account_deposits, T::DbWeight::get().reads(tips_len))
}
}
impl<T: UnlockConfig<I>, I: 'static> OnRuntimeUpgrade for UnreserveDeposits<T, I>
where
BalanceOf<T, I>: Sum,
{
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
use codec::Encode;
use frame_support::ensure;
let (account_deposits, _) = Self::get_deposits();
let account_reserved_before: BTreeMap<T::AccountId, BalanceOf<T, I>> = account_deposits
.keys()
.map(|account| (account.clone(), T::Currency::reserved_balance(&account)))
.collect();
ensure!(
account_deposits.iter().all(|(account, deposit)| *deposit <=
*account_reserved_before.get(account).unwrap_or(&Zero::zero())),
"Deposit amount is greater than reserved amount"
);
let total_deposits_to_unreserve =
account_deposits.clone().into_values().sum::<BalanceOf<T, I>>();
log::info!(target: LOG_TARGET, "Total accounts: {}", account_deposits.keys().count());
log::info!(target: LOG_TARGET, "Total amount to unreserve: {:?}", total_deposits_to_unreserve);
Ok(account_reserved_before.encode())
}
fn on_runtime_upgrade() -> frame_support::weights::Weight {
use frame_support::traits::Get;
let (account_deposits, initial_reads) = Self::get_deposits();
for (account, unreserve_amount) in account_deposits.iter() {
if unreserve_amount.is_zero() {
continue
}
T::Currency::unreserve(&account, *unreserve_amount);
}
T::DbWeight::get()
.reads_writes(account_deposits.len() as u64, account_deposits.len() as u64)
.saturating_add(initial_reads)
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(
account_reserved_before_bytes: alloc::vec::Vec<u8>,
) -> Result<(), sp_runtime::TryRuntimeError> {
use codec::Decode;
let account_reserved_before = BTreeMap::<T::AccountId, BalanceOf<T, I>>::decode(
&mut &account_reserved_before_bytes[..],
)
.map_err(|_| "Failed to decode account_reserved_before_bytes")?;
let (account_deposits, _) = Self::get_deposits();
for (account, actual_reserved_before) in account_reserved_before {
let actual_reserved_after = T::Currency::reserved_balance(&account);
let expected_amount_deducted = *account_deposits
.get(&account)
.expect("account deposit must exist to be in account_reserved_before, qed");
let expected_reserved_after =
actual_reserved_before.saturating_sub(expected_amount_deducted);
if actual_reserved_after != expected_reserved_after {
log::error!(
target: LOG_TARGET,
"Reserved balance for {:?} is incorrect. actual before: {:?}, actual after, {:?}, expected deducted: {:?}",
account,
actual_reserved_before,
actual_reserved_after,
expected_amount_deducted
);
return Err("Reserved balance is incorrect".into())
}
}
Ok(())
}
}
#[cfg(all(feature = "try-runtime", test))]
mod test {
use super::*;
use crate::{
migrations::unreserve_deposits::UnreserveDeposits,
tests::{new_test_ext, Balances, RuntimeOrigin, Test, Tips},
};
use frame_support::{assert_ok, parameter_types, traits::TypedGet};
use frame_system::pallet_prelude::BlockNumberFor;
use sp_core::ConstU64;
parameter_types! {
const PalletName: &'static str = "Tips";
}
struct UnlockConfigImpl;
impl super::UnlockConfig<()> for UnlockConfigImpl {
type Currency = Balances;
type TipReportDepositBase = ConstU64<1>;
type DataDepositPerByte = ConstU64<1>;
type Hash = sp_core::H256;
type AccountId = u128;
type BlockNumber = BlockNumberFor<Test>;
type DbWeight = ();
type PalletName = PalletName;
}
#[test]
fn unreserve_all_funds_works() {
let tipper_0 = 0;
let tipper_1 = 1;
let tipper_0_initial_reserved = 0;
let tipper_1_initial_reserved = 5;
let recipient = 100;
let tip_0_reason = b"what_is_really_not_awesome".to_vec();
let tip_1_reason = b"pineapple_on_pizza".to_vec();
new_test_ext().execute_with(|| {
assert_ok!(<Test as pallet_treasury::Config>::Currency::reserve(
&tipper_0,
tipper_0_initial_reserved
));
assert_ok!(<Test as pallet_treasury::Config>::Currency::reserve(
&tipper_1,
tipper_1_initial_reserved
));
assert_ok!(Tips::report_awesome(
RuntimeOrigin::signed(tipper_0),
tip_0_reason.clone(),
recipient
));
assert_ok!(Tips::report_awesome(
RuntimeOrigin::signed(tipper_1),
tip_1_reason.clone(),
recipient
));
assert_eq!(
<Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_0),
tipper_0_initial_reserved +
<Test as crate::Config>::TipReportDepositBase::get() +
<Test as crate::Config>::DataDepositPerByte::get() *
tip_0_reason.len() as u64
);
assert_eq!(
<Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_1),
tipper_1_initial_reserved +
<Test as crate::Config>::TipReportDepositBase::get() +
<Test as crate::Config>::DataDepositPerByte::get() *
tip_1_reason.len() as u64
);
let bytes = match UnreserveDeposits::<UnlockConfigImpl, ()>::pre_upgrade() {
Ok(bytes) => bytes,
Err(e) => panic!("pre_upgrade failed: {:?}", e),
};
UnreserveDeposits::<UnlockConfigImpl, ()>::on_runtime_upgrade();
assert_ok!(UnreserveDeposits::<UnlockConfigImpl, ()>::post_upgrade(bytes));
assert_eq!(
<Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_0),
tipper_0_initial_reserved
);
assert_eq!(
<Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_1),
tipper_1_initial_reserved
);
});
}
}