1use alloc::collections::btree_map::BTreeMap;
22use core::iter::Sum;
23use frame_support::{
24 pallet_prelude::OptionQuery,
25 storage_alias,
26 traits::{Currency, LockableCurrency, OnRuntimeUpgrade, ReservableCurrency},
27 weights::RuntimeDbWeight,
28 Parameter, Twox64Concat,
29};
30use sp_runtime::{traits::Zero, Saturating};
31
32#[cfg(feature = "try-runtime")]
33const LOG_TARGET: &str = "runtime::tips::migrations::unreserve_deposits";
34
35type BalanceOf<T, I> =
36 <<T as UnlockConfig<I>>::Currency as Currency<<T as UnlockConfig<I>>::AccountId>>::Balance;
37
38pub trait UnlockConfig<I>: 'static {
40 type Hash: Parameter;
42 type AccountId: Parameter + Ord;
44 type Currency: LockableCurrency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
48 type TipReportDepositBase: sp_core::Get<BalanceOf<Self, I>>;
52 type DataDepositPerByte: sp_core::Get<BalanceOf<Self, I>>;
56 type PalletName: sp_core::Get<&'static str>;
59 type DbWeight: sp_core::Get<RuntimeDbWeight>;
61 type BlockNumber: Parameter + Zero + Copy + Ord;
63}
64
65#[storage_alias(dynamic)]
68type Tips<T: UnlockConfig<I>, I: 'static> = StorageMap<
69 <T as UnlockConfig<I>>::PalletName,
70 Twox64Concat,
71 <T as UnlockConfig<I>>::Hash,
72 crate::OpenTip<
73 <T as UnlockConfig<I>>::AccountId,
74 BalanceOf<T, I>,
75 <T as UnlockConfig<I>>::BlockNumber,
76 <T as UnlockConfig<I>>::Hash,
77 >,
78 OptionQuery,
79>;
80
81pub struct UnreserveDeposits<T: UnlockConfig<I>, I: 'static>(core::marker::PhantomData<(T, I)>);
89
90impl<T: UnlockConfig<I>, I: 'static> UnreserveDeposits<T, I> {
91 fn get_deposits() -> (BTreeMap<T::AccountId, BalanceOf<T, I>>, frame_support::weights::Weight) {
100 use sp_core::Get;
101
102 let mut tips_len = 0;
103 let account_deposits: BTreeMap<T::AccountId, BalanceOf<T, I>> = Tips::<T, I>::iter()
104 .map(|(_hash, open_tip)| open_tip)
105 .fold(BTreeMap::new(), |mut acc, tip| {
106 tips_len.saturating_inc();
108
109 acc.entry(tip.finder).or_insert(Zero::zero()).saturating_accrue(tip.deposit);
111 acc
112 });
113
114 (account_deposits, T::DbWeight::get().reads(tips_len))
115 }
116}
117
118impl<T: UnlockConfig<I>, I: 'static> OnRuntimeUpgrade for UnreserveDeposits<T, I>
119where
120 BalanceOf<T, I>: Sum,
121{
122 #[cfg(feature = "try-runtime")]
136 fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
137 use codec::Encode;
138 use frame_support::ensure;
139
140 let (account_deposits, _) = Self::get_deposits();
142
143 let account_reserved_before: BTreeMap<T::AccountId, BalanceOf<T, I>> = account_deposits
145 .keys()
146 .map(|account| (account.clone(), T::Currency::reserved_balance(&account)))
147 .collect();
148
149 ensure!(
153 account_deposits.iter().all(|(account, deposit)| *deposit <=
154 *account_reserved_before.get(account).unwrap_or(&Zero::zero())),
155 "Deposit amount is greater than reserved amount"
156 );
157
158 let total_deposits_to_unreserve =
160 account_deposits.clone().into_values().sum::<BalanceOf<T, I>>();
161 log::info!(target: LOG_TARGET, "Total accounts: {}", account_deposits.keys().count());
162 log::info!(target: LOG_TARGET, "Total amount to unreserve: {:?}", total_deposits_to_unreserve);
163
164 Ok(account_reserved_before.encode())
167 }
168
169 fn on_runtime_upgrade() -> frame_support::weights::Weight {
171 use frame_support::traits::Get;
172
173 let (account_deposits, initial_reads) = Self::get_deposits();
175
176 for (account, unreserve_amount) in account_deposits.iter() {
178 if unreserve_amount.is_zero() {
179 continue
180 }
181 T::Currency::unreserve(&account, *unreserve_amount);
182 }
183
184 T::DbWeight::get()
185 .reads_writes(account_deposits.len() as u64, account_deposits.len() as u64)
186 .saturating_add(initial_reads)
187 }
188
189 #[cfg(feature = "try-runtime")]
191 fn post_upgrade(
192 account_reserved_before_bytes: alloc::vec::Vec<u8>,
193 ) -> Result<(), sp_runtime::TryRuntimeError> {
194 use codec::Decode;
195
196 let account_reserved_before = BTreeMap::<T::AccountId, BalanceOf<T, I>>::decode(
197 &mut &account_reserved_before_bytes[..],
198 )
199 .map_err(|_| "Failed to decode account_reserved_before_bytes")?;
200
201 let (account_deposits, _) = Self::get_deposits();
203
204 for (account, actual_reserved_before) in account_reserved_before {
206 let actual_reserved_after = T::Currency::reserved_balance(&account);
207 let expected_amount_deducted = *account_deposits
208 .get(&account)
209 .expect("account deposit must exist to be in account_reserved_before, qed");
210 let expected_reserved_after =
211 actual_reserved_before.saturating_sub(expected_amount_deducted);
212
213 if actual_reserved_after != expected_reserved_after {
214 log::error!(
215 target: LOG_TARGET,
216 "Reserved balance for {:?} is incorrect. actual before: {:?}, actual after, {:?}, expected deducted: {:?}",
217 account,
218 actual_reserved_before,
219 actual_reserved_after,
220 expected_amount_deducted
221 );
222 return Err("Reserved balance is incorrect".into())
223 }
224 }
225
226 Ok(())
227 }
228}
229
230#[cfg(all(feature = "try-runtime", test))]
231mod test {
232 use super::*;
233 use crate::{
234 migrations::unreserve_deposits::UnreserveDeposits,
235 tests::{new_test_ext, Balances, RuntimeOrigin, Test, Tips},
236 };
237 use frame_support::{assert_ok, parameter_types, traits::TypedGet};
238 use frame_system::pallet_prelude::BlockNumberFor;
239 use sp_core::ConstU64;
240
241 parameter_types! {
242 const PalletName: &'static str = "Tips";
243 }
244
245 struct UnlockConfigImpl;
246 impl super::UnlockConfig<()> for UnlockConfigImpl {
247 type Currency = Balances;
248 type TipReportDepositBase = ConstU64<1>;
249 type DataDepositPerByte = ConstU64<1>;
250 type Hash = sp_core::H256;
251 type AccountId = u128;
252 type BlockNumber = BlockNumberFor<Test>;
253 type DbWeight = ();
254 type PalletName = PalletName;
255 }
256
257 #[test]
258 fn unreserve_all_funds_works() {
259 let tipper_0 = 0;
260 let tipper_1 = 1;
261 let tipper_0_initial_reserved = 0;
262 let tipper_1_initial_reserved = 5;
263 let recipient = 100;
264 let tip_0_reason = b"what_is_really_not_awesome".to_vec();
265 let tip_1_reason = b"pineapple_on_pizza".to_vec();
266 new_test_ext().execute_with(|| {
267 assert_ok!(<Test as pallet_treasury::Config>::Currency::reserve(
269 &tipper_0,
270 tipper_0_initial_reserved
271 ));
272 assert_ok!(<Test as pallet_treasury::Config>::Currency::reserve(
273 &tipper_1,
274 tipper_1_initial_reserved
275 ));
276
277 assert_ok!(Tips::report_awesome(
279 RuntimeOrigin::signed(tipper_0),
280 tip_0_reason.clone(),
281 recipient
282 ));
283 assert_ok!(Tips::report_awesome(
284 RuntimeOrigin::signed(tipper_1),
285 tip_1_reason.clone(),
286 recipient
287 ));
288
289 assert_eq!(
291 <Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_0),
292 tipper_0_initial_reserved +
293 <Test as crate::Config>::TipReportDepositBase::get() +
294 <Test as crate::Config>::DataDepositPerByte::get() *
295 tip_0_reason.len() as u64
296 );
297 assert_eq!(
298 <Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_1),
299 tipper_1_initial_reserved +
300 <Test as crate::Config>::TipReportDepositBase::get() +
301 <Test as crate::Config>::DataDepositPerByte::get() *
302 tip_1_reason.len() as u64
303 );
304
305 let bytes = match UnreserveDeposits::<UnlockConfigImpl, ()>::pre_upgrade() {
307 Ok(bytes) => bytes,
308 Err(e) => panic!("pre_upgrade failed: {:?}", e),
309 };
310 UnreserveDeposits::<UnlockConfigImpl, ()>::on_runtime_upgrade();
311 assert_ok!(UnreserveDeposits::<UnlockConfigImpl, ()>::post_upgrade(bytes));
312
313 assert_eq!(
315 <Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_0),
316 tipper_0_initial_reserved
317 );
318 assert_eq!(
319 <Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_1),
320 tipper_1_initial_reserved
321 );
322 });
323 }
324}