diff --git a/frame/contracts/src/migration.rs b/frame/contracts/src/migration.rs index cd8f1dd1c9cd4..9aee4725a4ba1 100644 --- a/frame/contracts/src/migration.rs +++ b/frame/contracts/src/migration.rs @@ -15,18 +15,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{BalanceOf, CodeHash, Config, Pallet, TrieId, Weight}; use codec::{Decode, Encode}; -use frame_support::{ - codec, - pallet_prelude::*, - storage::migration, - storage_alias, - traits::{Get, OnRuntimeUpgrade}, - Identity, Twox64Concat, -}; -use sp_runtime::traits::Saturating; -use sp_std::{marker::PhantomData, prelude::*}; +use core::cmp::{max, min}; +use frame_support::{codec, Identity, Twox64Concat, storage_alias, LOG_TARGET}; +use frame_support::pallet_prelude::*; +use frame_support::storage::migration; +use frame_support::traits::fungible::Inspect; +use frame_support::traits::{Get, OnRuntimeUpgrade, ReservableCurrency, Currency, ExistenceRequirement}; +use sp_core::hexdisplay::HexDisplay; +use sp_runtime::traits::{Saturating, Zero}; +use sp_runtime::Perbill; +use sp_std::marker::PhantomData; +use sp_std::prelude::*; +use sp_std::ops::Deref; + +use crate::{AccountIdOf, BalanceOf, CodeHash, Config, Pallet, TrieId, Weight}; +use crate::address::AddressGenerator; /// Performs all necessary migrations based on `StorageVersion`. pub struct Migration(PhantomData); @@ -59,7 +63,11 @@ impl OnRuntimeUpgrade for Migration { v9::migrate::(&mut weight); } - StorageVersion::new(9).put::>(); + if version < 10 { + v10::migrate::(&mut weight); + } + + StorageVersion::new(10).put::>(); weight.saturating_accrue(T::DbWeight::get().writes(1)); weight @@ -400,6 +408,142 @@ mod v9 { } } +mod v10 { + use super::*; + use v8::ContractInfo as OldContractInfo; + + #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + #[scale_info(skip_type_params(T))] + pub struct ContractInfo { + pub trie_id: TrieId, + deposit_account: DepositAccount, + pub code_hash: CodeHash, + storage_bytes: u32, + storage_items: u32, + pub storage_byte_deposit: BalanceOf, + storage_item_deposit: BalanceOf, + storage_base_deposit: BalanceOf, + } + + #[storage_alias] + pub type ContractInfoOf = + StorageMap, Twox64Concat, ::AccountId, V>; + + #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)] + #[scale_info(skip_type_params(T))] + pub struct DepositAccount(AccountIdOf); + + impl Deref for DepositAccount { + type Target = AccountIdOf; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + pub fn migrate(weight: &mut Weight) { + >>::translate(|account, contract: OldContractInfo| { + log::debug!(target: LOG_TARGET, "Account: 0x{} ", HexDisplay::from(&account.encode())); + // Get the new deposit account address + let deposit_account: DepositAccount = + DepositAccount(T::AddressGenerator::deposit_address(&account)); + + let min_balance = Pallet::::min_balance(); + // Calculate the existing deposit, that should be reserved on the contract account + let old_deposit = contract + .storage_base_deposit + .saturating_add(contract.storage_item_deposit) + .saturating_add(contract.storage_byte_deposit); + + // Unreserve the existing deposit + // Note we can't use repatriate_reserve, because it only works with existing accounts + let remaining = T::Currency::unreserve(&account, old_deposit); + if !remaining.is_zero() { + log::warn!( + target: LOG_TARGET, + "Partially unreserved. Remaining {:?} out of {:?} asked", + remaining, + old_deposit + ); + } + + // Attempt to transfer the old deposit to the deposit account. + let amount = old_deposit + .saturating_sub(min_balance) + .min(T::Currency::reducible_balance(&account, true)); + + let new_deposit = T::Currency::transfer_no_cdd( + &account, + &deposit_account, + amount, + ExistenceRequirement::KeepAlive, + ) + .map(|_| { + log::debug!( + target: LOG_TARGET, + "Transferred deposit ({:?}) to deposit account", + amount + ); + amount + }) + // If it fails we fallback to minting the ED. + .unwrap_or_else(|err| { + log::error!(target: LOG_TARGET, "Failed to transfer the base deposit, reason: {:?}", err); + T::Currency::deposit_creating(&deposit_account, min_balance); + min_balance + }); + + // Calculate the new base_deposit to store in the contract: + // Ideally, it should be the same as the old one + // Ideally, it should be at least 2xED (for the contract and deposit accounts). + // It can't be more than the `new_deposit`. + let new_base_deposit = min( + max(contract.storage_base_deposit, min_balance.saturating_add(min_balance)), + new_deposit, + ); + + // Calculate the ratio to adjust storage_byte and storage_item deposits. + let new_deposit_without_base = new_deposit.saturating_sub(new_base_deposit); + let old_deposit_without_base = + old_deposit.saturating_sub(contract.storage_base_deposit); + let ratio = Perbill::from_rational(new_deposit_without_base, old_deposit_without_base); + + // Calculate the new storage deposits based on the ratio + let storage_byte_deposit = ratio.mul_ceil(contract.storage_byte_deposit); + let storage_item_deposit = ratio.mul_ceil(contract.storage_item_deposit); + + // Recalculate the new base deposit, instead of using new_base_deposit to avoid rounding + // errors + let storage_base_deposit = new_deposit + .saturating_sub(storage_byte_deposit) + .saturating_sub(storage_item_deposit); + + // Reads: One read for each storage item plus the contract info itself. + // Writes: Only the new contract info. + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + + Some(ContractInfo { + trie_id: contract.trie_id, + deposit_account, + code_hash: contract.code_hash, + storage_bytes: contract.storage_bytes, + storage_items: contract.storage_items, + storage_byte_deposit, + storage_item_deposit, + storage_base_deposit, + }) + }); + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade_step() -> Result, TryRuntimeError> { + let sample: Vec<_> = ContractInfoOf::::iter().take(10).collect(); + + log::debug!(target: LOG_TARGET, "Taking sample of {} contracts", sample.len()); + Ok(sample.encode()) + } +} + // Post checks always need to be run against the latest storage version. This is why we // do not scope them in the per version modules. They always need to be ported to the latest // version. @@ -431,6 +575,10 @@ mod post_checks { v9::()?; } + if old_version < 10 { + v10::()?; + } + Ok(()) } @@ -470,4 +618,35 @@ mod post_checks { } Ok(()) } + + fn v10() -> Result<(), &'static str> { + let sample = )> as Decode>::decode(&mut &state[..]) + .expect("pre_upgrade_step provides a valid state; qed"); + + log::debug!(target: LOG_TARGET, "Validating sample of {} contracts", sample.len()); + for (account, old_contract) in sample { + log::debug!(target: LOG_TARGET, "==="); + log::debug!(target: LOG_TARGET, "Account: 0x{} ", HexDisplay::from(&account.encode())); + let contract = ContractInfoOf::::get(&account).unwrap(); + ensure!(old_contract.trie_id == contract.trie_id, "invalid trie_id"); + ensure!(old_contract.code_hash == contract.code_hash, "invalid code_hash"); + ensure!(old_contract.storage_bytes == contract.storage_bytes, "invalid storage_bytes"); + ensure!(old_contract.storage_items == contract.storage_items, "invalid storage_items"); + + let deposit = + <::Currency as frame_support::traits::Currency<_>>::total_balance( + &contract.deposit_account, + ); + ensure!( + deposit == + contract + .storage_base_deposit + .saturating_add(contract.storage_item_deposit) + .saturating_add(contract.storage_byte_deposit), + "deposit mismatch" + ); + } + + Ok(()) + } }