1use super::PALLET_MIGRATIONS_ID;
38#[cfg(feature = "try-runtime")]
39use crate::BalanceOf;
40use crate::{
41 AccountInfoOf, CodeInfoOf, Config, DeletionQueue, HoldReason, LOG_TARGET, NativeDepositOf,
42 Pallet, TrieId,
43 address::AddressMapper,
44 deposit_payment::Deposit,
45 storage::{AccountType, DeletionQueueItem},
46 weights::WeightInfo,
47};
48use codec::{Decode, Encode, MaxEncodedLen};
49use core::marker::PhantomData;
50use frame_support::{
51 Twox64Concat,
52 migrations::{MigrationId, SteppedMigration, SteppedMigrationError},
53 storage_alias,
54 weights::WeightMeter,
55};
56use scale_info::TypeInfo;
57use sp_core::{H160, H256};
58use sp_runtime::traits::{Saturating, TrailingZeroInput};
59
60extern crate alloc;
61
62#[cfg(feature = "try-runtime")]
63use alloc::{collections::btree_map::BTreeMap, vec::Vec};
64
65#[derive(Clone, Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, Eq, Debug)]
67pub enum Cursor {
68 CodeUpload(H256),
70 Contract(Option<H160>),
74 DeletionQueue(Option<u32>),
78}
79
80pub struct Migration<T>(PhantomData<T>);
82
83impl<T: Config> SteppedMigration for Migration<T> {
84 type Cursor = Cursor;
85 type Identifier = MigrationId<17>;
86
87 fn id() -> Self::Identifier {
88 MigrationId { pallet_id: *PALLET_MIGRATIONS_ID, version_from: 3, version_to: 4 }
89 }
90
91 fn step(
92 mut cursor: Option<Self::Cursor>,
93 meter: &mut WeightMeter,
94 ) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
95 let code_step = <T as Config>::WeightInfo::v4_code_upload_step();
96 let contract_step = <T as Config>::WeightInfo::v4_contract_step();
97 let deletion_queue_step = <T as Config>::WeightInfo::v4_deletion_queue_step();
98 let required = code_step.max(contract_step).max(deletion_queue_step);
99 if !meter.can_consume(required) {
100 return Err(SteppedMigrationError::InsufficientWeight { required });
101 }
102
103 loop {
104 let step_weight = match &cursor {
105 None | Some(Cursor::CodeUpload(_)) => code_step,
106 Some(Cursor::Contract(_)) => contract_step,
107 Some(Cursor::DeletionQueue(_)) => deletion_queue_step,
108 };
109 if meter.try_consume(step_weight).is_err() {
110 break;
111 }
112 cursor = Self::step_once(cursor);
113 if cursor.is_none() {
114 break;
115 }
116 }
117 Ok(cursor)
118 }
119
120 #[cfg(feature = "try-runtime")]
121 fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
122 use crate::deposit_payment::Deposit;
123
124 let mut per_owner: BTreeMap<T::AccountId, BalanceOf<T>> = BTreeMap::new();
125 if T::Deposit::SUPPORTS_PGAS {
126 for (_hash, info) in CodeInfoOf::<T>::iter() {
127 let entry = per_owner.entry(info.owner().clone()).or_default();
128 *entry = entry.saturating_add(info.deposit());
129 }
130 }
131
132 let mut per_contract: BTreeMap<H160, BalanceOf<T>> = BTreeMap::new();
133 for (addr, info) in AccountInfoOf::<T>::iter() {
134 if !matches!(info.account_type, AccountType::Contract(_)) {
135 continue;
136 }
137 let contract = T::AddressMapper::to_account_id(&addr);
138 let total = T::Deposit::total_on_hold(HoldReason::StorageDepositReserve, &contract);
139 per_contract.insert(addr, total);
140 }
141
142 let deletion_queue: BTreeMap<u32, TrieId> = old::DeletionQueue::<T>::iter().collect();
143
144 Ok((per_owner, per_contract, deletion_queue).encode())
145 }
146
147 #[cfg(feature = "try-runtime")]
148 fn post_upgrade(prev: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
149 use crate::deposit_payment::Deposit;
150
151 let (per_owner, per_contract, deletion_queue) = <(
152 BTreeMap<T::AccountId, BalanceOf<T>>,
153 BTreeMap<H160, BalanceOf<T>>,
154 BTreeMap<u32, TrieId>,
155 )>::decode(&mut &prev[..])
156 .expect("Failed to decode pre_upgrade state");
157
158 let pallet_account = Pallet::<T>::account_id();
160 for (owner, expected) in per_owner {
161 let got = NativeDepositOf::<T>::get(&pallet_account, &owner);
162 assert_eq!(
163 got, expected,
164 "v4: NativeDepositOf[pallet][{owner:?}] = {got:?}, expected {expected:?}",
165 );
166 }
167
168 for (addr, expected) in per_contract {
169 let contract = T::AddressMapper::to_account_id(&addr);
170 let total = T::Deposit::total_on_hold(HoldReason::StorageDepositReserve, &contract);
171 assert_eq!(
172 total, expected,
173 "v4: contract {addr:?} total_on_hold changed: {total:?} != pre-migration {expected:?}",
174 );
175 }
176
177 let zero_account = T::AccountId::decode(&mut TrailingZeroInput::zeroes())
178 .expect("zero input decodes to a valid AccountId; qed");
179 for (key, trie_id) in deletion_queue {
180 let got = DeletionQueue::<T>::get(key);
181 let expected = DeletionQueueItem::<T>::new(trie_id, zero_account.clone());
182 assert_eq!(
183 got,
184 Some(expected),
185 "v4: DeletionQueue[{key}] not rewritten into the new format",
186 );
187 }
188 Ok(())
189 }
190}
191
192pub(crate) mod old {
194 use super::*;
195
196 #[storage_alias]
199 pub(crate) type DeletionQueue<T: Config> = StorageMap<Pallet<T>, Twox64Concat, u32, TrieId>;
200}
201
202impl<T: Config> Migration<T> {
203 pub(crate) fn step_once(cursor: Option<Cursor>) -> Option<Cursor> {
206 if !T::Deposit::SUPPORTS_PGAS && !cfg!(feature = "runtime-benchmarks") {
209 return match cursor {
210 None | Some(Cursor::CodeUpload(_)) | Some(Cursor::Contract(_)) => {
211 Some(Cursor::DeletionQueue(None))
212 },
213 Some(Cursor::DeletionQueue(last)) => match Self::step_3_deletion_queue(last) {
214 Some(next) => Some(Cursor::DeletionQueue(Some(next))),
215 None => None,
216 },
217 };
218 }
219
220 match cursor {
221 None | Some(Cursor::CodeUpload(_)) => {
222 let last = if let Some(Cursor::CodeUpload(h)) = cursor { Some(h) } else { None };
223 Self::step_1_code_upload(last)
224 },
225 Some(Cursor::Contract(last)) => Some(match Self::step_2_contract(last) {
226 Some(next) => Cursor::Contract(Some(next)),
227 None => Cursor::DeletionQueue(None),
228 }),
229 Some(Cursor::DeletionQueue(last)) => match Self::step_3_deletion_queue(last) {
230 Some(next) => Some(Cursor::DeletionQueue(Some(next))),
231 None => None,
232 },
233 }
234 }
235
236 fn step_1_code_upload(last: Option<H256>) -> Option<Cursor> {
239 let mut iter = match last {
240 Some(last) => CodeInfoOf::<T>::iter_from(CodeInfoOf::<T>::hashed_key_for(last)),
241 None => CodeInfoOf::<T>::iter(),
242 };
243
244 let Some((hash, info)) = iter.next() else { return Some(Cursor::Contract(None)) };
245
246 let pallet_account = Pallet::<T>::account_id();
247 NativeDepositOf::<T>::mutate(&pallet_account, info.owner(), |entitlement| {
248 *entitlement = entitlement.saturating_add(info.deposit());
249 });
250 Some(Cursor::CodeUpload(hash))
251 }
252
253 fn step_2_contract(last: Option<H160>) -> Option<H160> {
256 use frame_support::traits::fungible::InspectHold;
257
258 let mut iter = match last {
259 Some(last) => AccountInfoOf::<T>::iter_from(AccountInfoOf::<T>::hashed_key_for(last)),
260 None => AccountInfoOf::<T>::iter(),
261 };
262
263 let (addr, info) = iter.next()?;
264 if matches!(info.account_type, AccountType::Contract(_)) {
265 let contract = T::AddressMapper::to_account_id(&addr);
266 let held =
267 T::Currency::balance_on_hold(&HoldReason::StorageDepositReserve.into(), &contract);
268 if let Err(err) = T::Deposit::migrate_native_to_pgas(
269 HoldReason::StorageDepositReserve,
270 &contract,
271 held,
272 ) {
273 log::error!(
274 target: LOG_TARGET,
275 "v4: failed to migrate native -> PGAS deposit for contract {addr:?}: {err:?}",
276 );
277 }
278 }
279 Some(addr)
280 }
281
282 fn step_3_deletion_queue(last: Option<u32>) -> Option<u32> {
287 let mut iter = match last {
288 Some(last) => {
289 old::DeletionQueue::<T>::iter_from(old::DeletionQueue::<T>::hashed_key_for(last))
290 },
291 None => old::DeletionQueue::<T>::iter(),
292 };
293
294 let (key, trie_id) = iter.next()?;
295 let zero_account = T::AccountId::decode(&mut TrailingZeroInput::zeroes())
298 .expect("zero input decodes to a valid AccountId; qed");
299 DeletionQueue::<T>::insert(key, DeletionQueueItem::<T>::new(trie_id, zero_account));
300 Some(key)
301 }
302}
303
304#[cfg(any(feature = "runtime-benchmarks", feature = "try-runtime", test))]
305impl<T: Config> Migration<T> {
306 pub fn run_to_completion() {
308 let mut cursor: Option<Cursor> = None;
309 let mut meter = WeightMeter::new();
310 while let Ok(Some(next)) = <Self as SteppedMigration>::step(cursor, &mut meter) {
311 cursor = Some(next);
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::{
320 CodeInfo, FreezeReason,
321 storage::{AccountInfo, ContractInfo},
322 tests::{Assets, AssetsFreezer, AssetsHolder, ExtBuilder, PGasAssetId, Test},
323 };
324 use frame_support::traits::fungible::{
325 Inspect as _, InspectHold as _, Mutate as _, MutateHold as _,
326 };
327 use sp_runtime::AccountId32;
328
329 type V4 = Migration<Test>;
330
331 fn seed_code_upload(hash: H256, owner: AccountId32, deposit: u128) {
332 let pallet_account = Pallet::<Test>::account_id();
333 let ed = <Test as Config>::Currency::minimum_balance();
334 <Test as Config>::Currency::mint_into(&pallet_account, ed).unwrap();
335 <Test as Config>::Currency::mint_into(&pallet_account, deposit).unwrap();
336 <Test as Config>::Currency::hold(
337 &HoldReason::CodeUploadDepositReserve.into(),
338 &pallet_account,
339 deposit,
340 )
341 .unwrap();
342 CodeInfoOf::<Test>::insert(hash, CodeInfo::<Test>::new_with_deposit(owner, deposit));
343 }
344
345 fn seed_contract(address: H160, code_hash: H256, storage_deposit: u128) {
346 let contract_account = <Test as Config>::AddressMapper::to_account_id(&address);
347 let info = ContractInfo::<Test>::new(&address, 0u32.into(), code_hash).unwrap();
348 AccountInfoOf::<Test>::insert(
349 address,
350 AccountInfo::<Test> { account_type: AccountType::Contract(info), dust: 0 },
351 );
352
353 let ed = <Test as Config>::Currency::minimum_balance();
354 <Test as Config>::Currency::mint_into(&contract_account, ed).unwrap();
355 <Test as Config>::Currency::mint_into(&contract_account, storage_deposit).unwrap();
356 <Test as Config>::Currency::hold(
357 &HoldReason::StorageDepositReserve.into(),
358 &contract_account,
359 storage_deposit,
360 )
361 .unwrap();
362 }
363
364 #[test]
365 fn phase_one_populates_native_deposit_for_code_upload() {
366 ExtBuilder::default().genesis_config(None).build().execute_with(|| {
367 let pallet_account = Pallet::<Test>::account_id();
368 let owner_a = AccountId32::new([1; 32]);
369 let owner_b = AccountId32::new([2; 32]);
370 seed_code_upload(H256::repeat_byte(0xAA), owner_a.clone(), 1_000);
371 seed_code_upload(H256::repeat_byte(0xAB), owner_a.clone(), 500);
372 seed_code_upload(H256::repeat_byte(0xBB), owner_b.clone(), 2_000);
373
374 V4::run_to_completion();
375
376 assert_eq!(
377 NativeDepositOf::<Test>::get(&pallet_account, &owner_a),
378 1_500,
379 "owner_a sum of code deposits"
380 );
381 assert_eq!(
382 NativeDepositOf::<Test>::get(&pallet_account, &owner_b),
383 2_000,
384 "owner_b sum of code deposits"
385 );
386
387 assert_eq!(
388 <Test as Config>::Currency::balance_on_hold(
389 &HoldReason::CodeUploadDepositReserve.into(),
390 &pallet_account,
391 ),
392 3_500,
393 );
394 });
395 }
396
397 #[test]
398 fn phase_two_burns_native_and_mints_pgas_on_contracts() {
399 ExtBuilder::default().genesis_config(None).build().execute_with(|| {
400 let owner = AccountId32::new([1; 32]);
401 let hash = H256::repeat_byte(0xCC);
402 seed_code_upload(hash, owner.clone(), 0);
403
404 let c1 = H160::repeat_byte(0x10);
405 let c2 = H160::repeat_byte(0x20);
406 seed_contract(c1, hash, 700);
407 seed_contract(c2, hash, 1_300);
408
409 let c1_acc = <Test as Config>::AddressMapper::to_account_id(&c1);
410 let c2_acc = <Test as Config>::AddressMapper::to_account_id(&c2);
411
412 let total_issuance_before = <Test as Config>::Currency::total_issuance();
413
414 V4::run_to_completion();
415
416 assert_eq!(
417 <Test as Config>::Currency::balance_on_hold(
418 &HoldReason::StorageDepositReserve.into(),
419 &c1_acc,
420 ),
421 0,
422 );
423 assert_eq!(
424 <Test as Config>::Currency::balance_on_hold(
425 &HoldReason::StorageDepositReserve.into(),
426 &c2_acc,
427 ),
428 0,
429 );
430
431 assert_eq!(
432 total_issuance_before - <Test as Config>::Currency::total_issuance(),
433 700 + 1_300,
434 );
435
436 use frame_support::traits::tokens::fungibles::{Inspect, InspectHold};
437 let pgas_ed = Assets::minimum_balance(PGasAssetId::get());
438 assert_eq!(
439 AssetsHolder::balance_on_hold(
440 PGasAssetId::get(),
441 &HoldReason::StorageDepositReserve.into(),
442 &c1_acc,
443 ),
444 700,
445 );
446 assert_eq!(
447 AssetsHolder::balance_on_hold(
448 PGasAssetId::get(),
449 &HoldReason::StorageDepositReserve.into(),
450 &c2_acc,
451 ),
452 1_300,
453 );
454 assert_eq!(Assets::balance(PGasAssetId::get(), &c1_acc), pgas_ed);
458 assert_eq!(Assets::balance(PGasAssetId::get(), &c2_acc), pgas_ed);
459 use frame_support::traits::tokens::fungibles::InspectFreeze;
460 assert_eq!(
461 AssetsFreezer::balance_frozen(
462 PGasAssetId::get(),
463 &FreezeReason::PGasMinBalance.into(),
464 &c1_acc,
465 ),
466 pgas_ed,
467 );
468 assert_eq!(
469 AssetsFreezer::balance_frozen(
470 PGasAssetId::get(),
471 &FreezeReason::PGasMinBalance.into(),
472 &c2_acc,
473 ),
474 pgas_ed,
475 );
476 });
477 }
478
479 #[test]
480 fn phase_three_rewrites_legacy_deletion_queue_entries() {
481 use crate::{
482 DeletionQueueCounter,
483 storage::{DeletionQueueItem, DeletionQueueManager},
484 };
485
486 ExtBuilder::default().genesis_config(None).build().execute_with(|| {
487 let trie_a: TrieId = vec![0xAA; 16].try_into().unwrap();
488 let trie_b: TrieId = vec![0xBB; 24].try_into().unwrap();
489 old::DeletionQueue::<Test>::insert(0u32, trie_a.clone());
490 old::DeletionQueue::<Test>::insert(1u32, trie_b.clone());
491 let mut q = DeletionQueueManager::<Test>::from_test_values(2, 0);
492 DeletionQueueCounter::<Test>::set(q.clone());
493 let _ = &mut q;
494
495 V4::run_to_completion();
496
497 let zero = AccountId32::new([0u8; 32]);
498 assert_eq!(
499 DeletionQueue::<Test>::get(0u32),
500 Some(DeletionQueueItem::<Test>::new(trie_a, zero.clone())),
501 );
502 assert_eq!(
503 DeletionQueue::<Test>::get(1u32),
504 Some(DeletionQueueItem::<Test>::new(trie_b, zero)),
505 );
506 });
507 }
508
509 #[test]
510 fn eoa_accounts_are_skipped() {
511 use crate::test_utils::{ALICE, ALICE_ADDR, BOB, BOB_ADDR};
512 use frame_support::traits::tokens::fungibles::InspectHold;
513
514 ExtBuilder::default().genesis_config(None).build().execute_with(|| {
515 let _ = <Test as Config>::Currency::mint_into(&ALICE, Pallet::<Test>::min_balance());
516 let _ = <Test as Config>::Currency::mint_into(&BOB, Pallet::<Test>::min_balance());
517 AccountInfoOf::<Test>::insert(
518 ALICE_ADDR,
519 AccountInfo::<Test> { account_type: AccountType::EOA, dust: 0 },
520 );
521
522 let owner = AccountId32::new([1; 32]);
523 let hash = H256::repeat_byte(0xDD);
524 seed_code_upload(hash, owner.clone(), 0);
525 seed_contract(BOB_ADDR, hash, 400);
526
527 V4::run_to_completion();
528
529 assert_eq!(
530 AssetsHolder::balance_on_hold(
531 PGasAssetId::get(),
532 &HoldReason::StorageDepositReserve.into(),
533 &BOB,
534 ),
535 400,
536 );
537 });
538 }
539}