referrerpolicy=no-referrer-when-downgrade

pallet_contracts/migration/
v10.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Don't rely on reserved balances keeping an account alive
19//! See <https://github.com/paritytech/substrate/pull/13369>.
20
21use crate::{
22	exec::AccountIdOf,
23	migration::{IsFinished, MigrationStep},
24	weights::WeightInfo,
25	CodeHash, Config, Pallet, TrieId, Weight, LOG_TARGET,
26};
27use codec::{Decode, Encode};
28use core::{
29	cmp::{max, min},
30	ops::Deref,
31};
32use frame_support::{
33	pallet_prelude::*,
34	storage_alias,
35	traits::{
36		tokens::{fungible::Inspect, Fortitude::Polite, Preservation::Preserve},
37		ExistenceRequirement, ReservableCurrency,
38	},
39	weights::WeightMeter,
40	DefaultNoBound,
41};
42use sp_core::hexdisplay::HexDisplay;
43use sp_runtime::{
44	traits::{Hash, TrailingZeroInput, Zero},
45	Perbill, Saturating,
46};
47
48#[cfg(feature = "try-runtime")]
49use alloc::vec::Vec;
50#[cfg(feature = "try-runtime")]
51use sp_runtime::TryRuntimeError;
52
53mod v9 {
54	use super::*;
55
56	pub type BalanceOf<T, OldCurrency> = <OldCurrency as frame_support::traits::Currency<
57		<T as frame_system::Config>::AccountId,
58	>>::Balance;
59
60	#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
61	#[scale_info(skip_type_params(T, OldCurrency))]
62	pub struct ContractInfo<T: Config, OldCurrency>
63	where
64		OldCurrency: ReservableCurrency<<T as frame_system::Config>::AccountId>,
65	{
66		pub trie_id: TrieId,
67		pub code_hash: CodeHash<T>,
68		pub storage_bytes: u32,
69		pub storage_items: u32,
70		pub storage_byte_deposit: BalanceOf<T, OldCurrency>,
71		pub storage_item_deposit: BalanceOf<T, OldCurrency>,
72		pub storage_base_deposit: BalanceOf<T, OldCurrency>,
73	}
74
75	#[storage_alias]
76	pub type ContractInfoOf<T: Config, OldCurrency> = StorageMap<
77		Pallet<T>,
78		Twox64Concat,
79		<T as frame_system::Config>::AccountId,
80		ContractInfo<T, OldCurrency>,
81	>;
82}
83
84#[cfg(feature = "runtime-benchmarks")]
85pub fn store_old_contract_info<T: Config, OldCurrency>(
86	account: T::AccountId,
87	info: crate::ContractInfo<T>,
88) where
89	OldCurrency: ReservableCurrency<<T as frame_system::Config>::AccountId> + 'static,
90{
91	let info = v9::ContractInfo {
92		trie_id: info.trie_id,
93		code_hash: info.code_hash,
94		storage_bytes: Default::default(),
95		storage_items: Default::default(),
96		storage_byte_deposit: Default::default(),
97		storage_item_deposit: Default::default(),
98		storage_base_deposit: Default::default(),
99	};
100	v9::ContractInfoOf::<T, OldCurrency>::insert(account, info);
101}
102
103#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)]
104#[scale_info(skip_type_params(T))]
105pub struct DepositAccount<T: Config>(AccountIdOf<T>);
106
107impl<T: Config> Deref for DepositAccount<T> {
108	type Target = AccountIdOf<T>;
109
110	fn deref(&self) -> &Self::Target {
111		&self.0
112	}
113}
114
115#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
116#[scale_info(skip_type_params(T, OldCurrency))]
117pub struct ContractInfo<T: Config, OldCurrency>
118where
119	OldCurrency: ReservableCurrency<<T as frame_system::Config>::AccountId>,
120{
121	pub trie_id: TrieId,
122	deposit_account: DepositAccount<T>,
123	pub code_hash: CodeHash<T>,
124	storage_bytes: u32,
125	storage_items: u32,
126	pub storage_byte_deposit: v9::BalanceOf<T, OldCurrency>,
127	storage_item_deposit: v9::BalanceOf<T, OldCurrency>,
128	storage_base_deposit: v9::BalanceOf<T, OldCurrency>,
129}
130
131#[derive(Encode, Decode, MaxEncodedLen, DefaultNoBound)]
132pub struct Migration<T: Config, OldCurrency = ()> {
133	last_account: Option<T::AccountId>,
134	_phantom: PhantomData<(T, OldCurrency)>,
135}
136
137#[storage_alias]
138type ContractInfoOf<T: Config, OldCurrency> = StorageMap<
139	Pallet<T>,
140	Twox64Concat,
141	<T as frame_system::Config>::AccountId,
142	ContractInfo<T, OldCurrency>,
143>;
144
145/// Formula: `hash("contract_depo_v1" ++ contract_addr)`
146fn deposit_address<T: Config>(
147	contract_addr: &<T as frame_system::Config>::AccountId,
148) -> <T as frame_system::Config>::AccountId {
149	let entropy = (b"contract_depo_v1", contract_addr)
150		.using_encoded(<T as frame_system::Config>::Hashing::hash);
151	Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
152		.expect("infinite length input; no invalid inputs for type; qed")
153}
154
155impl<T: Config, OldCurrency: 'static> MigrationStep for Migration<T, OldCurrency>
156where
157	OldCurrency: ReservableCurrency<<T as frame_system::Config>::AccountId>
158		+ Inspect<<T as frame_system::Config>::AccountId, Balance = v9::BalanceOf<T, OldCurrency>>,
159{
160	const VERSION: u16 = 10;
161
162	fn max_step_weight() -> Weight {
163		T::WeightInfo::v10_migration_step()
164	}
165
166	fn step(&mut self, meter: &mut WeightMeter) -> IsFinished {
167		let mut iter = if let Some(last_account) = self.last_account.take() {
168			v9::ContractInfoOf::<T, OldCurrency>::iter_from(
169				v9::ContractInfoOf::<T, OldCurrency>::hashed_key_for(last_account),
170			)
171		} else {
172			v9::ContractInfoOf::<T, OldCurrency>::iter()
173		};
174
175		if let Some((account, contract)) = iter.next() {
176			let min_balance = <OldCurrency as frame_support::traits::Currency<
177				<T as frame_system::Config>::AccountId,
178			>>::minimum_balance();
179			log::debug!(target: LOG_TARGET, "Account: 0x{} ", HexDisplay::from(&account.encode()));
180
181			// Get the new deposit account address
182			let deposit_account: DepositAccount<T> = DepositAccount(deposit_address::<T>(&account));
183
184			// Calculate the existing deposit, that should be reserved on the contract account
185			let old_deposit = contract
186				.storage_base_deposit
187				.saturating_add(contract.storage_item_deposit)
188				.saturating_add(contract.storage_byte_deposit);
189
190			// Unreserve the existing deposit
191			// Note we can't use repatriate_reserve, because it only works with existing accounts
192			let remaining = OldCurrency::unreserve(&account, old_deposit);
193			if !remaining.is_zero() {
194				log::warn!(
195					target: LOG_TARGET,
196					"Partially unreserved. Remaining {:?} out of {:?} asked",
197					remaining,
198					old_deposit
199				);
200			}
201
202			// Attempt to transfer the old deposit to the deposit account.
203			let amount = old_deposit
204				.saturating_sub(min_balance)
205				.min(OldCurrency::reducible_balance(&account, Preserve, Polite));
206
207			let new_deposit = OldCurrency::transfer(
208				&account,
209				&deposit_account,
210				amount,
211				ExistenceRequirement::KeepAlive,
212			)
213			.map(|_| {
214				log::debug!(
215					target: LOG_TARGET,
216					"Transferred deposit ({:?}) to deposit account",
217					amount
218				);
219				amount
220			})
221			// If it fails we fallback to minting the ED.
222			.unwrap_or_else(|err| {
223				log::error!(
224					target: LOG_TARGET,
225					"Failed to transfer the base deposit, reason: {:?}",
226					err
227				);
228				let _ = OldCurrency::deposit_creating(&deposit_account, min_balance);
229				min_balance
230			});
231
232			// Calculate the new base_deposit to store in the contract:
233			// Ideally, it should be the same as the old one
234			// Ideally, it should be at least 2xED (for the contract and deposit accounts).
235			// It can't be more than the `new_deposit`.
236			let new_base_deposit = min(
237				max(contract.storage_base_deposit, min_balance.saturating_add(min_balance)),
238				new_deposit,
239			);
240
241			// Calculate the ratio to adjust storage_byte and storage_item deposits.
242			let new_deposit_without_base = new_deposit.saturating_sub(new_base_deposit);
243			let old_deposit_without_base =
244				old_deposit.saturating_sub(contract.storage_base_deposit);
245			let ratio = Perbill::from_rational(new_deposit_without_base, old_deposit_without_base);
246
247			// Calculate the new storage deposits based on the ratio
248			let storage_byte_deposit = ratio.mul_ceil(contract.storage_byte_deposit);
249			let storage_item_deposit = ratio.mul_ceil(contract.storage_item_deposit);
250
251			// Recalculate the new base deposit, instead of using new_base_deposit to avoid rounding
252			// errors
253			let storage_base_deposit = new_deposit
254				.saturating_sub(storage_byte_deposit)
255				.saturating_sub(storage_item_deposit);
256
257			let new_contract_info = ContractInfo {
258				trie_id: contract.trie_id,
259				deposit_account,
260				code_hash: contract.code_hash,
261				storage_bytes: contract.storage_bytes,
262				storage_items: contract.storage_items,
263				storage_byte_deposit,
264				storage_item_deposit,
265				storage_base_deposit,
266			};
267
268			ContractInfoOf::<T, OldCurrency>::insert(&account, new_contract_info);
269
270			// Store last key for next migration step
271			self.last_account = Some(account);
272
273			meter.consume(T::WeightInfo::v10_migration_step());
274			IsFinished::No
275		} else {
276			log::debug!(target: LOG_TARGET, "Done Migrating contract info");
277			meter.consume(T::WeightInfo::v10_migration_step());
278			IsFinished::Yes
279		}
280	}
281
282	#[cfg(feature = "try-runtime")]
283	fn pre_upgrade_step() -> Result<Vec<u8>, TryRuntimeError> {
284		let sample: Vec<_> = v9::ContractInfoOf::<T, OldCurrency>::iter().take(10).collect();
285
286		log::debug!(target: LOG_TARGET, "Taking sample of {} contracts", sample.len());
287		Ok(sample.encode())
288	}
289
290	#[cfg(feature = "try-runtime")]
291	fn post_upgrade_step(state: Vec<u8>) -> Result<(), TryRuntimeError> {
292		let sample = <Vec<(T::AccountId, v9::ContractInfo<T, OldCurrency>)> as Decode>::decode(
293			&mut &state[..],
294		)
295		.expect("pre_upgrade_step provides a valid state; qed");
296
297		log::debug!(target: LOG_TARGET, "Validating sample of {} contracts", sample.len());
298		for (account, old_contract) in sample {
299			log::debug!(target: LOG_TARGET, "===");
300			log::debug!(target: LOG_TARGET, "Account: 0x{} ", HexDisplay::from(&account.encode()));
301			let contract = ContractInfoOf::<T, OldCurrency>::get(&account).unwrap();
302			ensure!(old_contract.trie_id == contract.trie_id, "invalid trie_id");
303			ensure!(old_contract.code_hash == contract.code_hash, "invalid code_hash");
304			ensure!(old_contract.storage_bytes == contract.storage_bytes, "invalid storage_bytes");
305			ensure!(old_contract.storage_items == contract.storage_items, "invalid storage_items");
306
307			let deposit = <OldCurrency as frame_support::traits::Currency<_>>::total_balance(
308				&contract.deposit_account,
309			);
310			ensure!(
311				deposit ==
312					contract
313						.storage_base_deposit
314						.saturating_add(contract.storage_item_deposit)
315						.saturating_add(contract.storage_byte_deposit),
316				"deposit mismatch"
317			);
318		}
319
320		Ok(())
321	}
322}