use crate::{PropIndex, Voting, DEMOCRACY_ID};
use alloc::{collections::btree_map::BTreeMap, vec::Vec};
use core::iter::Sum;
use frame_support::{
pallet_prelude::ValueQuery,
storage_alias,
traits::{Currency, LockableCurrency, OnRuntimeUpgrade, ReservableCurrency},
weights::RuntimeDbWeight,
Parameter, Twox64Concat,
};
use sp_core::Get;
use sp_runtime::{traits::Zero, BoundedVec, Saturating};
const LOG_TARGET: &str = "runtime::democracy::migrations::unlock_and_unreserve_all_funds";
type BalanceOf<T> =
<<T as UnlockConfig>::Currency as Currency<<T as UnlockConfig>::AccountId>>::Balance;
pub trait UnlockConfig: 'static {
type AccountId: Parameter + Ord;
type Currency: LockableCurrency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
type PalletName: Get<&'static str>;
type MaxVotes: Get<u32>;
type MaxDeposits: Get<u32>;
type DbWeight: Get<RuntimeDbWeight>;
type BlockNumber: Parameter + Zero + Copy + Ord;
}
#[storage_alias(dynamic)]
type DepositOf<T: UnlockConfig> = StorageMap<
<T as UnlockConfig>::PalletName,
Twox64Concat,
PropIndex,
(BoundedVec<<T as UnlockConfig>::AccountId, <T as UnlockConfig>::MaxDeposits>, BalanceOf<T>),
>;
#[storage_alias(dynamic)]
type VotingOf<T: UnlockConfig> = StorageMap<
<T as UnlockConfig>::PalletName,
Twox64Concat,
<T as UnlockConfig>::AccountId,
Voting<
BalanceOf<T>,
<T as UnlockConfig>::AccountId,
<T as UnlockConfig>::BlockNumber,
<T as UnlockConfig>::MaxVotes,
>,
ValueQuery,
>;
pub struct UnlockAndUnreserveAllFunds<T: UnlockConfig>(core::marker::PhantomData<T>);
impl<T: UnlockConfig> UnlockAndUnreserveAllFunds<T> {
fn get_account_deposits_and_locks() -> (
BTreeMap<T::AccountId, BalanceOf<T>>,
BTreeMap<T::AccountId, BalanceOf<T>>,
frame_support::weights::Weight,
) {
let mut deposit_of_len = 0;
let mut total_voting_vec_entries: u64 = 0;
let account_deposits: BTreeMap<T::AccountId, BalanceOf<T>> = DepositOf::<T>::iter()
.flat_map(|(_prop_index, (accounts, balance))| {
deposit_of_len.saturating_inc();
total_voting_vec_entries.saturating_accrue(accounts.len() as u64);
accounts.into_iter().map(|account| (account, balance)).collect::<Vec<_>>()
})
.fold(BTreeMap::new(), |mut acc, (account, balance)| {
acc.entry(account.clone()).or_insert(Zero::zero()).saturating_accrue(balance);
acc
});
let account_stakes: BTreeMap<T::AccountId, BalanceOf<T>> = VotingOf::<T>::iter()
.map(|(account_id, voting)| (account_id, voting.locked_balance()))
.collect();
let voting_of_len = account_stakes.len() as u64;
(
account_deposits,
account_stakes,
T::DbWeight::get().reads(
deposit_of_len.saturating_add(voting_of_len).saturating_add(
total_voting_vec_entries
.saturating_mul(T::MaxVotes::get().saturating_add(5) as u64),
),
),
)
}
}
impl<T: UnlockConfig> OnRuntimeUpgrade for UnlockAndUnreserveAllFunds<T>
where
BalanceOf<T>: Sum,
{
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
use alloc::collections::btree_set::BTreeSet;
use codec::Encode;
let (account_deposits, account_locks, _) = Self::get_account_deposits_and_locks();
let all_accounts = account_deposits
.keys()
.chain(account_locks.keys())
.cloned()
.collect::<BTreeSet<_>>();
let account_reserved_before: BTreeMap<T::AccountId, BalanceOf<T>> = account_deposits
.keys()
.map(|account| (account.clone(), T::Currency::reserved_balance(&account)))
.collect();
let bugged_deposits = all_accounts
.iter()
.filter(|account| {
account_deposits.get(&account).unwrap_or(&Zero::zero()) >
account_reserved_before.get(&account).unwrap_or(&Zero::zero())
})
.count();
let total_deposits_to_unreserve =
account_deposits.clone().into_values().sum::<BalanceOf<T>>();
let total_stake_to_unlock = account_locks.clone().into_values().sum::<BalanceOf<T>>();
log::info!(target: LOG_TARGET, "Total accounts: {:?}", all_accounts.len());
log::info!(target: LOG_TARGET, "Total stake to unlock: {:?}", total_stake_to_unlock);
log::info!(
target: LOG_TARGET,
"Total deposit to unreserve: {:?}",
total_deposits_to_unreserve
);
log::info!(
target: LOG_TARGET,
"Bugged deposits: {}/{}",
bugged_deposits,
account_deposits.len()
);
Ok(account_reserved_before.encode())
}
fn on_runtime_upgrade() -> frame_support::weights::Weight {
let (account_deposits, account_stakes, initial_reads) =
Self::get_account_deposits_and_locks();
for (account, unreserve_amount) in account_deposits.iter() {
if unreserve_amount.is_zero() {
log::warn!(target: LOG_TARGET, "Unexpected zero amount to unreserve!");
continue
}
T::Currency::unreserve(&account, *unreserve_amount);
}
for account in account_stakes.keys() {
T::Currency::remove_lock(DEMOCRACY_ID, account);
}
T::DbWeight::get()
.reads_writes(
account_stakes.len().saturating_add(account_deposits.len()) as u64,
account_stakes.len().saturating_add(account_deposits.len()) as u64,
)
.saturating_add(initial_reads)
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(
account_reserved_before_bytes: Vec<u8>,
) -> Result<(), sp_runtime::TryRuntimeError> {
use codec::Decode;
let account_reserved_before =
BTreeMap::<T::AccountId, BalanceOf<T>>::decode(&mut &account_reserved_before_bytes[..])
.map_err(|_| "Failed to decode account_reserved_before_bytes")?;
let (account_deposits, _, _) = Self::get_account_deposits_and_locks();
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 pre_migration_data, qed");
let expected_reserved_after =
actual_reserved_before.saturating_sub(expected_amount_deducted);
assert!(
actual_reserved_after == expected_reserved_after,
"Reserved balance for {:?} is incorrect. actual before: {:?}, actual after, {:?}, expected deducted: {:?}",
account,
actual_reserved_before,
actual_reserved_after,
expected_amount_deducted,
);
}
Ok(())
}
}
#[cfg(all(feature = "try-runtime", test))]
mod test {
use super::*;
use crate::{
tests::{new_test_ext, Balances, Test},
DepositOf, Voting, VotingOf,
};
use frame_support::{
assert_ok, parameter_types,
traits::{Currency, OnRuntimeUpgrade, ReservableCurrency, WithdrawReasons},
BoundedVec,
};
use frame_system::pallet_prelude::BlockNumberFor;
use sp_core::ConstU32;
parameter_types! {
const PalletName: &'static str = "Democracy";
}
struct UnlockConfigImpl;
impl super::UnlockConfig for UnlockConfigImpl {
type Currency = Balances;
type MaxVotes = ConstU32<100>;
type MaxDeposits = ConstU32<1000>;
type AccountId = u64;
type BlockNumber = BlockNumberFor<Test>;
type DbWeight = ();
type PalletName = PalletName;
}
#[test]
fn unreserve_works_for_depositor() {
let depositor_0 = 10;
let depositor_1 = 11;
let deposit = 25;
let depositor_0_initial_reserved = 0;
let depositor_1_initial_reserved = 15;
let initial_balance = 100_000;
new_test_ext().execute_with(|| {
<Test as crate::Config>::Currency::make_free_balance_be(&depositor_0, initial_balance);
<Test as crate::Config>::Currency::make_free_balance_be(&depositor_1, initial_balance);
assert_ok!(<Test as crate::Config>::Currency::reserve(
&depositor_0,
depositor_0_initial_reserved + deposit
));
assert_ok!(<Test as crate::Config>::Currency::reserve(
&depositor_1,
depositor_1_initial_reserved + deposit
));
let depositors =
BoundedVec::<_, <Test as crate::Config>::MaxDeposits>::truncate_from(vec![
depositor_0,
depositor_1,
]);
DepositOf::<Test>::insert(0, (depositors, deposit));
assert_eq!(
<Test as crate::Config>::Currency::reserved_balance(&depositor_0),
depositor_0_initial_reserved + deposit
);
assert_eq!(
<Test as crate::Config>::Currency::reserved_balance(&depositor_1),
depositor_1_initial_reserved + deposit
);
let bytes = UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::pre_upgrade()
.unwrap_or_else(|e| panic!("pre_upgrade failed: {:?}", e));
UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::on_runtime_upgrade();
assert_ok!(UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::post_upgrade(bytes));
assert_eq!(
<Test as crate::Config>::Currency::reserved_balance(&depositor_0),
depositor_0_initial_reserved
);
assert_eq!(
<Test as crate::Config>::Currency::reserved_balance(&depositor_1),
depositor_1_initial_reserved
);
});
}
#[test]
fn unlock_works_for_voter() {
let voter = 10;
let stake = 25;
let initial_locks = vec![(b"somethin", 10)];
let initial_balance = 100_000;
new_test_ext().execute_with(|| {
<Test as crate::Config>::Currency::make_free_balance_be(&voter, initial_balance);
for lock in initial_locks.clone() {
<Test as crate::Config>::Currency::set_lock(
*lock.0,
&voter,
lock.1,
WithdrawReasons::all(),
);
}
VotingOf::<Test>::insert(voter, Voting::default());
<Test as crate::Config>::Currency::set_lock(
DEMOCRACY_ID,
&voter,
stake,
WithdrawReasons::all(),
);
let mut voter_all_locks = initial_locks.clone();
voter_all_locks.push((&DEMOCRACY_ID, stake));
assert_eq!(
<Test as crate::Config>::Currency::locks(&voter)
.iter()
.map(|lock| (&lock.id, lock.amount))
.collect::<Vec<_>>(),
voter_all_locks
);
let bytes = UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::pre_upgrade()
.unwrap_or_else(|e| panic!("pre_upgrade failed: {:?}", e));
UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::on_runtime_upgrade();
assert_ok!(UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::post_upgrade(bytes));
assert_eq!(
<Test as crate::Config>::Currency::locks(&voter)
.iter()
.map(|lock| (&lock.id, lock.amount))
.collect::<Vec<_>>(),
initial_locks
);
});
}
}